ts_llm_instructions.txt
Rama main · réplica de lectura del repositorio original.
## Node.js style guide
MUST write valid TypeScript w/ Node.js v22+ features + best practices:
- ES6+ syntax always.
- Built-in `fetch` for HTTP, not `node-fetch`.
- Node.js `import`, never `require`.
## TypeScript style guide
- interface/type for complex objects
- Built-in utility types (Record, Partial, Pick) over any
## Generated folders — do not edit
`encore.gen/` + `.encore/` = CLI-regenerated; never edit.
`encore.app` at repo root = app manifest — JSON **text** file, not binary despite `.app` ext. Read like any source file.
## Encore local MCP server
Encore's local MCP server is registered as **`encore-local`** in the editor's MCP config when set up via `encore app create` / `encore llm-rules init` (Claude Code: `.mcp.json`, Cursor: `.cursor/mcp.json`, VS Code: `.vscode/mcp.json`, Zed: `.zed/settings.json`). Not configured? Add a stdio server running `encore mcp run --app=<app-id>`. Prefer it for live app introspection; use its docs tools when unsure of Encore API surface.
MCP client gets full tool catalog (names, descriptions, input schemas) via `tools/list` on session init — no list here.
### Important tools (runtime-only — no filesystem equivalent)
These do what file search + reading source can't. For static structure, see "MCP only for runtime data" below.
- **`call_endpoint`** — args: `service`, `endpoint`, `method`, `path`, `payload` (JSON string w/ body/query/headers/path-params), optional `auth_token` / `auth_payload` / `correlation_id`. **auto-starts app** if not running. Use instead of `curl` to test endpoints.
- **`query_database`** — args: `queries` (array of `{database, query}`). Runs SQL against named DBs, multi queries per call. Beats `encore db conn-uri` + `psql` round-trips.
- **`get_traces`** — args: optional `service`, `endpoint`, `topic`, `subscription`, `error` (`"true"`/`"false"`), `start_time` / `end_time` (ISO), `min_duration_ms` / `max_duration_ms`, `parent_trace_id`, `limit`. Recent root request/Pub/Sub/test traces (timing, status, ids).
- **`get_trace_spans`** — args: `trace_ids` (array from `get_traces`). Full per-span details for deep debug.
- **`get_objects`** — args: `buckets` (array of bucket names). Lists objects + metadata in storage buckets.
- **`search_docs`** — args: `query`, optional `hits_per_page`, `page`, `facet_filters`. Algolia-backed Encore docs search.
- **`get_docs`** — args: `paths` (array of doc paths, e.g. `/docs/ts/primitives/databases`). Fetches full doc pages found via `search_docs`.
### When to reach for it
| Situation | Tool |
|---|---|
| "What services / endpoints / databases exist?" | `get_metadata` once |
| "Test my new POST /orders endpoint" | `call_endpoint` (auto-starts app) |
| "Did the Pub/Sub handler run after I published?" | `get_traces` w/ `topic` / `subscription` filter |
| "Why did last request fail?" | `get_traces` then `get_trace_spans` |
| "How does Encore API surface work?" | `search_docs` then `get_docs` |
- **MCP only for runtime data** - For **static structure** in source (services, endpoints, topics, subscriptions, schemas, secrets, middleware, cron jobs), searching + reading source files = faster + more reliable than `get_*` tools.
## Encore Cloud access
Needed for deploying, cloud secrets, `encore logs --env`, `encore db shell --env`, and the Cloud MCP. Local dev (`encore run`, `encore test`, `encore-local` MCP) needs no login.
### CLI login
- Check first: `encore auth whoami`. Logged in → done.
- Logged out + interactive: ask the user to run `encore auth login` (opens a browser; can't be completed by an agent). New account: `encore auth signup`.
- CI / no browser: set `ENCORE_AUTH_KEY` (app-scoped auth key from Cloud Dashboard > app > App Settings > Auth Keys; prefer ephemeral keys); CLI picks it up automatically. Never pass a real key via `--auth-key`: leaks into shell history + process list.
### Cloud MCP (deployed environments)
Remote MCP server at `https://api.encore.dev/mcp` (HTTP transport): prod/staging traces, deploys, builds + logs, envs, infra, secrets list, read-only DB queries. `encore-local` only sees the local app. Not configured by `encore app create` / `encore llm-rules init`; register as `encore-cloud` if the app is on Encore Cloud and the user wants it.
Auth (separate from CLI login):
- OAuth (default, local dev): no credentials in config. First use opens a browser to grant access → tell the user to expect it.
- API key (CI / shared config): user generates one in Cloud Dashboard > app Settings > API Keys, send as `Authorization: Bearer <key>` header. Keep key out of git: use an env var or user-level (not project) config.
Config:
- Claude Code: `claude mcp add --transport http encore-cloud https://api.encore.dev/mcp` (API key: add `--header "Authorization: Bearer $ENCORE_API_KEY"`). Verify: `claude mcp list`.
- Cursor (`.cursor/mcp.json`): `{"mcpServers": {"encore-cloud": {"url": "https://api.encore.dev/mcp"}}}`
- VS Code (`.vscode/mcp.json`): `{"servers": {"encore-cloud": {"type": "http", "url": "https://api.encore.dev/mcp"}}}`
- Zed (`.zed/settings.json`): `{"context_servers": {"encore-cloud": {"url": "https://api.encore.dev/mcp"}}}`
- API key in JSON configs: add `"headers": {"Authorization": "Bearer ${env:ENCORE_API_KEY}"}` (Cursor/VS Code syntax) rather than a literal key.
Docs: https://encore.dev/docs/platform/cloud-mcp
## Encore check (verify app builds + endpoints work)
`encore check` compiles, boots, health-checks app, optionally runs `curl` cmds once healthy. Use instead of manual `encore run + healthz poll + curl` pattern.
```bash
encore check # compile + boot only
encore check 'curl /ping' # GET a relative path
encore check 'curl /orders -X POST -d "{\"customer_id\":\"c1\",\"amount_cents\":1999}"' # POST with JSON body (flags after path)
encore check 'curl /a; curl /b' # chain in one quoted string
encore check 'curl /a' 'curl /b' # or as separate quoted args
encore check < commands.txt # one curl per line, piped from file
```
Rules for embedded curl DSL:
- **Paths MUST be relative** (start w/ `/`). Use `curl /orders/1`, **not** `curl http://localhost:4000/orders/1` — host/port supplied by `encore check`. Absolute URL fails w/ `curl path "http..." must be relative`.
- **Path first, flags after.** `curl /orders -X POST -d '...'` works; `curl -X POST /orders ...` fails — parser reads first token after `curl` as path, rejects flags (`curl path "-X" must be relative`).
- One curl per quoted cmd. Chain w/ `;` inside one quoted string, pass multi quoted args, or pipe file w/ one curl per line.
- **HTTP errors fail cmd.** Status >= 400 → cmd fails, response body still printed. Expecting error status (e.g. 401 w/o auth)? Add `--no-fail`: `curl /admin --no-fail -i`.
## Pub/sub verification
- Delivery = async + at-least-once + not order-preserving.
- For "most-recent first" by publish order, do NOT sort by `created_at = NOW()` nor subscription-side row id — both = subscription-insert order, at-least-once can flip. Sort by column monotonic w/ publish (entity id from event payload, or publish-time seq in event).
- Verify a handler ran: `get_traces` (filter by `topic` / `subscription`), then `get_trace_spans` for handler errors.
## Deploying to Encore Cloud
Deploy = commit + push to the git repo the app deploys from:
- Default: Encore-managed git → push to `encore` remote.
- App linked to GitHub repo (Cloud Dashboard > Settings > GitHub) → push to that repo (usually `origin`); GitHub pushes trigger deploys.
- Check `git remote -v`. Can't tell which one the app deploys from → ask user.
Which env deploys: each env deploys from its own branch (env Settings > Branch Push) or only manually. New apps have one env, `staging` (type `development`), deploying app's main branch. One push can deploy several envs. If a production env deploys from the branch you push, the push deploys to production → check before pushing.
```bash
git add -A && git commit -m "describe change"
git push encore # Encore-managed git
git push origin <branch> # GitHub-linked app
```
- `encore` remote: success prints one line per env, `remote: <branch>: triggered deploy https://app.encore.dev/<app-id>/envs/<env>/deploys/<id>`. No such line = nothing deployed (nothing new committed, or no env deploys from pushed branch).
- GitHub: git prints nothing from Encore. Check Deployments in Cloud Dashboard, or Cloud MCP (above; lists envs, deploys, builds + logs).
- Push returns once deploy starts; no CLI wait. Env API: `https://<env>-<app-id>.encr.app` (unless custom domain). Logs: `encore logs --env=<env>`.
- Deploy runs tests; failing tests block deploy. Run `CI=true encore test` first (no TTY → vitest watch mode never exits).
- Every declared secret needs value in target env, else deploy fails `secret key(s) not defined`. Set per env type: `printf '%s' "$VAL" | encore secret set --type <types> <Name>` (`dev` = development envs incl. `staging`, `prod` = production, `pr` = preview, `local`), or one env: `--env <env-name>`.
- Redeploy w/o code change (e.g. after setting secret): `git commit --allow-empty -m "Redeploy"` + push to deploy branch, or Cloud Dashboard > Deployments > New Deploy > Branch.
- No `encore` remote and no GitHub link = app created logged out / `--platform=false`. Log in (see Encore Cloud access > CLI login), `rm encore.app` (copy non-`id`/`lang` settings first, re-add after), `encore app init <name> --lang ts` (registers app + adds `encore` remote), commit, push. App created in Cloud Dashboard: `encore app clone <app-id>` (GitHub-linked: `git clone` the GitHub repo).
- Failed deploys: https://encore.dev/docs/platform/deploy/troubleshooting
## Encore.ts domain knowledge
### API definition
Encore.ts = type-safe TS API endpoints w/ built-in request validation. APIs = async fns w/ TS interfaces for req/resp. Source parsing enables auto req validation against schemas.
Syntax:
import { api } from "encore.dev/api";
export const endpoint = api(options, async handler);
Options: method (HTTP method), expose (bool public access, default: false), auth (bool requires auth), path (URL path pattern)
Example:
import { api } from "encore.dev/api";
interface PingParams {
name: string;
}
interface PingResponse {
message: string;
}
export const ping = api(
{ method: "POST" },
async (p: PingParams): Promise<PingResponse> => {
return { message: `Hello ${p.name}!` };
}
);
Schema patterns:
- Full: api({ ... }, async (params: Params): Promise<Response> => {})
- Resp only: api({ ... }, async (): Promise<Response> => {})
- Req only: api({ ... }, async (params: Params): Promise<void> => {})
- No data: api({ ... }, async (): Promise<void> => {})
Param types:
- Header<"Header-Name">: maps field to HTTP header
- Query<type>: maps field to URL query param
- Path: maps to URL path params via :param or *wildcard
### API calls
Service-to-service calls = simple fn call syntax. Services imported from ~encore/clients. Compile-time type check + IDE autocomplete.
Example:
import { hello } from "~encore/clients";
export const myOtherAPI = api({}, async (): Promise<void> => {
const resp = await hello.ping({ name: "World" });
console.log(resp.message); // "Hello World!"
});
### Application structure
Core:
- Monorepo design for entire backend
- One Encore app = full app model benefits
- Supports monolith + microservices
- Services cannot nest
Service def:
import { Service } from "encore.dev/service";
export default new Service("my-service");
Patterns:
Single service (best start):
/my-app
├── package.json
├── encore.app
├── encore.service.ts // service root
├── api.ts // endpoints
└── db.ts // database
Multi service:
/my-app
├── encore.app
├── hello/
│ ├── migrations/
│ ├── encore.service.ts
│ ├── hello.ts
│ └── hello.test.ts
└── world/
├── encore.service.ts
└── world.ts
Large scale (systems-based):
/my-trello-clone
├── encore.app
├── trello/ // system
│ ├── board/ // service
│ └── card/ // service
├── premium/ // system
│ ├── payment/ // service
│ └── subscription/ // service
└── usr/ // system
├── org/ // service
└── user/ // service
### Raw endpoints
Raw endpoints = lower-level HTTP req access via Node.js/Express.js style handling. For webhooks + custom HTTP handling.
Example:
import { api } from "encore.dev/api";
export const myRawEndpoint = api.raw(
{ expose: true, path: "/raw", method: "GET" },
async (req, resp) => {
resp.writeHead(200, { "Content-Type": "text/plain" });
resp.end("Hello, raw world!");
}
);
Usage: curl http://localhost:4000/raw → Hello, raw world!
Use: webhooks, custom HTTP resp format, direct req/resp control
### API errors
Error format:
{
"code": "not_found",
"message": "sprocket not found",
"details": null
}
Impl:
import { APIError, ErrCode } from "encore.dev/api";
throw new APIError(ErrCode.NotFound, "sprocket not found");
// shorthand:
throw APIError.notFound("sprocket not found");
Error codes (name → string_value → HTTP status):
- OK → ok → 200 OK
- Canceled → canceled → 499 Client Closed Request
- Unknown → unknown → 500 Internal Server Error
- InvalidArgument → invalid_argument → 400 Bad Request
- DeadlineExceeded → deadline_exceeded → 504 Gateway Timeout
- NotFound → not_found → 404 Not Found
- AlreadyExists → already_exists → 409 Conflict
- PermissionDenied → permission_denied → 403 Forbidden
- ResourceExhausted → resource_exhausted → 429 Too Many Requests
- FailedPrecondition → failed_precondition → 400 Bad Request
- Aborted → aborted → 409 Conflict
- OutOfRange → out_of_range → 400 Bad Request
- Unimplemented → unimplemented → 501 Not Implemented
- Internal → internal → 500 Internal Server Error
- Unavailable → unavailable → 503 Unavailable
- DataLoss → data_loss → 500 Internal Server Error
- Unauthenticated → unauthenticated → 401 Unauthorized
withDetails on APIError = attach structured details returned to external clients.
### SQL databases
Encore treats SQL DBs as logical resources + natively supports PostgreSQL.
DB creation:
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("todo", {
migrations: "./migrations",
});
-- todo/migrations/1_create_table.up.sql --
CREATE TABLE todo_item (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT false
);
Migration naming: starts w/ number + underscore, must increase sequentially, ends w/ .up.sql (e.g., 001_first_migration.up.sql, 002_second_migration.up.sql)
DB ops (use only these methods):
- query: async iterator for multi rows
const allTodos = db.query<{ id: number; title: string; done: boolean }>`SELECT id, title, done FROM todo_item`;
for await (const todo of allTodos) {
// Process each todo
}
**Always parameterize `db.query` / `db.queryRow` w/ explicit row generic.** Without it `row.*` accesses typed `any` + silently drop into resp (e.g. `TIMESTAMPTZ` col lands as `Date` in `string`-typed resp field, no compile error). Row type fields = snake_case SQL cols, not camelCase API resp — map between yourself in loop.
const rows = await db.query<{ email: string; source_url: string; scraped_at: Date }>`
SELECT email, source_url, created_at as scraped_at
FROM scraped_emails
ORDER BY created_at DESC
`;
const emails = [];
for await (const row of rows) {
emails.push(row);
}
return { emails };
- queryRow: single row or null
async function getTodoTitle(id: number): Promise<string | undefined> {
const row = await db.queryRow<{ title: string }>`SELECT title FROM todo_item WHERE id = ${id}`;
return row?.title;
}
- exec: inserts + queries not returning rows
await db.exec`
INSERT INTO todo_item (title, done)
VALUES (${title}, false)
`;
CLI: db shell (opens psql), db conn-uri (outputs conn string), db proxy (local conn proxy)
Advanced:
- Sharing DBs: export SQLDatabase from shared module or use SQLDatabase.named("name") to ref existing DB
- Extensions: pgvector, PostGIS (uses encoredotdev/postgres Docker image)
- ORM: Prisma, Drizzle (must support std SQL driver + gen std SQL files)
### Cron jobs
Encore.ts = declarative Cron Jobs for periodic + recurring tasks.
Example:
import { CronJob } from "encore.dev/cron";
import { api } from "encore.dev/api";
const _ = new CronJob("welcome-email", {
title: "Send welcome emails",
every: "2h",
endpoint: sendWelcomeEmail,
})
export const sendWelcomeEmail = api({}, async () => {
// Send welcome emails...
});
Schedule:
- every: periodic, starts midnight UTC. Interval must divide 24h evenly. Valid: 10m, 6h. Invalid: 7h
- schedule: cron exprs for complex (e.g., "0 4 15 * *" = 4am UTC on 15th)
### Pub/Sub
Async event broadcasting between services. Decouples services for reliability, improves responsiveness, cloud-agnostic.
Topics (must be package-level vars, cannot create in fns, accessible from any service):
import { Topic } from "encore.dev/pubsub"
export interface SignupEvent {
userID: string;
}
export const signups = new Topic<SignupEvent>("signups", {
deliveryGuarantee: "at-least-once",
});
Publish:
const messageID = await signups.publish({userID: id});
Subscriptions:
import { Subscription } from "encore.dev/pubsub";
const _ = new Subscription(signups, "send-welcome-email", {
handler: async (event) => {
// Send a welcome email using the event.
},
});
Errors: failed events retried per retry policy. After max retries → dead-letter queue.
Delivery guarantees:
- at-least-once: default, possible dup, handlers must be idempotent
- exactly-once: stronger, minimized dups. Limits: AWS 300 msg/s/topic, GCP 3000+ msg/s/region. No publish-side dedup.
Message attributes (key-value for filter/order):
import { Topic, Attribute } from "encore.dev/pubsub";
export interface SignupEvent {
userID: string;
source: Attribute<string>;
}
Ordered delivery (delivered in order by orderingAttribute):
import { Topic, Attribute } from "encore.dev/pubsub";
export interface CartEvent {
shoppingCartID: Attribute<number>;
event: string;
}
export const cartEvents = new Topic<CartEvent>("cart-events", {
deliveryGuarantee: "at-least-once",
orderingAttribute: "shoppingCartID",
})
Limits: AWS 300 msg/s/topic, GCP 1 MBps/ordering key. No effect locally.
### Object storage
Simple + scalable file/unstructured data store.
Buckets (must be package-level vars, cannot create in fns, accessible from any service):
import { Bucket } from "encore.dev/storage/objects";
export const profilePictures = new Bucket("profile-pictures", {
versioned: false
});
Ops:
- Upload:
const data = Buffer.from(...); // image data
const attributes = await profilePictures.upload("my-image.jpeg", data, {
contentType: "image/jpeg",
});
- Download:
const data = await profilePictures.download("my-image.jpeg");
- List:
for await (const entry of profilePictures.list({})) {
// Process entry
}
- Delete:
await profilePictures.remove("my-image.jpeg");
- Attrs:
const attrs = await profilePictures.attrs("my-image.jpeg");
const exists = await profilePictures.exists("my-image.jpeg");
Public access:
export const publicProfilePictures = new Bucket("public-profile-pictures", {
public: true,
versioned: false
});
const url = publicProfilePictures.publicUrl("my-image.jpeg");
Errors: ObjectNotFound (no such object), PreconditionFailed (upload preconditions failed), ObjectsError (base)
Bucket refs (controlled access):
Permissions: Downloader, Uploader, Lister, Attrser, Remover, ReadWriter
import { Uploader } from "encore.dev/storage/objects";
const ref = profilePictures.ref<Uploader>();
Must call from within service for proper permission tracking.
### Caching
Type-safe cache layer backed by Redis w/ auto infra provisioning.
Cluster def:
import { CacheCluster } from "encore.dev/storage/cache";
const cluster = new CacheCluster("my-cache", {
evictionPolicy: "allkeys-lru",
});
Ref existing cluster from 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"
Keyspaces (type-safe storage, Key type + keyPattern → Redis keys):
StringKeyspace example:
import { CacheCluster, StringKeyspace, expireIn } from "encore.dev/storage/cache";
const cluster = new CacheCluster("my-cache", { evictionPolicy: "allkeys-lru" });
const tokens = new StringKeyspace<{ tokenId: string }>(cluster, {
keyPattern: "token/:tokenId",
defaultExpiry: expireIn(3600 * 1000),
});
await tokens.set({ tokenId: "abc123" }, "value");
const token = await tokens.get({ tokenId: "abc123" }); // undefined on miss
await tokens.delete({ tokenId: "abc123" });
IntKeyspace example:
import { IntKeyspace } from "encore.dev/storage/cache";
const counters = new IntKeyspace<{ id: string }>(cluster, {
keyPattern: "counter/:id",
});
await counters.increment({ id: "visits" }, 1);
await counters.decrement({ id: "visits" }, 1);
StructKeyspace example:
import { StructKeyspace } from "encore.dev/storage/cache";
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: "user123" }, { name: "Alice", email: "alice@example.com" });
Other keyspaces (all from "encore.dev/storage/cache"):
- FloatKeyspace<Key>: float vals. Has increment().
- StringListKeyspace<Key>: string lists. pushRight, pushLeft, popRight, popLeft, getRange, items.
- NumberListKeyspace<Key>: number lists. pushRight, pushLeft, popRight, popLeft, items.
- StringSetKeyspace<Key>: string sets. add, remove, contains, items.
- NumberSetKeyspace<Key>: number sets. add, remove, contains, items.
Expiry helpers (from "encore.dev/storage/cache"): expireIn(ms), expireInSeconds(s), expireInMinutes(m), expireInHours(h), expireDailyAt(hour, min, sec), neverExpire, keepTTL
Write opts:
await keyspace.set(key, value, { expiry: expireInMinutes(30) }); // override default
await keyspace.setIfNotExists(key, value); // only if not exists
await keyspace.replace(key, value); // only if exists, throws CacheMiss if absent
Error types: CacheMiss, CacheKeyExists (from "encore.dev/storage/cache"). get() → undefined on miss.
Local dev = in-memory Redis impl w/ ~100 key limit.
### Secrets management
Built-in secrets mgr for API keys, passwords, private keys.
Def:
import { secret } from "encore.dev/config";
const githubToken = secret("GitHubAPIToken");
Usage:
async function callGitHub() {
const resp = await fetch("https://api.github.com/user", {
headers: {
Authorization: `token ${githubToken()}`,
},
});
}
Secret keys globally unique across app.
Storage methods:
- Cloud dashboard: https://app.encore.dev → Settings → Secrets
- CLI: encore secret set --type <types> <secret-name> (types: production/prod, development/dev, preview/pr, local)
- Local override: .secrets.local.cue file (e.g., GitHubAPIToken: "my-local-override-token")
Env settings: one secret value per env type. Env-specific overrides env-type values.
### Streaming APIs
API endpoints for data streaming via WebSocket.
Stream types: StreamIn (client→server), StreamOut (server→client), StreamInOut (bidirectional)
StreamIn:
import { api } from "encore.dev/api";
interface Message {
data: string;
done: boolean;
}
export const uploadStream = api.streamIn<Message>(
{ path: "/upload", expose: true },
async (stream) => {
for await (const data of stream) {
// Process incoming data
if (data.done) break;
}
}
);
StreamOut:
export const dataStream = api.streamOut<Message>(
{ path: "/stream", expose: true },
async (stream) => {
// Send messages to client
await stream.send({ data: "message" });
await stream.close();
}
);
StreamInOut:
export const chatStream = api.streamInOut<InMessage, OutMessage>(
{ path: "/chat", expose: true },
async (stream) => {
for await (const msg of stream) {
await stream.send(/* response */);
}
}
);
Handshake supports: path params, query params, headers, auth data
Client usage:
const stream = client.serviceName.endpointName();
await stream.send({ /* message */ });
for await (const msg of stream) {
// Handle incoming messages
}
Service-to-service:
import { service } from "~encore/clients";
const stream = await service.streamEndpoint();
### Validation
Built-in req validation via TS types — runtime + compile-time.
Example:
import { Header, Query, api } from "encore.dev/api";
import { Min, Max, MinLen, MaxLen, IsEmail, IsURL, StartsWith, EndsWith, MatchesRegexp } from "encore.dev/validate";
interface Request {
limit?: Query<number>; // Optional query parameter
myHeader: Header<"X-My-Header">; // Required header
type: "sprocket" | "widget"; // Required enum in body
}
export const myEndpoint = api<Request, Response>(
{ expose: true, method: "POST", path: "/api" },
async ({ limit, myHeader, type }) => {
// Implementation
}
);
Basic types: string, number, boolean, arrays (string[], number[], { name: string }[], (string | number)[]), enums ("BLOG_POST" | "COMMENT")
Modifiers:
- Optional: fieldName?: type;
- Nullable: fieldName: type | null;
Rules:
- Min/Max: count: number & (Min<3> & Max<1000>);
- MinLen/MaxLen: username: string & (MinLen<5> & MaxLen<20>);
- Format: contact: string & (IsURL | IsEmail);
Source types:
- Body: default for methods w/ req bodies, parsed from JSON body
- Query: URL query params, use Query type or default for GET/HEAD/DELETE
- Headers: HTTP headers, use Header<"Name-Of-Header"> type
- Params: URL path params (e.g., path: "/user/:id", param: { id: string })
Error resp (400 Bad Request):
{
"code": "invalid_argument",
"message": "unable to decode request body",
"internal_message": "Error details"
}
### Static assets
Built-in serving for static assets (images, HTML, CSS, JS). For static sites or pre-compiled SPAs.
Basic:
import { api } from "encore.dev/api";
export const assets = api.static(
{ expose: true, path: "/frontend/*path", dir: "./assets" },
);
Serves files from ./assets under /frontend prefix. Auto-serves index.html at dir roots.
Root serving (uses !path to avoid conflicts):
export const assets = api.static(
{ expose: true, path: "/!path", dir: "./assets" },
);
Custom 404:
export const assets = api.static(
{
expose: true,
path: "/!path",
dir: "./assets",
notFound: "./not_found.html"
},
);
### GraphQL
Encore.ts has GraphQL via raw endpoints w/ auto tracing.
Apollo example:
import { ApolloServer, HeaderMap } from "@apollo/server";
import { api } from "encore.dev/api";
import { json } from "node:stream/consumers";
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
export const graphqlAPI = api.raw(
{ expose: true, path: "/graphql", method: "*" },
async (req, res) => {
server.assertStarted("/graphql");
const headers = new HeaderMap();
for (const [key, value] of Object.entries(req.headers)) {
if (value !== undefined) {
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
}
}
const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({
httpGraphQLRequest: {
headers,
method: req.method!.toUpperCase(),
body: await json(req),
search: new URLSearchParams(req.url ?? "").toString(),
},
context: async () => ({ req, res }),
});
// Set response headers and status
for (const [key, value] of httpGraphQLResponse.headers) {
res.setHeader(key, value);
}
res.statusCode = httpGraphQLResponse.status || 200;
// Write response
if (httpGraphQLResponse.body.kind === "complete") {
res.end(httpGraphQLResponse.body.string);
return;
}
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
res.write(chunk);
}
res.end();
}
);
REST integration example:
Schema:
type Query {
books: [Book]
}
type Book {
title: String!
author: String!
}
Resolver:
import { book } from "~encore/clients";
import { QueryResolvers } from "../__generated__/resolvers-types";
const queries: QueryResolvers = {
books: async () => {
const { books } = await book.list();
return books;
},
};
REST endpoint:
import { api } from "encore.dev/api";
import { Book } from "../__generated__/resolvers-types";
export const list = api(
{ expose: true, method: "GET", path: "/books" },
async (): Promise<{ books: Book[] }> => {
return { books: db };
}
);
### Authentication
Auth system for identifying API callers. Enable: set auth: true in endpoint options.
Auth handler:
import { Header, Gateway } from "encore.dev/api";
import { authHandler } from "encore.dev/auth";
interface AuthParams {
authorization: Header<"Authorization">;
}
interface AuthData {
userID: string;
}
export const auth = authHandler<AuthParams, AuthData>(
async (params) => {
// Authenticate user based on params
return {userID: "my-user-id"};
}
)
export const gateway = new Gateway({
authHandler: auth,
})
Reject: throw APIError.unauthenticated("bad credentials");
Auth process:
1. Determine auth: triggers on any req w/ auth params. Returns AuthData (success), throws Unauthenticated (treated as no auth), or throws other error (req aborted).
2. Endpoint call: if endpoint needs auth + req unauth → reject. If authed, auth data passed regardless of requirements.
Auth data access: import getAuthData from ~encore/auth for type-safe resolution. Auto propagates in internal API calls. Calls to auth-required endpoints fail if orig req lacks auth.
### Metadata
Access env + app info via metadata API from encore.dev.
appMeta() returns: appId (app name), apiBaseUrl (public API URL), environment (current env), build (VCS revision), deploy (deploy ID + timestamp)
currentRequest() returns:
- API call:
interface APICallMeta {
type: "api-call";
api: APIDesc;
method: Method;
path: string;
pathAndQuery: string;
pathParams: Record<string, any>;
headers: Record<string, string | string[]>;
parsedPayload?: Record<string, any>;
}
- PubSub:
interface PubSubMessageMeta {
type: "pubsub-message";
service: string;
topic: string;
subscription: string;
messageId: string;
deliveryAttempt: number;
parsedPayload?: Record<string, any>;
}
Returns undefined if called during service init.
Use cases:
import { appMeta } from "encore.dev";
// Cloud-specific behavior
async function audit(userID: string, event: Record<string, any>) {
const cloud = appMeta().environment.cloud;
switch (cloud) {
case "aws": return writeIntoRedshift(userID, event);
case "gcp": return writeIntoBigQuery(userID, event);
case "local": return writeIntoFile(userID, event);
default: throw new Error(`unknown cloud: ${cloud}`);
}
}
// Environment-specific behavior
switch (appMeta().environment.type) {
case "test":
case "development":
await markEmailVerified(userID);
break;
default:
await sendVerificationEmail(userID);
break;
}
### Middleware
Reusable code running before/after API reqs across endpoints.
Basic:
import { middleware } from "encore.dev/api";
import { Service } from "encore.dev/service";
export default new Service("myService", {
middlewares: [
middleware({ target: { auth: true } }, async (req, next) => {
// Pre-handler logic
const resp = await next(req);
// Post-handler logic
return resp
})
]
});
Req access types:
- Typed API: req.requestMeta
- Streaming: req.requestMeta, req.stream
- Raw: req.rawRequest, req.rawResponse
Resp handling (HandlerResponse):
resp.header.set(key, value)
resp.header.add(key, value)
Ordering (executes in definition order):
export default new Service("myService", {
middlewares: [
first,
second,
third
],
});
Targeting: use target option instead of runtime filter for perf. Defaults to all endpoints if target unset.
### ORM integration
Built-in ORM + migration framework support via named DBs + SQL migration files.
Requires: ORM must support std SQL driver. Migration framework must gen std SQL files.
DB conn:
import { SQLDatabase } from "encore.dev/storage/sqldb";
const SiteDB = new SQLDatabase("siteDB", {
migrations: "./migrations",
});
const connStr = SiteDB.connectionString;
### Drizzle integration
DB setup (database.ts):
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { drizzle } from "drizzle-orm/node-postgres";
import { users } from "./schema";
const db = new SQLDatabase("test", {
migrations: {
path: "migrations",
source: "drizzle",
},
});
const orm = drizzle(db.connectionString);
await orm.select().from(users);
Drizzle config (drizzle.config.ts):
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: 'migrations',
schema: 'schema.ts',
dialect: 'postgresql',
});
Schema (schema.ts):
import * as p from "drizzle-orm/pg-core";
export const users = p.pgTable("users", {
id: p.serial().primaryKey(),
name: p.text(),
email: p.text().unique(),
});
Gen migrations: drizzle-kit generate (run in dir w/ drizzle.config.ts)
Migrations auto-applied at Encore runtime.
### CORS
CORS controls which origins can access API. Scope: browser reqs to resources on different origins (scheme, domain, port).
Config in encore.app under global_cors:
- debug: bool, CORS debug logging
- allow_headers: string[], additional accepted headers ("*" = all)
- expose_headers: string[], additional exposed headers ("*" = all)
- allow_origins_without_credentials: string[], allowed origins for non-cred reqs (default: ["*"])
- allow_origins_with_credentials: string[], allowed origins for credentialed (wildcards: https://*.example.com, https://*-myapp.example.com)
Defaults: allow unauth reqs from all origins, disallow auth reqs from other origins, all origins allowed in local dev.
Header handling: Encore auto-configures headers via static analysis. Additional headers configurable via allow_headers + expose_headers for custom headers in raw endpoints.
### Logging
Built-in structured logging — free-form msgs + type-safe key-value pairs.
import log from "encore.dev/log";
Levels: error, warn, info, debug, trace
Basic:
log.info("log message", {is_subscriber: true})
log.error(err, "something went terribly wrong!")
With context (group logs w/ shared key-value pairs):
const logger = log.with({is_subscriber: true})
logger.info("user logged in", {login_method: "oauth"}) // includes is_subscriber=true
### Testing
Encore.ts uses std TS testing tools. Recommended: Vitest.
Setup:
npm install -D vitest
Add to package.json:
{
"scripts": {
"test": "vitest"
}
}
Run tests:
- encore test: recommended — auto sets up test DBs, isolated infra per test, handles service deps
- npm test: direct, no infra setup
Test API endpoint:
import { describe, it, expect } from "vitest";
import { hello } from "./api";
describe("hello endpoint", () => {
it("returns a greeting", async () => {
const response = await hello();
expect(response.message).toBe("Hello, World!");
});
});
Test w/ req params:
import { describe, it, expect } from "vitest";
import { getUser } from "./api";
describe("getUser endpoint", () => {
it("returns the user by ID", async () => {
const user = await getUser({ id: "123" });
expect(user.id).toBe("123");
expect(user.name).toBeDefined();
});
});
Test DB ops (Encore gives isolated test DBs):
import { describe, it, expect, beforeEach } from "vitest";
import { createUser, getUser, db } from "./user";
describe("user operations", () => {
beforeEach(async () => {
await db.exec`DELETE FROM users`;
});
it("creates and retrieves a user", async () => {
const created = await createUser({ email: "test@example.com", name: "Test" });
const retrieved = await getUser({ id: created.id });
expect(retrieved.email).toBe("test@example.com");
});
});
Test error cases:
import { describe, it, expect } from "vitest";
import { getUser } from "./api";
import { APIError } from "encore.dev/api";
describe("error handling", () => {
it("throws NotFound for missing user", async () => {
await expect(getUser({ id: "nonexistent" }))
.rejects
.toThrow("user not found");
});
it("throws with correct error code", async () => {
try {
await getUser({ id: "nonexistent" });
} catch (error) {
expect(error).toBeInstanceOf(APIError);
expect((error as APIError).code).toBe("not_found");
}
});
});
Test Pub/Sub:
import { describe, it, expect } from "vitest";
import { orderCreated } from "./events";
describe("pub/sub", () => {
it("publishes order created event", async () => {
const messageId = await orderCreated.publish({
orderId: "order-123",
userId: "user-456",
total: 9999,
});
expect(messageId).toBeDefined();
});
});
Test Cron Jobs (test underlying fn, not cron schedule):
import { describe, it, expect } from "vitest";
import { cleanupExpiredSessions } from "./cleanup";
describe("cleanup job", () => {
it("removes expired sessions", async () => {
await createExpiredSession();
await cleanupExpiredSessions();
const remaining = await countSessions();
expect(remaining).toBe(0);
});
});
Vitest config (vitest.config.ts):
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
include: ["**/*.test.ts"],
coverage: {
reporter: ["text", "json", "html"],
},
},
});
Guidelines:
- Use `encore test` to run w/ infra setup
- `encore test` runs against separate test DBs (not your local dev data); data persists across tests in a run, so clean up in `beforeEach`
- Test API endpoints by calling directly as fns
- Service-to-service calls work normally in tests
- Mock external deps (third-party APIs, email services)
- Don't mock Encore infra (DBs, Pub/Sub) — use real thing
### Encore Toolbar (frontend dev panel)
Dev-only browser panel: intercepts frontend HTTP calls, captures Encore trace IDs, jumps to traces in the dashboard. Install: add `<script src="https://encore.dev/encore-toolbar.js"></script>` early in `<head>` (no `async`/`defer`). Reach for it when a frontend talks to an Encore backend.
### Example apps
- Hello World: https://github.com/encoredev/examples/tree/main/ts/hello-world
- URL Shortener: https://github.com/encoredev/examples/tree/main/ts/url-shortener
- Uptime Monitor: https://github.com/encoredev/examples/tree/main/ts/uptime
### Package management
Default: single root-level package.json (monorepo) for Encore.ts projects including frontend deps.
Alt: separate package.json in sub-packages, but Encore.ts app must use one package w/ single package.json, other separate packages must be pre-transpiled to JS.
## Encore CLI reference
Execution:
- encore help [command]: lists cmds (or shows usage for specific cmd). Use when unsure of subcommand/flag.
- encore run [--debug] [--watch=true] [-p port] [flags]: runs app. Before starting yourself, check if already running by polling `http://localhost:<port>/__encore/healthz` (default `4000`) — `200` = healthy, hit endpoints directly w/o re-running.
- encore check ['curl ...'; 'curl ...'] | encore check < commands.txt: compile + boot app, verify services healthy, optionally run curls. See section above.
- encore test [flags]: provisions isolated test infra (DBs, Pub/Sub, etc.) then runs `test` script from `package.json` (Vitest recommended). Test DBs skip fsync + use in-memory FS for speed. Use instead of `npm test` / `vitest` direct when code touches Encore infra. No TTY: `CI=true encore test` (vitest watch mode never exits).
- encore exec path/to/script [args...]: run executable script against local app.
App mgmt:
- encore app clone [app-id] [directory]: clone Encore app
- encore app create [name] [--example=name] [-l ts]: create new Encore app
- encore app init [name]: create new app from existing repo
- encore app link [app-id] [-f]: link to existing Encore Cloud app (adds `encore` remote only if repo has no remotes)
Auth:
- encore auth login: log in (no browser: set `ENCORE_AUTH_KEY` env var)
- encore auth logout: log out
- encore auth signup: create account
- encore auth whoami: show current user
Daemon:
- encore daemon: restart daemon for unexpected behavior
- encore daemon env: output env info
DB:
- encore db shell database-name [--env=name]: connect via psql (--write, --admin, --superuser flags)
- encore db conn-uri database-name [--env=name]: output conn string
- encore db proxy [--env=name]: local DB conn proxy
- encore db reset <db-names...|--all>: reset specified DBs
Code gen:
- encore gen client [app-id] [--env=name] [--lang=lang]: gen API client (langs: go, typescript, javascript, openapi)
Logs:
- encore logs [--env=prod] [--json] [-q]: stream app logs
K8s:
- encore k8s configure --env=ENV_NAME: update kubectl config for env
Secrets:
- encore secret set --type types <secret-name>: set secret (types: production/prod, development/dev, preview/pr, local)
- encore secret set --env <env-name> <secret-name>: set env-specific secret
- encore secret list [keys...]: list secrets
- encore secret delete <id>: delete secret value
Namespaces (infra envs, alias `encore ns`):
- encore namespace list [--output=columns|json]: list infra namespaces
- encore namespace create NAME: create new namespace
- encore namespace switch [--create] NAME: switch active namespace
- encore namespace delete NAME: delete namespace
MCP (programmatic access to the local app — same surface as `.mcp.json`):
- encore mcp run: stdio-based MCP session
- encore mcp start: SSE-based MCP session; prints the SSE URL
Config:
- encore config <key> [<value>] [--app|--global|--all]: get/set CLI config
Telemetry:
- encore telemetry: status
- encore telemetry enable / disable: toggle telemetry reporting
LLM rules:
- encore llm-rules init: generate LLM rule files for this project
Random data (utility):
- encore rand uuid [-1|-4|-6|-7]: gen UUID (default v4)
- encore rand bytes N [-f format]: gen N random bytes
- encore rand words [--sep=SEP] NUM: gen memorable passphrase
Deploy:
- git push encore (or push to linked GitHub repo): deploy (see Deploying to Encore Cloud)
Version:
- encore version: report current
- encore version update: check + apply updates
Build:
- encore build docker IMAGE_TAG [--base string] [--push] [--cgo] [--os] [--arch]: build portable Docker image