encore-infrastructure by encoredev/skills
npx skills add https://github.com/encoredev/skills --skill encore-infrastructureEncore.ts 使用声明式基础设施——您在代码中定义资源,Encore 负责配置:
encore run) - Encore 在 Docker 中运行基础设施(Postgres、Redis 等)所有基础设施必须在包级别(文件顶部)声明,不能在函数内部。
import { SQLDatabase } from "encore.dev/storage/sqldb";
// 正确:包级别
const db = new SQLDatabase("mydb", {
migrations: "./migrations",
});
// 错误:在函数内部
async function setup() {
const db = new SQLDatabase("mydb", { migrations: "./migrations" });
}
在 migrations/ 目录中创建迁移文件:
service/
├── encore.service.ts
├── api.ts
├── db.ts
└── migrations/
├── 001_create_users.up.sql
└── 002_add_email_index.up.sql
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
迁移文件命名格式:{数字}_{描述}.up.sql
import { Topic } from "encore.dev/pubsub";
interface OrderCreatedEvent {
orderId: string;
userId: string;
total: number;
}
// 包级别声明
export const orderCreated = new Topic<OrderCreatedEvent>("order-created", {
deliveryGuarantee: "at-least-once",
});
await orderCreated.publish({
orderId: "123",
userId: "user-456",
total: 99.99,
});
import { Subscription } from "encore.dev/pubsub";
const _ = new Subscription(orderCreated, "send-confirmation-email", {
handler: async (event) => {
await sendEmail(event.userId, event.orderId);
},
});
使用 Attribute<T> 标记应作为消息属性的字段(用于过滤/排序):
import { Topic, Attribute } from "encore.dev/pubsub";
interface CartEvent {
cartId: Attribute<string>; // 用于排序
userId: string;
action: "add" | "remove";
productId: string;
}
// 有序主题 - 相同 cartId 的事件按顺序传递
export const cartEvents = new Topic<CartEvent>("cart-events", {
deliveryGuarantee: "at-least-once",
orderingAttribute: "cartId",
});
将主题访问权限传递给其他代码,同时保持静态分析:
import { Publisher } from "encore.dev/pubsub";
// 创建具有发布权限的引用
const publisherRef = orderCreated.ref<Publisher>();
// 使用引用
async function notifyOrder(ref: typeof publisherRef, orderId: string) {
await ref.publish({ orderId, userId: "123", total: 99.99 });
}
import { CronJob } from "encore.dev/cron";
import { api } from "encore.dev/api";
// 要调用的端点
export const cleanupExpiredSessions = api(
{ expose: false },
async (): Promise<void> => {
// 清理逻辑
}
);
// 包级别定时任务声明
const _ = new CronJob("cleanup-sessions", {
title: "清理过期会话",
schedule: "0 * * * *", // 每小时
endpoint: cleanupExpiredSessions,
});
| 格式 | 示例 | 描述 |
|---|---|---|
every | "1h", "30m" | 简单间隔(必须能整除 24 小时) |
schedule | "0 9 * * 1" | Cron 表达式(每周一上午 9 点) |
import { Bucket } from "encore.dev/storage/objects";
// 包级别
export const uploads = new Bucket("user-uploads", {
versioned: false, // 设置为 true 可保留对象的多个版本
});
// 公共存储桶(文件可通过公共 URL 访问)
export const publicAssets = new Bucket("public-assets", {
public: true,
versioned: false,
});
// 上传
const attrs = await uploads.upload("path/to/file.jpg", buffer, {
contentType: "image/jpeg",
});
// 下载
const data = await uploads.download("path/to/file.jpg");
// 检查是否存在
const exists = await uploads.exists("path/to/file.jpg");
// 获取属性(大小、内容类型、ETag)
const attrs = await uploads.attrs("path/to/file.jpg");
// 删除
await uploads.remove("path/to/file.jpg");
// 列出对象
for await (const entry of uploads.list({})) {
console.log(entry.key, entry.size);
}
// 公共 URL(仅适用于公共存储桶)
const url = publicAssets.publicUrl("image.jpg");
生成临时 URL 用于上传/下载,而无需暴露您的存储桶:
// 签名上传 URL(2 小时后过期)
const uploadUrl = await uploads.signedUploadUrl("user-uploads/avatar.jpg", { ttl: 7200 });
// 签名下载 URL
const downloadUrl = await uploads.signedDownloadUrl("documents/report.pdf", { ttl: 7200 });
将具有特定权限的存储桶访问权限传递给其他代码:
import { Uploader, Downloader } from "encore.dev/storage/objects";
// 创建仅具有上传权限的引用
const uploaderRef = uploads.ref<Uploader>();
// 创建仅具有下载权限的引用
const downloaderRef = uploads.ref<Downloader>();
// 权限类型:Downloader、Uploader、Lister、Attrser、Remover、
// SignedDownloader、SignedUploader、ReadWriter
import { CacheCluster } from "encore.dev/storage/cache";
// 包级别
const cluster = new CacheCluster("my-cache", {
evictionPolicy: "allkeys-lru",
});
引用在另一个服务中定义的集群:
const cluster = CacheCluster.named("my-cache");
驱逐策略:"allkeys-lru"(默认)、"noeviction"、"allkeys-lfu"、"allkeys-random"、"volatile-lru"、"volatile-lfu"、"volatile-ttl"、"volatile-random"。
每个键空间都有一个键类型(用于生成 Redis 键)和一个值类型。
import {
StringKeyspace,
IntKeyspace,
FloatKeyspace,
StructKeyspace,
StringListKeyspace,
NumberListKeyspace,
StringSetKeyspace,
NumberSetKeyspace,
expireIn,
} from "encore.dev/storage/cache";
// 字符串值
const tokens = new StringKeyspace<{ tokenId: string }>(cluster, {
keyPattern: "token/:tokenId",
defaultExpiry: expireIn(3600 * 1000), // 1 小时,以毫秒为单位
});
await tokens.set({ tokenId: "abc" }, "value");
const val = await tokens.get({ tokenId: "abc" }); // 未命中时返回 undefined
await tokens.delete({ tokenId: "abc" });
// 整数值(支持递增/递减)
const counters = new IntKeyspace<{ userId: string }>(cluster, {
keyPattern: "requests/:userId",
defaultExpiry: expireIn(10 * 1000),
});
const count = await counters.increment({ userId: "user123" }, 1);
await counters.decrement({ userId: "user123" }, 1);
// 浮点值
const scores = new FloatKeyspace<{ oddsId: string }>(cluster, {
keyPattern: "odds/:oddsId",
});
// 结构化数据(以 JSON 格式存储)
interface UserProfile {
name: string;
email: string;
}
const profiles = new StructKeyspace<{ userId: string }, UserProfile>(cluster, {
keyPattern: "profile/:userId",
defaultExpiry: expireIn(3600 * 1000),
});
await profiles.set({ userId: "123" }, { name: "Alice", email: "alice@example.com" });
// 列表
const recentItems = new StringListKeyspace<{ userId: string }>(cluster, {
keyPattern: "recent/:userId",
});
await recentItems.pushRight({ userId: "user123" }, "item1", "item2");
const items = await recentItems.getRange({ userId: "user123" }, 0, -1);
// 集合
const tags = new StringSetKeyspace<{ articleId: string }>(cluster, {
keyPattern: "tags/:articleId",
});
await tags.add({ articleId: "post1" }, "typescript", "encore", "backend");
const hasTag = await tags.contains({ articleId: "post1" }, "typescript");
interface ResourceKey {
userId: string;
resourcePath: string;
}
const resourceRequests = new IntKeyspace<ResourceKey>(cluster, {
keyPattern: "requests/:userId/:resourcePath",
defaultExpiry: expireIn(10 * 1000),
});
import {
expireIn, // 毫秒
expireInSeconds,
expireInMinutes,
expireInHours,
expireDailyAt, // 每天特定的 UTC 时间
neverExpire,
keepTTL, // 更新时保留现有的 TTL
} from "encore.dev/storage/cache";
// 覆盖默认过期时间
await keyspace.set(key, value, { expiry: expireInMinutes(30) });
// 保留现有的 TTL
await keyspace.set(key, value, { expiry: keepTTL });
// 仅当键不存在时设置(否则抛出 CacheKeyExists)
await keyspace.setIfNotExists(key, value);
// 仅当键已存在时设置(否则抛出 CacheMiss)
await keyspace.replace(key, value);
import { CacheMiss, CacheKeyExists } from "encore.dev/storage/cache";
// get() 在未命中时返回 undefined(不抛出异常)
const value = await keyspace.get(key);
// replace() 在键不存在时抛出 CacheMiss
// setIfNotExists() 在键已存在时抛出 CacheKeyExists
import { secret } from "encore.dev/config";
// 包级别
const stripeKey = secret("StripeSecretKey");
// 使用方式(作为函数调用)
const key = stripeKey();
通过 CLI 设置密钥:
encore secret set --type prod StripeSecretKey
expose: false(仅内部使用)每周安装量
167
代码仓库
GitHub 星标数
20
首次出现
2026年1月21日
安全审计
安装于
codex130
opencode128
gemini-cli124
claude-code112
github-copilot108
cursor102
Encore.ts uses declarative infrastructure - you define resources in code and Encore handles provisioning:
encore run) - Encore runs infrastructure in Docker (Postgres, Redis, etc.)All infrastructure must be declared at package level (top of file), not inside functions.
import { SQLDatabase } from "encore.dev/storage/sqldb";
// CORRECT: Package level
const db = new SQLDatabase("mydb", {
migrations: "./migrations",
});
// WRONG: Inside function
async function setup() {
const db = new SQLDatabase("mydb", { migrations: "./migrations" });
}
Create migrations in the migrations/ directory:
service/
├── encore.service.ts
├── api.ts
├── db.ts
└── migrations/
├── 001_create_users.up.sql
└── 002_add_email_index.up.sql
Migration naming: {number}_{description}.up.sql
import { Topic } from "encore.dev/pubsub";
interface OrderCreatedEvent {
orderId: string;
userId: string;
total: number;
}
// Package level declaration
export const orderCreated = new Topic<OrderCreatedEvent>("order-created", {
deliveryGuarantee: "at-least-once",
});
await orderCreated.publish({
orderId: "123",
userId: "user-456",
total: 99.99,
});
import { Subscription } from "encore.dev/pubsub";
const _ = new Subscription(orderCreated, "send-confirmation-email", {
handler: async (event) => {
await sendEmail(event.userId, event.orderId);
},
});
Use Attribute<T> for fields that should be message attributes (for filtering/ordering):
import { Topic, Attribute } from "encore.dev/pubsub";
interface CartEvent {
cartId: Attribute<string>; // Used for ordering
userId: string;
action: "add" | "remove";
productId: string;
}
// Ordered topic - events with same cartId delivered in order
export const cartEvents = new Topic<CartEvent>("cart-events", {
deliveryGuarantee: "at-least-once",
orderingAttribute: "cartId",
});
Pass topic access to other code while maintaining static analysis:
import { Publisher } from "encore.dev/pubsub";
// Create a reference with publish permission
const publisherRef = orderCreated.ref<Publisher>();
// Use the reference
async function notifyOrder(ref: typeof publisherRef, orderId: string) {
await ref.publish({ orderId, userId: "123", total: 99.99 });
}
import { CronJob } from "encore.dev/cron";
import { api } from "encore.dev/api";
// The endpoint to call
export const cleanupExpiredSessions = api(
{ expose: false },
async (): Promise<void> => {
// Cleanup logic
}
);
// Package level cron declaration
const _ = new CronJob("cleanup-sessions", {
title: "Clean up expired sessions",
schedule: "0 * * * *", // Every hour
endpoint: cleanupExpiredSessions,
});
| Format | Example | Description |
|---|---|---|
every | "1h", "30m" | Simple interval (must divide 24h evenly) |
schedule | "0 9 * * 1" | Cron expression (9am every Monday) |
import { Bucket } from "encore.dev/storage/objects";
// Package level
export const uploads = new Bucket("user-uploads", {
versioned: false, // Set to true to keep multiple versions of objects
});
// Public bucket (files accessible via public URL)
export const publicAssets = new Bucket("public-assets", {
public: true,
versioned: false,
});
// Upload
const attrs = await uploads.upload("path/to/file.jpg", buffer, {
contentType: "image/jpeg",
});
// Download
const data = await uploads.download("path/to/file.jpg");
// Check existence
const exists = await uploads.exists("path/to/file.jpg");
// Get attributes (size, content type, ETag)
const attrs = await uploads.attrs("path/to/file.jpg");
// Delete
await uploads.remove("path/to/file.jpg");
// List objects
for await (const entry of uploads.list({})) {
console.log(entry.key, entry.size);
}
// Public URL (only for public buckets)
const url = publicAssets.publicUrl("image.jpg");
Generate temporary URLs for upload/download without exposing your bucket:
// Signed upload URL (expires in 2 hours)
const uploadUrl = await uploads.signedUploadUrl("user-uploads/avatar.jpg", { ttl: 7200 });
// Signed download URL
const downloadUrl = await uploads.signedDownloadUrl("documents/report.pdf", { ttl: 7200 });
Pass bucket access with specific permissions to other code:
import { Uploader, Downloader } from "encore.dev/storage/objects";
// Create a reference with upload permission only
const uploaderRef = uploads.ref<Uploader>();
// Create a reference with download permission only
const downloaderRef = uploads.ref<Downloader>();
// Permission types: Downloader, Uploader, Lister, Attrser, Remover,
// SignedDownloader, SignedUploader, ReadWriter
import { CacheCluster } from "encore.dev/storage/cache";
// Package level
const cluster = new CacheCluster("my-cache", {
evictionPolicy: "allkeys-lru",
});
Reference a cluster defined in another service:
const cluster = CacheCluster.named("my-cache");
Eviction policies: "allkeys-lru" (default), "noeviction", "allkeys-lfu", "allkeys-random", "volatile-lru", "volatile-lfu", "volatile-ttl", "volatile-random".
Each keyspace has a key type (used to generate the Redis key) and a value type.
import {
StringKeyspace,
IntKeyspace,
FloatKeyspace,
StructKeyspace,
StringListKeyspace,
NumberListKeyspace,
StringSetKeyspace,
NumberSetKeyspace,
expireIn,
} from "encore.dev/storage/cache";
// String values
const tokens = new StringKeyspace<{ tokenId: string }>(cluster, {
keyPattern: "token/:tokenId",
defaultExpiry: expireIn(3600 * 1000), // 1 hour in ms
});
await tokens.set({ tokenId: "abc" }, "value");
const val = await tokens.get({ tokenId: "abc" }); // undefined on miss
await tokens.delete({ tokenId: "abc" });
// Integer values (supports increment/decrement)
const counters = new IntKeyspace<{ userId: string }>(cluster, {
keyPattern: "requests/:userId",
defaultExpiry: expireIn(10 * 1000),
});
const count = await counters.increment({ userId: "user123" }, 1);
await counters.decrement({ userId: "user123" }, 1);
// Float values
const scores = new FloatKeyspace<{ oddsId: string }>(cluster, {
keyPattern: "odds/:oddsId",
});
// Structured data (stored as JSON)
interface UserProfile {
name: string;
email: string;
}
const profiles = new StructKeyspace<{ userId: string }, UserProfile>(cluster, {
keyPattern: "profile/:userId",
defaultExpiry: expireIn(3600 * 1000),
});
await profiles.set({ userId: "123" }, { name: "Alice", email: "alice@example.com" });
// Lists
const recentItems = new StringListKeyspace<{ userId: string }>(cluster, {
keyPattern: "recent/:userId",
});
await recentItems.pushRight({ userId: "user123" }, "item1", "item2");
const items = await recentItems.getRange({ userId: "user123" }, 0, -1);
// Sets
const tags = new StringSetKeyspace<{ articleId: string }>(cluster, {
keyPattern: "tags/:articleId",
});
await tags.add({ articleId: "post1" }, "typescript", "encore", "backend");
const hasTag = await tags.contains({ articleId: "post1" }, "typescript");
interface ResourceKey {
userId: string;
resourcePath: string;
}
const resourceRequests = new IntKeyspace<ResourceKey>(cluster, {
keyPattern: "requests/:userId/:resourcePath",
defaultExpiry: expireIn(10 * 1000),
});
import {
expireIn, // milliseconds
expireInSeconds,
expireInMinutes,
expireInHours,
expireDailyAt, // specific UTC time each day
neverExpire,
keepTTL, // keep existing TTL when updating
} from "encore.dev/storage/cache";
// Override default expiry
await keyspace.set(key, value, { expiry: expireInMinutes(30) });
// Keep existing TTL
await keyspace.set(key, value, { expiry: keepTTL });
// Only set if key doesn't exist (throws CacheKeyExists otherwise)
await keyspace.setIfNotExists(key, value);
// Only set if key already exists (throws CacheMiss otherwise)
await keyspace.replace(key, value);
import { CacheMiss, CacheKeyExists } from "encore.dev/storage/cache";
// get() returns undefined on miss (does not throw)
const value = await keyspace.get(key);
// replace() throws CacheMiss if key doesn't exist
// setIfNotExists() throws CacheKeyExists if key already exists
import { secret } from "encore.dev/config";
// Package level
const stripeKey = secret("StripeSecretKey");
// Usage (call as function)
const key = stripeKey();
Set secrets via CLI:
encore secret set --type prod StripeSecretKey
expose: false (internal only)Weekly Installs
167
Repository
GitHub Stars
20
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex130
opencode128
gemini-cli124
claude-code112
github-copilot108
cursor102
Azure Data Explorer (Kusto) 查询技能:KQL数据分析、日志遥测与时间序列处理
125,100 周安装