| name | encore-infrastructure |
| description | Declare databases, Pub/Sub, cron jobs, caching, object storage, and secrets with Encore.ts. |
Encore Infrastructure Declaration
Instructions
Encore.ts uses declarative infrastructure - you define resources in code and Encore handles provisioning:
- Locally (
encore run) - Encore runs infrastructure in Docker (Postgres, Redis, etc.)
- Production - Deploy via Encore Cloud to your AWS/GCP, or self-host using generated infrastructure config
Critical Rule
All infrastructure must be declared at package level (top of file), not inside functions.
Databases (PostgreSQL)
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
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
Pub/Sub
Topics
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",
});
Publishing
await orderCreated.publish({
orderId: "123",
userId: "user-456",
total: 99.99,
});
Subscriptions
import { Subscription } from "encore.dev/pubsub";
const _ = new Subscription(orderCreated, "send-confirmation-email", {
handler: async (event) => {
await sendEmail(event.userId, event.orderId);
},
});
Message Attributes
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>;
userId: string;
action: "add" | "remove";
productId: string;
}
export const cartEvents = new Topic<CartEvent>("cart-events", {
deliveryGuarantee: "at-least-once",
orderingAttribute: "cartId",
});
Topic References
Pass topic access to other code while maintaining static analysis:
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 });
}
Cron Jobs
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: "Clean up expired sessions",
schedule: "0 * * * *",
endpoint: cleanupExpiredSessions,
});
Schedule Formats
| Format | Example | Description |
|---|
every | "1h", "30m" | Simple interval (must divide 24h evenly) |
schedule | "0 9 * * 1" | Cron expression (9am every Monday) |
Object Storage
import { Bucket } from "encore.dev/storage/objects";
export const uploads = new Bucket("user-uploads", {
versioned: false,
});
export const publicAssets = new Bucket("public-assets", {
public: true,
versioned: false,
});
Operations
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");
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);
}
const url = publicAssets.publicUrl("image.jpg");
Signed URLs
Generate temporary URLs for upload/download without exposing your bucket:
const uploadUrl = await uploads.signedUploadUrl("user-uploads/avatar.jpg", { ttl: 7200 });
const downloadUrl = await uploads.signedDownloadUrl("documents/report.pdf", { ttl: 7200 });
Bucket References
Pass bucket access with specific permissions to other code:
import { Uploader, Downloader } from "encore.dev/storage/objects";
const uploaderRef = uploads.ref<Uploader>();
const downloaderRef = uploads.ref<Downloader>();
Caching (Redis)
Cache Clusters
import { CacheCluster } from "encore.dev/storage/cache";
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".
Keyspace Types
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";
const tokens = new StringKeyspace<{ tokenId: string }>(cluster, {
keyPattern: "token/:tokenId",
defaultExpiry: expireIn(3600 * 1000),
});
await tokens.set({ tokenId: "abc" }, "value");
const val = await tokens.get({ tokenId: "abc" });
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",
});
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");
Key Patterns with Multiple Fields
interface ResourceKey {
userId: string;
resourcePath: string;
}
const resourceRequests = new IntKeyspace<ResourceKey>(cluster, {
keyPattern: "requests/:userId/:resourcePath",
defaultExpiry: expireIn(10 * 1000),
});
Expiry Options
import {
expireIn,
expireInSeconds,
expireInMinutes,
expireInHours,
expireDailyAt,
neverExpire,
keepTTL,
} from "encore.dev/storage/cache";
Write Options
await keyspace.set(key, value, { expiry: expireInMinutes(30) });
await keyspace.set(key, value, { expiry: keepTTL });
await keyspace.setIfNotExists(key, value);
await keyspace.replace(key, value);
Error Handling
import { CacheMiss, CacheKeyExists } from "encore.dev/storage/cache";
const value = await keyspace.get(key);
Secrets
import { secret } from "encore.dev/config";
const stripeKey = secret("StripeSecretKey");
const key = stripeKey();
Set secrets via CLI:
encore secret set --type prod StripeSecretKey
Guidelines
- Infrastructure declarations MUST be at package level
- Use descriptive names for resources
- Keep migrations sequential and numbered
- Subscription handlers must be idempotent (at-least-once delivery)
- Secrets are accessed by calling the secret as a function
- Cron endpoints should be
expose: false (internal only)