---
title: "encoredev/skills"
description: "Agent Skills for development with Encore."
source: https://github.com/encoredev/skills
ref: main
license: Apache-2.0
licenseName: "Apache License 2.0"
canonical: https://skillsdocs.com/encoredev/skills
base: https://github.com/encoredev/skills/blob/main/
chapters: 28
inlined: 28
withheld: 0
words: 7202
updated: 2026-05-15T21:00:31Z
generator: "Skills Docs"
---

> **encoredev/skills** — every Agent Skill in this repository, inlined verbatim.
>
> Canonical HTML: https://skillsdocs.com/encoredev/skills
> Per-chapter Markdown: https://skillsdocs.com/encoredev/skills/<skill>.md
> Machine manifest: https://skillsdocs.com/encoredev/skills/.well-known/agent-skills/index.json
> JSON: https://skillsdocs.com/api/v1/books/encoredev/skills
> Install: `npx skills add encoredev/skills`
> Upstream: https://github.com/encoredev/skills @ `main`
> Licence: Apache-2.0
>
> Content is mirrored from GitHub and © its authors, served unmodified. Takedown: https://github.com/DreambaseAI/skillsdocs/issues/new?labels=takedown&title=Takedown+request

# encoredev/skills

Agent Skills for development with Encore.

- **Chapters:** 28
- **Inlined:** 28 (licence detected)
- **Words:** 7,202
- **Reading time:** 40 min
- **Stars:** 26

## Table of contents

1. [encore-api](https://skillsdocs.com/encoredev/skills/api.md) — Define typed API endpoints in Encore.ts using `api(...)` from `encore.dev/api`. Covers typed request/response interfaces, path/query/header/cookie params, requ…
2. [encore-auth](https://skillsdocs.com/encoredev/skills/auth.md) — Protect Encore.ts endpoints with authentication and authorize callers. Covers `authHandler`, `Gateway`, `getAuthData`, and `auth: true`.
3. [encore-bucket](https://skillsdocs.com/encoredev/skills/bucket.md) — Store unstructured files in Encore.ts using `Bucket` from `encore.dev/storage/objects` — uploads, images, documents, blobs.
4. [encore-cache](https://skillsdocs.com/encoredev/skills/cache.md) — Cache data in Redis from Encore.ts using `CacheCluster` and typed keyspaces from `encore.dev/storage/cache`. Type-safe key/value access with TTLs, atomic incre…
5. [encore-code-review](https://skillsdocs.com/encoredev/skills/code-review.md) — Review existing Encore.ts code for best practices and common anti-patterns.
6. [encore-cron](https://skillsdocs.com/encoredev/skills/cron.md) — Schedule periodic / recurring work in Encore.ts using `CronJob` from `encore.dev/cron`. Covers `every: "1h"` interval syntax and `schedule: "0 9 * * 1"` cron e…
7. [encore-database](https://skillsdocs.com/encoredev/skills/database.md) — Work with PostgreSQL in Encore.ts using `SQLDatabase` from `encore.dev/storage/sqldb` — schema migrations and SQL queries.
8. [encore-frontend](https://skillsdocs.com/encoredev/skills/frontend.md) — Connect a frontend application (React, Next.js, Vue, Svelte, etc.) to an Encore.ts backend.
9. [encore-getting-started](https://skillsdocs.com/encoredev/skills/getting-started.md) — Bootstrap a brand-new Encore.ts project from zero. Only for first-time CLI install and `encore app create` — not for architecture or feature questions.
10. [encore-go-api](https://skillsdocs.com/encoredev/skills/go-api.md) — Define typed API endpoints in Encore Go using `//encore:api` annotations. Covers typed request/response structs, path/query/header/cookie params, and error ret…
11. [encore-go-auth](https://skillsdocs.com/encoredev/skills/go-auth.md) — Protect Encore Go endpoints with authentication and authorize callers. Covers `auth.AuthHandler`, `auth.UserID`, the `Authorization` header, and `//encore:api…
12. [encore-go-bucket](https://skillsdocs.com/encoredev/skills/go-bucket.md) — Store unstructured files in Encore Go using `objects.NewBucket` from `encore.dev/storage/objects` — uploads, images, documents, blobs.
13. [encore-go-cache](https://skillsdocs.com/encoredev/skills/go-cache.md) — Cache data in Redis from Encore Go using `cache.NewCluster` and typed keyspaces from `encore.dev/storage/cache`. Type-safe key/value access with TTLs, atomic i…
14. [encore-go-code-review](https://skillsdocs.com/encoredev/skills/go-code-review.md) — Review existing Encore Go code for best practices and common anti-patterns.
15. [encore-go-cron](https://skillsdocs.com/encoredev/skills/go-cron.md) — Schedule periodic / recurring work in Encore Go using `cron.NewJob` from `encore.dev/cron`. Covers `Every: "1h"` interval syntax and `Schedule: "0 9 * * 1"` cr…
16. [encore-go-database](https://skillsdocs.com/encoredev/skills/go-database.md) — Work with PostgreSQL in Encore Go using `sqldb.NewDatabase` from `encore.dev/storage/sqldb` — schema migrations and SQL queries.
17. [encore-go-getting-started](https://skillsdocs.com/encoredev/skills/go-getting-started.md) — Bootstrap a brand-new Encore Go project from zero. Only for first-time CLI install and `encore app create` — not for architecture or feature questions.
18. [encore-go-pubsub](https://skillsdocs.com/encoredev/skills/go-pubsub.md) — Asynchronous messaging in Encore Go via `pubsub.NewTopic` and `pubsub.NewSubscription` from `encore.dev/pubsub` — broadcast events, decouple producers from con…
19. [encore-go-secret](https://skillsdocs.com/encoredev/skills/go-secret.md) — Manage API keys, credentials, and other secrets in Encore Go using a package-level `secrets` struct.
20. [encore-go-service](https://skillsdocs.com/encoredev/skills/go-service.md) — Plan how to split an Encore Go application into services and lay out its directory structure. Architecture and decomposition, not first-time CLI install (that'…
21. [encore-go-testing](https://skillsdocs.com/encoredev/skills/go-testing.md) — Write or run automated tests for Encore Go code with `encore test` and the standard library `testing` package. Covers isolated per-test databases, calling hand…
22. [encore-go-webhook](https://skillsdocs.com/encoredev/skills/go-webhook.md) — Receive inbound webhooks from external services (Stripe, GitHub, Slack, Twilio, etc.) in Encore Go using `//encore:api raw`. The right skill any time the user…
23. [encore-migrate](https://skillsdocs.com/encoredev/skills/migrate.md) — Migrate an existing backend application to Encore. Supports any source framework, targets Encore.ts or Encore Go. Drives a structured DISCOVER → PLAN → MIGRATE…
24. [encore-pubsub](https://skillsdocs.com/encoredev/skills/pubsub.md) — Asynchronous messaging in Encore.ts via `Topic` and `Subscription` from `encore.dev/pubsub` — broadcast events, decouple producers from consumers, and run back…
25. [encore-secret](https://skillsdocs.com/encoredev/skills/secret.md) — Manage API keys, credentials, and other secrets in Encore.ts using `secret(...)` from `encore.dev/config`.
26. [encore-service](https://skillsdocs.com/encoredev/skills/service.md) — Plan how to split an Encore.ts application into services and lay out its directory structure. Architecture and decomposition, not first-time CLI install (that'…
27. [encore-testing](https://skillsdocs.com/encoredev/skills/testing.md) — Write or run automated tests for Encore.ts code with `encore test` and vitest/jest. Covers isolated per-test databases, calling handlers directly, and `describ…
28. [encore-webhook](https://skillsdocs.com/encoredev/skills/webhook.md) — Receive inbound webhooks from external services (Stripe, GitHub, Slack, Twilio, etc.) using `api.raw(...)` from `encore.dev/api`. The right skill any time the…


## Front matter

_The repository README, verbatim except that relative links are resolved against https://github.com/encoredev/skills/blob/main/._

# Encore Skills

Agent skills for building backend applications with [Encore](https://encore.dev), the backend framework for Go and TypeScript.

## What These Skills Do

Encore is a backend framework with built-in infrastructure. You declare what you need (databases, Pub/Sub, cron jobs, etc.) in code, and Encore understands how to run it.

These skills help AI agents use Encore's declarative patterns correctly:

- **Declarative infrastructure** - define resources in code
- **Type-safe APIs** - request/response validation built-in
- **Service-to-service calls** - automatic type safety across service boundaries
- **Built-in observability** - tracing, metrics, and logging out of the box

### How Infrastructure Works

- **Local development** (`encore run`) - Encore provisions Docker containers automatically (Postgres, Redis, etc.)
- **Production deployment** - Either use [Encore Cloud](https://encore.dev/cloud) to provision in your AWS/GCP account, or self-host using the generated infrastructure configuration

## Installation

```bash
npx add-skill encoredev/skills
```

Works with Cursor, Claude Code, Codex, OpenCode, and 10+ other agents.

```bash
# List available skills
npx add-skill encoredev/skills --list

# Install specific skills
npx add-skill encoredev/skills --skill encore-getting-started --skill encore-api

# Install to specific agents
npx add-skill encoredev/skills -a cursor -a claude-code

# Global installation
npx add-skill encoredev/skills -g
```

### Claude Code Marketplace

If you prefer to use Claude Code directly:

```bash
claude plugin marketplace add encoredev/skills
claude plugin install encore-skills@encore-skills
```

### Manual Installation

Copy the `SKILL.md` files from `encore/` to your agent's skills directory.

## Available Skills

### TypeScript

| Skill | Description |
|-------|-------------|
| `encore-getting-started` | Bootstrap a brand-new Encore.ts project |
| `encore-api` | Define typed API endpoints (`api(...)`) |
| `encore-webhook` | Receive inbound webhooks via `api.raw(...)` |
| `encore-auth` | Protect endpoints with authentication |
| `encore-database` | Postgres queries, migrations, ORM integration |
| `encore-pubsub` | Pub/Sub topics and subscriptions |
| `encore-cron` | Scheduled / periodic jobs |
| `encore-bucket` | Object storage (file uploads, images, blobs) |
| `encore-cache` | Redis-backed caching with typed keyspaces |
| `encore-secret` | API keys, credentials, signing secrets |
| `encore-service` | Plan service boundaries and layout |
| `encore-testing` | Test APIs with Vitest |
| `encore-frontend` | Connect React/Next.js apps |
| `encore-code-review` | Review code for best practices |
| `encore-migrate` | Migrate existing backends to Encore |

### Go

| Skill | Description |
|-------|-------------|
| `encore-go-getting-started` | Bootstrap a brand-new Encore Go project |
| `encore-go-api` | Define typed API endpoints (`//encore:api`) |
| `encore-go-webhook` | Receive inbound webhooks via `//encore:api raw` |
| `encore-go-auth` | Protect endpoints with authentication |
| `encore-go-database` | Postgres queries and migrations |
| `encore-go-pubsub` | Pub/Sub topics and subscriptions |
| `encore-go-cron` | Scheduled / periodic jobs |
| `encore-go-bucket` | Object storage (file uploads, images, blobs) |
| `encore-go-cache` | Redis-backed caching with typed keyspaces |
| `encore-go-secret` | API keys, credentials, signing secrets |
| `encore-go-service` | Plan service boundaries and layout |
| `encore-go-testing` | Test APIs and services |
| `encore-go-code-review` | Review code for best practices |

## References

- [Encore.ts Documentation](https://encore.dev/docs)
- [Encore GitHub](https://github.com/encoredev/encore)

## License

Apache-2.0

---

<!-- chapter:begin slug=api position=1 -->

## 1. encore-api

- **Source:** https://github.com/encoredev/skills/blob/main/encore/api/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/api/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/api.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-api
description: Define typed API endpoints in Encore.ts using `api(...)` from `encore.dev/api`. Covers typed request/response interfaces, path/query/header/cookie params, request validation, and `APIError`. For raw endpoints (`api.raw()`) and inbound webhooks, use `encore-webhook` instead.
when_to_use: >-
  User wants to define an endpoint, route, or REST handler in their own service — anything with a typed JSON request/response shape. Mentions of an endpoint, GET/POST/PUT/PATCH/DELETE, paths like `/orders` or `/users/:id`, request body, query parameters (`Query<>`), path parameters, headers (`Header<>`), cookies (`Cookie<>`), HTTP status codes (`HttpStatus`), request validation (`Min`, `MaxLen`, `IsEmail`, `IsURL`), `APIError.notFound` / 4xx-5xx errors, or `expose: true`. Trigger phrases: "POST endpoint at /orders", "typed endpoint", "GET /users/:id", "request validation", "return 404", "JSON response shape".
---

# Encore API Endpoints

## Instructions

When creating API endpoints with Encore.ts, follow these patterns:

### 1. Import the API module

```typescript
import { api } from "encore.dev/api";
```

### 2. Define typed request/response interfaces

Always define explicit TypeScript interfaces for request and response types:

```typescript
interface CreateUserRequest {
  email: string;
  name: string;
}

interface CreateUserResponse {
  id: string;
  email: string;
  name: string;
}
```

### 3. Create the endpoint

```typescript
export const createUser = api(
  { method: "POST", path: "/users", expose: true },
  async (req: CreateUserRequest): Promise<CreateUserResponse> => {
    // Implementation
  }
);
```

## API Options

| Option | Type | Description |
|--------|------|-------------|
| `method` | string | HTTP method: GET, POST, PUT, PATCH, DELETE |
| `path` | string | URL path, supports `:param` and `*wildcard` |
| `expose` | boolean | If true, accessible from outside (default: false) |
| `auth` | boolean | If true, requires authentication |
| `sensitive` | boolean | If true, redacts request/response payloads from traces |

## Request/Response Patterns

Encore supports four endpoint configurations:

```typescript
// Both request and response
export const createUser = api(
  { method: "POST", path: "/users", expose: true },
  async (req: CreateRequest): Promise<CreateResponse> => { ... }
);

// Response only (no request body)
export const listUsers = api(
  { method: "GET", path: "/users", expose: true },
  async (): Promise<ListResponse> => { ... }
);

// Request only (no response body)
export const deleteUser = api(
  { method: "DELETE", path: "/users/:id", expose: true },
  async (req: DeleteRequest): Promise<void> => { ... }
);

// Neither request nor response
export const ping = api(
  { method: "GET", path: "/ping", expose: true },
  async (): Promise<void> => { ... }
);
```

## Custom HTTP Status Codes

Include an `HttpStatus` field in your response to return custom status codes:

```typescript
import { api, HttpStatus } from "encore.dev/api";

interface CreateResponse {
  id: string;
  status: HttpStatus;
}

export const create = api(
  { method: "POST", path: "/items", expose: true },
  async (req: CreateRequest): Promise<CreateResponse> => {
    const item = await createItem(req);
    return { id: item.id, status: HttpStatus.Created };  // Returns 201
  }
);
```

## Parameter Types

### Path Parameters

```typescript
// Path: "/users/:id"
interface GetUserRequest {
  id: string;  // Automatically mapped from :id
}
```

### Query Parameters

```typescript
import { Query } from "encore.dev/api";

interface ListUsersRequest {
  limit?: Query<number>;
  offset?: Query<number>;
}
```

### Headers

```typescript
import { Header } from "encore.dev/api";

interface WebhookRequest {
  signature: Header<"X-Webhook-Signature">;
  payload: string;
}
```

### Cookies

```typescript
import { Cookie } from "encore.dev/api";

interface SessionRequest {
  session?: Cookie<"session">;
  settings?: Cookie<"user-settings">;
}
```

## Request Validation

Encore validates requests at runtime using TypeScript types. Add constraints for stricter validation:

```typescript
import { api } from "encore.dev/api";
import { Min, Max, MinLen, MaxLen, IsEmail, IsURL } from "encore.dev/validate";

interface CreateUserRequest {
  email: string & IsEmail;                    // Must be valid email
  username: string & MinLen<3> & MaxLen<20>;  // 3-20 characters
  age: number & Min<13> & Max<120>;           // Between 13 and 120
  website?: string & IsURL;                   // Optional, must be URL if provided
}
```

### Combining Validation Rules

Use `&` for AND logic (must pass all rules) and `|` for OR logic (must pass at least one):

```typescript
import { IsEmail, IsURL, MinLen, MaxLen } from "encore.dev/validate";

interface ContactRequest {
  // Must be valid email OR valid URL
  contact: string & (IsEmail | IsURL);
  // Must be 5-100 chars AND be a valid URL
  website: string & MinLen<5> & MaxLen<100> & IsURL;
}
```

### Available Validators

| Validator | Applies To | Example |
|-----------|-----------|---------|
| `Min<N>` | number | `age: number & Min<18>` |
| `Max<N>` | number | `count: number & Max<100>` |
| `MinLen<N>` | string, array | `name: string & MinLen<1>` |
| `MaxLen<N>` | string, array | `tags: string[] & MaxLen<10>` |
| `IsEmail` | string | `email: string & IsEmail` |
| `IsURL` | string | `link: string & IsURL` |
| `StartsWith<S>` | string | `id: string & StartsWith<"usr_">` |
| `EndsWith<S>` | string | `file: string & EndsWith<".json">` |
| `MatchesRegexp<R>` | string | `code: string & MatchesRegexp<"^[A-Z]{3}$">` |

### Validation Error Response

Invalid requests return 400 with details:

```json
{
  "code": "invalid_argument",
  "message": "validation failed",
  "details": { "field": "email", "error": "must be a valid email" }
}
```

## Error Handling

Use `APIError` for proper HTTP error responses:

```typescript
import { APIError, ErrCode } from "encore.dev/api";

// Throw with error code
throw new APIError(ErrCode.NotFound, "user not found");

// Or use shorthand
throw APIError.notFound("user not found");
throw APIError.invalidArgument("email is required");
throw APIError.unauthenticated("invalid token");
```

## Common Error Codes

| Code | HTTP Status | Usage |
|------|-------------|-------|
| `NotFound` | 404 | Resource doesn't exist |
| `InvalidArgument` | 400 | Bad input |
| `Unauthenticated` | 401 | Missing/invalid auth |
| `PermissionDenied` | 403 | Not allowed |
| `AlreadyExists` | 409 | Duplicate resource |

## Static Assets

Serve static files (HTML, CSS, JS, images) with `api.static`:

```typescript
import { api } from "encore.dev/api";

// Serve files from ./assets under /static/*
export const assets = api.static(
  { expose: true, path: "/static/*path", dir: "./assets" }
);

// Serve at root (use !path for fallback routing)
export const frontend = api.static(
  { expose: true, path: "/!path", dir: "./dist" }
);

// Custom 404 page
export const app = api.static(
  { expose: true, path: "/!path", dir: "./public", notFound: "./404.html" }
);
```

### Path Syntax

- `*path` - Standard wildcard: matches all paths under the prefix (e.g., `/static/*path`)
- `!path` - Fallback routing: serves static files at domain root without conflicting with other API endpoints. Use this for SPAs where unmatched routes should serve `index.html`

## Guidelines

- Always use `import` not `require`
- Define explicit interfaces for type safety
- Use `expose: true` only for public endpoints
- Throw `APIError` instead of returning error objects
- For inbound webhooks (Stripe, GitHub, etc.) use `api.raw` — see the `encore-webhook` skill
- Path parameters are automatically extracted from the path pattern
- Use validation constraints (`Min`, `MaxLen`, etc.) for user input

<!-- chapter:end slug=api -->

---

<!-- chapter:begin slug=auth position=2 -->

## 2. encore-auth

- **Source:** https://github.com/encoredev/skills/blob/main/encore/auth/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/auth/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/auth.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-auth
description: >-
  Protect Encore.ts endpoints with authentication and authorize callers. Covers `authHandler`, `Gateway`, `getAuthData`, and `auth: true`.
when_to_use: >-
  User wants to require login on an endpoint, restrict an endpoint to authenticated/signed-in users, validate a bearer token / JWT / API key from an Authorization header, read the current user inside a handler (`getAuthData`), set up an `authHandler<AuthParams, AuthData>` or `Gateway`, return 401/403 from a handler, or set `auth: true` on `api(...)`. Trigger phrases: "protect this endpoint", "only authenticated users", "require login", "Authorization header", "bearer token", "401", "403", "who is calling", "current user".
---

# Encore Authentication

## Instructions

Encore.ts provides a built-in authentication system for identifying API callers and protecting endpoints.

### 1. Create an Auth Handler

```typescript
// auth.ts
import { Header, Gateway } from "encore.dev/api";
import { authHandler } from "encore.dev/auth";

// Define what the auth handler receives
interface AuthParams {
  authorization: Header<"Authorization">;
}

// Define what authenticated requests will have access to
interface AuthData {
  userID: string;
  email: string;
  role: "admin" | "user";
}

export const auth = authHandler<AuthParams, AuthData>(
  async (params) => {
    // Validate the token (example with JWT)
    const token = params.authorization.replace("Bearer ", "");
    
    const payload = await verifyToken(token);
    if (!payload) {
      throw APIError.unauthenticated("invalid token");
    }
    
    return {
      userID: payload.sub,
      email: payload.email,
      role: payload.role,
    };
  }
);

// Register the auth handler with a Gateway
export const gateway = new Gateway({
  authHandler: auth,
});
```

### 2. Protect Endpoints

```typescript
import { api } from "encore.dev/api";

// Protected endpoint - requires authentication
export const getProfile = api(
  { method: "GET", path: "/profile", expose: true, auth: true },
  async (): Promise<Profile> => {
    // Only authenticated users reach here
  }
);

// Public endpoint - no authentication required
export const healthCheck = api(
  { method: "GET", path: "/health", expose: true },
  async () => ({ status: "ok" })
);
```

### 3. Access Auth Data in Endpoints

```typescript
import { api } from "encore.dev/api";
import { getAuthData } from "~encore/auth";

export const getProfile = api(
  { method: "GET", path: "/profile", expose: true, auth: true },
  async (): Promise<Profile> => {
    const auth = getAuthData()!;  // Non-null when auth: true
    
    return {
      userID: auth.userID,
      email: auth.email,
      role: auth.role,
    };
  }
);
```

## Auth Handler Behavior

| Scenario | Handler Returns | Result |
|----------|----------------|--------|
| Valid credentials | `AuthData` object | Request authenticated |
| Invalid credentials | Throws `APIError.unauthenticated()` | Treated as no auth |
| Other error | Throws other error | Request aborted |

## Auth with Endpoints

| Endpoint Config | Request Has Auth | Result |
|-----------------|------------------|--------|
| `auth: true` | Yes | Proceeds with auth data |
| `auth: true` | No | 401 Unauthenticated |
| `auth: false` or omitted | Yes | Proceeds (auth data available) |
| `auth: false` or omitted | No | Proceeds (no auth data) |

## Service-to-Service Auth Propagation

Auth data automatically propagates to internal service calls:

```typescript
import { user } from "~encore/clients";
import { getAuthData } from "~encore/auth";

export const getOrderWithUser = api(
  { method: "GET", path: "/orders/:id", expose: true, auth: true },
  async ({ id }): Promise<OrderWithUser> => {
    const auth = getAuthData()!;

    // Auth is automatically propagated to this call
    const orderUser = await user.getProfile();

    return { order: await getOrder(id), user: orderUser };
  }
);
```

### Overriding Auth Data

You can explicitly override auth data when making service-to-service calls:

```typescript
import { user } from "~encore/clients";

// Override auth data for this specific call
const adminUser = await user.getProfile(
  {},
  { authData: { userID: "admin-123", email: "admin@example.com", role: "admin" } }
);
```

## Common Auth Patterns

### JWT Token Validation

```typescript
import { jwtVerify } from "jose";
import { secret } from "encore.dev/config";

const jwtSecret = secret("JWTSecret");

async function verifyToken(token: string): Promise<JWTPayload | null> {
  try {
    const { payload } = await jwtVerify(
      token,
      new TextEncoder().encode(jwtSecret())
    );
    return payload;
  } catch {
    return null;
  }
}
```

### API Key Authentication

```typescript
export const auth = authHandler<AuthParams, AuthData>(
  async (params) => {
    const apiKey = params.authorization;
    
    const user = await db.queryRow<User>`
      SELECT id, email, role FROM users WHERE api_key = ${apiKey}
    `;
    
    if (!user) {
      throw APIError.unauthenticated("invalid API key");
    }
    
    return {
      userID: user.id,
      email: user.email,
      role: user.role,
    };
  }
);
```

### Cookie-Based Auth

```typescript
interface AuthParams {
  cookie: Header<"Cookie">;
}

export const auth = authHandler<AuthParams, AuthData>(
  async (params) => {
    const sessionId = parseCookie(params.cookie, "session");
    
    if (!sessionId) {
      throw APIError.unauthenticated("no session");
    }
    
    const session = await getSession(sessionId);
    if (!session || session.expiresAt < new Date()) {
      throw APIError.unauthenticated("session expired");
    }
    
    return {
      userID: session.userID,
      email: session.email,
      role: session.role,
    };
  }
);
```

## Testing with Auth

Mock authentication in tests using Vitest:

```typescript
import { describe, it, expect, vi } from "vitest";
import * as auth from "~encore/auth";
import { getProfile } from "./api";

describe("authenticated endpoints", () => {
  it("returns profile for authenticated user", async () => {
    // Mock getAuthData to return test user
    const spy = vi.spyOn(auth, "getAuthData");
    spy.mockImplementation(() => ({
      userID: "test-user-123",
      email: "test@example.com",
      role: "user",
    }));

    const profile = await getProfile();
    expect(profile.email).toBe("test@example.com");

    spy.mockRestore();
  });
});
```

## Guidelines

- Auth handlers must be registered with a Gateway
- Use `getAuthData()` from `~encore/auth` to access auth data
- `getAuthData()` returns `null` in unauthenticated requests
- Auth data propagates automatically in service-to-service calls
- Throw `APIError.unauthenticated()` for invalid credentials
- Keep auth handlers fast - they run on every authenticated request

<!-- chapter:end slug=auth -->

---

<!-- chapter:begin slug=bucket position=3 -->

## 3. encore-bucket

- **Source:** https://github.com/encoredev/skills/blob/main/encore/bucket/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/bucket/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/bucket.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-bucket
description: Store unstructured files in Encore.ts using `Bucket` from `encore.dev/storage/objects` — uploads, images, documents, blobs.
when_to_use: >-
  User wants to upload, download, list, or delete files — profile pictures, avatars, document uploads, image storage, media assets, generated reports, blob data. Covers public vs private buckets, signed upload/download URLs, bucket references with permission types (Uploader, Downloader, Lister, Attrser, Remover), and operations like `upload()`, `download()`, `list()`, `signedUploadUrl()`. Trigger phrases: "object storage", "bucket", "S3", "GCS", "blob", "user uploads", "profile picture", "image upload", "store a file", "file storage".
---

# Encore Object Storage

## Instructions

A `Bucket` is a logical store for files. Encore provisions the underlying object storage (S3 on AWS, GCS on GCP, in-memory locally). Declare buckets at package level.

```typescript
import { Bucket } from "encore.dev/storage/objects";

// Private bucket (default)
export const uploads = new Bucket("user-uploads", {
  versioned: false,  // Set to true to keep multiple versions
});

// Public bucket — files accessible via public URL
export const publicAssets = new Bucket("public-assets", {
  public: true,
  versioned: false,
});
```

## Operations

```typescript
// Upload
const attrs = await uploads.upload("path/to/file.jpg", buffer, {
  contentType: "image/jpeg",
});

// Download
const data = await uploads.download("path/to/file.jpg");

// Existence check
const exists = await uploads.exists("path/to/file.jpg");

// Attributes (size, content type, ETag)
const meta = await uploads.attrs("path/to/file.jpg");

// Delete
await uploads.remove("path/to/file.jpg");

// List
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");
```

## Signed URLs

Generate temporary URLs so clients can upload/download directly without going through your service:

```typescript
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 to other code with a specific permission set:

```typescript
import { Uploader, Downloader } from "encore.dev/storage/objects";

const uploaderRef = uploads.ref<Uploader>();
const downloaderRef = uploads.ref<Downloader>();

// Permission types: Downloader, Uploader, Lister, Attrser, Remover,
// SignedDownloader, SignedUploader, ReadWriter
```

## Errors

- `ObjectNotFound` — object doesn't exist
- `PreconditionFailed` — upload preconditions not met (e.g. `setIfNotExists`)
- `ObjectsError` — base error type

## Guidelines

- Declare buckets at package level.
- Default to private buckets; opt into `public: true` only for assets meant for unauthenticated download.
- Use signed URLs for browser uploads/downloads instead of streaming through your service.
- Use bucket references when passing access to helpers — they encode the permission contract in the type system.

<!-- chapter:end slug=bucket -->

---

<!-- chapter:begin slug=cache position=4 -->

## 4. encore-cache

- **Source:** https://github.com/encoredev/skills/blob/main/encore/cache/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/cache/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/cache.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-cache
description: Cache data in Redis from Encore.ts using `CacheCluster` and typed keyspaces from `encore.dev/storage/cache`. Type-safe key/value access with TTLs, atomic increments, and per-keyspace data shapes.
when_to_use: >-
  User wants to cache values, store ephemeral state, rate-limit by counter, build a leaderboard, speed up a hot read, or store short-lived tokens. Covers `CacheCluster`, `StringKeyspace`, `IntKeyspace`, `FloatKeyspace`, `StructKeyspace`, list and set keyspaces, TTL helpers (`expireIn`, `expireDailyAt`, `neverExpire`, `keepTTL`), atomic `increment`/`decrement`, `setIfNotExists`, `replace`, eviction policies, and `CacheMiss`/`CacheKeyExists` errors. Trigger phrases: "cache this", "Redis", "key-value store", "rate limit", "TTL", "expire after", "in-memory store", "session token store", "leaderboard counter".
---

# Encore Caching (Redis)

## Instructions

Encore's cache is a typed wrapper around Redis. Declare a `CacheCluster` once, then create `Keyspace` objects for each shape of data you need.

### Cluster

```typescript
import { CacheCluster } from "encore.dev/storage/cache";

const cluster = new CacheCluster("my-cache", {
  evictionPolicy: "allkeys-lru",
});
```

Reference a 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"`.

### Keyspace types

Each keyspace has a key shape (used to build the Redis key from `keyPattern`) and a value type.

```typescript
import {
  StringKeyspace,
  IntKeyspace,
  FloatKeyspace,
  StructKeyspace,
  StringListKeyspace,
  NumberListKeyspace,
  StringSetKeyspace,
  NumberSetKeyspace,
  expireIn,
} from "encore.dev/storage/cache";

// Strings
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" }); // undefined on miss

// Integers (atomic counters)
const counters = new IntKeyspace<{ userId: string }>(cluster, {
  keyPattern: "requests/:userId",
  defaultExpiry: expireIn(10 * 1000),
});
const count = await counters.increment({ userId: "user123" }, 1);

// Structs (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 recent = new StringListKeyspace<{ userId: string }>(cluster, {
  keyPattern: "recent/:userId",
});
await recent.pushRight({ userId: "user123" }, "item1", "item2");

// Sets
const tags = new StringSetKeyspace<{ articleId: string }>(cluster, {
  keyPattern: "tags/:articleId",
});
await tags.add({ articleId: "post1" }, "typescript", "encore");
const has = await tags.contains({ articleId: "post1" }, "typescript");
```

### Multi-field key patterns

```typescript
interface Key { userId: string; resourcePath: string; }

const requests = new IntKeyspace<Key>(cluster, {
  keyPattern: "requests/:userId/:resourcePath",
  defaultExpiry: expireIn(10 * 1000),
});
```

### Expiry helpers

```typescript
import {
  expireIn,          // milliseconds
  expireInSeconds,
  expireInMinutes,
  expireInHours,
  expireDailyAt,     // a specific UTC time each day
  neverExpire,
  keepTTL,           // keep existing TTL when updating
} from "encore.dev/storage/cache";
```

### Write options

```typescript
await keyspace.set(key, value, { expiry: expireInMinutes(30) });
await keyspace.set(key, value, { expiry: keepTTL });
await keyspace.setIfNotExists(key, value);  // throws CacheKeyExists if present
await keyspace.replace(key, value);          // throws CacheMiss if absent
```

### Errors

```typescript
import { CacheMiss, CacheKeyExists } from "encore.dev/storage/cache";

const value = await keyspace.get(key);  // undefined on miss (does not throw)
```

## Guidelines

- Declare `CacheCluster` and keyspaces at package level.
- Pick the most specific keyspace type — `IntKeyspace` for counters gives you atomic `increment`/`decrement` for free.
- `get()` returns `undefined` on miss; `replace()` and `setIfNotExists()` throw on conflict.
- Local development uses an in-memory Redis with a ~100-key cap — don't load-test it.
- For durable storage, use `encore-database` (Postgres) or `encore-bucket` (object storage) instead.

<!-- chapter:end slug=cache -->

---

<!-- chapter:begin slug=code-review position=5 -->

## 5. encore-code-review

- **Source:** https://github.com/encoredev/skills/blob/main/encore/code-review/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/code-review/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/code-review.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-code-review
description: Review existing Encore.ts code for best practices and common anti-patterns.
when_to_use: >-
  User is reviewing a pull request, auditing existing code, or checking for Encore-specific anti-patterns before merging — infrastructure declared inside functions, missing service files, wrong import paths, raw `Error` thrown instead of `APIError`, untyped APIs. SKIP for greenfield code being actively written. Trigger phrases: "audit", "review", "before merge", "PR review", "anti-patterns", "code smell", "lint this".
---

# Encore Code Review

## Instructions

When reviewing Encore.ts code, check for these common issues:

## Critical Issues

### 1. Infrastructure Inside Functions

```typescript
// WRONG: Infrastructure declared inside function
async function setup() {
  const db = new SQLDatabase("mydb", { migrations: "./migrations" });
  const topic = new Topic<Event>("events", { deliveryGuarantee: "at-least-once" });
}

// CORRECT: Package level declaration
const db = new SQLDatabase("mydb", { migrations: "./migrations" });
const topic = new Topic<Event>("events", { deliveryGuarantee: "at-least-once" });
```

### 2. Using require() Instead of import

```typescript
// WRONG
const { api } = require("encore.dev/api");

// CORRECT
import { api } from "encore.dev/api";
```

### 3. Wrong Service Import Pattern

```typescript
// WRONG: Direct import from another service
import { getUser } from "../user/api";

// CORRECT: Use ~encore/clients
import { user } from "~encore/clients";
const result = await user.getUser({ id });
```

### 4. Missing Error Handling

```typescript
// WRONG: Returning null for not found
const user = await db.queryRow`SELECT * FROM users WHERE id = ${id}`;
if (!user) return null;

// CORRECT: Throw APIError
import { APIError } from "encore.dev/api";

const user = await db.queryRow`SELECT * FROM users WHERE id = ${id}`;
if (!user) {
  throw APIError.notFound("user not found");
}
```

### 5. SQL Injection Risk

```typescript
// WRONG: String concatenation
await db.query(`SELECT * FROM users WHERE email = '${email}'`);

// CORRECT: Template literal with automatic escaping
await db.queryRow`SELECT * FROM users WHERE email = ${email}`;
```

## Warning Issues

### 6. Missing Type Annotations

```typescript
// WEAK: No explicit types
export const getUser = api(
  { method: "GET", path: "/users/:id", expose: true },
  async ({ id }) => {
    return await findUser(id);
  }
);

// BETTER: Explicit request/response types
interface GetUserRequest { id: string; }
interface User { id: string; email: string; name: string; }

export const getUser = api(
  { method: "GET", path: "/users/:id", expose: true },
  async ({ id }: GetUserRequest): Promise<User> => {
    return await findUser(id);
  }
);
```

### 7. Exposed Internal Endpoints

```typescript
// CHECK: Should this cron endpoint be exposed?
export const cleanupJob = api(
  { expose: true },  // Probably should be false
  async () => { /* ... */ }
);
```

### 8. Non-Idempotent Subscription Handlers

```typescript
// RISKY: Not idempotent (pubsub has at-least-once delivery)
const _ = new Subscription(orderCreated, "process-order", {
  handler: async (event) => {
    await chargeCustomer(event.orderId);  // Could charge twice!
  },
});

// SAFER: Check before processing
const _ = new Subscription(orderCreated, "process-order", {
  handler: async (event) => {
    const order = await getOrder(event.orderId);
    if (order.status !== "pending") return;  // Already processed
    await chargeCustomer(event.orderId);
  },
});
```

### 9. Secrets Called at Module Level

```typescript
// WRONG: Secret accessed at startup
const stripeKey = secret("StripeKey");
const client = new Stripe(stripeKey());  // Called during import

// CORRECT: Access inside functions
const stripeKey = secret("StripeKey");

async function charge() {
  const client = new Stripe(stripeKey());  // Called at runtime
}
```

## Review Checklist

- [ ] All infrastructure at package level
- [ ] Using ES6 imports, not require()
- [ ] Cross-service calls use `~encore/clients`
- [ ] Proper error handling with APIError
- [ ] SQL uses template literals
- [ ] Request/response types defined
- [ ] Internal endpoints have `expose: false`
- [ ] Subscription handlers are idempotent
- [ ] Secrets accessed inside functions, not at import time
- [ ] Migrations follow naming convention (001_name.up.sql)

## Output Format

When reviewing, report issues as:

```
[CRITICAL] [file:line] Description of issue
[WARNING] [file:line] Description of concern  
[GOOD] Notable good practice observed
```

<!-- chapter:end slug=code-review -->

---

<!-- chapter:begin slug=cron position=6 -->

## 6. encore-cron

- **Source:** https://github.com/encoredev/skills/blob/main/encore/cron/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/cron/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/cron.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-cron
description: >-
  Schedule periodic / recurring work in Encore.ts using `CronJob` from `encore.dev/cron`. Covers `every: "1h"` interval syntax and `schedule: "0 9 * * 1"` cron expressions.
when_to_use: >-
  User wants to run a job on a schedule — anything with the words schedule, scheduled, daily, hourly, weekly, periodic, recurring, every N minutes/hours, "at HH:MM UTC", midnight, batch job, aggregation job, nightly, cleanup job, or background work that runs on a timer rather than in response to a request. Trigger phrases: "every day at 02:00 UTC", "daily aggregation", "run hourly", "scheduled task", "cron", "nightly cleanup", "on a schedule".
---

# Encore Cron Jobs

## Instructions

A `CronJob` declaration in Encore.ts ties a schedule to an existing `api(...)` endpoint. The endpoint runs at the chosen cadence. Declare the `CronJob` at package level — not inside a function.

```typescript
import { CronJob } from "encore.dev/cron";
import { api } from "encore.dev/api";

// 1. The endpoint to call (typically internal: expose: false)
export const aggregateDailyOrders = api(
  { expose: false },
  async (): Promise<void> => {
    // Aggregation logic
  }
);

// 2. Package-level cron declaration
const _ = new CronJob("aggregate-daily-orders", {
  title: "Aggregate orders for the previous day",
  schedule: "0 2 * * *",  // 02:00 UTC every day
  endpoint: aggregateDailyOrders,
});
```

## Schedule Formats

| Field | Example | Description |
|---|---|---|
| `every` | `"1h"`, `"30m"`, `"6h"` | Simple interval. **Must divide 24h evenly** — `"7h"` is invalid. |
| `schedule` | `"0 9 * * 1"` | Standard cron expression (5 fields, UTC). |

### Common cron expressions

| Cron | Meaning |
|---|---|
| `"0 * * * *"` | Every hour, on the hour |
| `"0 2 * * *"` | Daily at 02:00 UTC |
| `"0 0 * * 0"` | Weekly on Sunday at midnight UTC |
| `"0 4 15 * *"` | 04:00 UTC on the 15th of each month |

## Important behaviour

- **Cron jobs do not execute when running locally with `encore run`.** Only deployed environments fire crons.
- The cron endpoint should be `expose: false` so it can't be triggered externally — only the cron scheduler should call it.
- All times in `schedule` are UTC. Convert from local time when designing the schedule.
- The endpoint must already exist at module load — declare it before the `CronJob`.

## Guidelines

- Use `every` for "run on a regular interval" (must divide 24h).
- Use `schedule` for specific times of day or days of week.
- Keep endpoint logic idempotent: a cron may fire late or be retried in a redeploy window.
- For event-driven background work (not time-driven), use the `encore-pubsub` skill instead.

<!-- chapter:end slug=cron -->

---

<!-- chapter:begin slug=database position=7 -->

## 7. encore-database

- **Source:** https://github.com/encoredev/skills/blob/main/encore/database/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/database/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/database.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-database
description: Work with PostgreSQL in Encore.ts using `SQLDatabase` from `encore.dev/storage/sqldb` — schema migrations and SQL queries.
when_to_use: >-
  User wants to add a database table, write a migration, run a SQL query, insert/update/delete rows, set up Drizzle or Prisma against an Encore database, or design a relational schema. Covers `new SQLDatabase(...)`, `db.query`, `db.queryRow`, `db.exec`, the `migrations/` directory, `*.up.sql` files, sequential migration numbering, and ORM integration via `db.connectionString`. Trigger phrases: "Postgres table", "user_sessions table", "SQL", "migration", "queryRow", "INSERT", "SELECT", "schema", "Drizzle", "Prisma".
---

# Encore Database Operations

## Instructions

### Database Setup

```typescript
import { SQLDatabase } from "encore.dev/storage/sqldb";

const db = new SQLDatabase("mydb", {
  migrations: "./migrations",
});
```

## Query Methods

Encore provides several query methods:

### `query` - Multiple Rows

Returns an async iterator for multiple rows:

```typescript
interface User {
  id: string;
  email: string;
  name: string;
}

const rows = await db.query<User>`
  SELECT id, email, name FROM users WHERE active = true
`;

const users: User[] = [];
for await (const row of rows) {
  users.push(row);
}
```

### `queryAll` - All Rows as Array

Returns all rows as an array (convenience wrapper around `query`):

```typescript
const users = await db.queryAll<User>`
  SELECT id, email, name FROM users WHERE active = true
`;
// users is User[]
```

### `queryRow` - Single Row

Returns one row or null:

```typescript
const user = await db.queryRow<User>`
  SELECT id, email, name FROM users WHERE id = ${userId}
`;

if (!user) {
  throw APIError.notFound("user not found");
}
```

### `exec` - No Return Value

For INSERT, UPDATE, DELETE operations:

```typescript
await db.exec`
  INSERT INTO users (id, email, name)
  VALUES (${id}, ${email}, ${name})
`;

await db.exec`
  UPDATE users SET name = ${newName} WHERE id = ${id}
`;

await db.exec`
  DELETE FROM users WHERE id = ${id}
`;
```

### Raw Query Methods

Use raw SQL strings with positional parameters (`$1`, `$2`, etc.) instead of template literals:

```typescript
// Raw query returning multiple rows
const rows = await db.rawQuery<User>("SELECT * FROM users WHERE active = $1", true);

// Raw query returning single row
const user = await db.rawQueryRow<User>("SELECT * FROM users WHERE id = $1", userId);

// Raw query returning all rows as array
const users = await db.rawQueryAll<User>("SELECT * FROM users WHERE role = $1", "admin");

// Raw exec for INSERT/UPDATE/DELETE
await db.rawExec("INSERT INTO users (id, email) VALUES ($1, $2)", id, email);
```

## Database Sharing Across Services

Reference a database owned by another service using `SQLDatabase.named()`:

```typescript
import { SQLDatabase } from "encore.dev/storage/sqldb";

// In the service that owns the database
const db = new SQLDatabase("shared-db", {
  migrations: "./migrations",
});

// In another service that needs access
const sharedDb = SQLDatabase.named("shared-db");

// Now you can query the shared database
const user = await sharedDb.queryRow<User>`SELECT * FROM users WHERE id = ${id}`;
```

## Migrations

### File Structure

```
service/
└── migrations/
    ├── 001_create_users.up.sql
    ├── 002_add_posts.up.sql
    └── 003_add_indexes.up.sql
```

### Naming Convention

- Start with a number (001, 002, etc.)
- Followed by underscore and description
- End with `.up.sql`
- Numbers must be sequential

### Example Migration

```sql
-- migrations/001_create_users.up.sql
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email TEXT UNIQUE NOT NULL,
    name TEXT NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
```

## Drizzle ORM Integration

### Setup

```typescript
// db.ts
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { drizzle } from "drizzle-orm/node-postgres";

const db = new SQLDatabase("mydb", {
  migrations: {
    path: "migrations",
    source: "drizzle",
  },
});

export const orm = drizzle(db.connectionString);
```

### Schema

```typescript
// schema.ts
import * as p from "drizzle-orm/pg-core";

export const users = p.pgTable("users", {
  id: p.uuid().primaryKey().defaultRandom(),
  email: p.text().unique().notNull(),
  name: p.text().notNull(),
  createdAt: p.timestamp().defaultNow(),
});
```

### Drizzle Config

```typescript
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  out: "migrations",
  schema: "schema.ts",
  dialect: "postgresql",
});
```

Generate migrations: `drizzle-kit generate`

### Using Drizzle

```typescript
import { orm } from "./db";
import { users } from "./schema";
import { eq } from "drizzle-orm";

// Select
const allUsers = await orm.select().from(users);
const user = await orm.select().from(users).where(eq(users.id, id));

// Insert
await orm.insert(users).values({ email, name });

// Update
await orm.update(users).set({ name }).where(eq(users.id, id));

// Delete
await orm.delete(users).where(eq(users.id, id));
```

## SQL Injection Protection

Encore's template literals automatically escape values:

```typescript
// SAFE - values are parameterized
const email = "user@example.com";
await db.queryRow`SELECT * FROM users WHERE email = ${email}`;

// WRONG - SQL injection risk
await db.queryRow(`SELECT * FROM users WHERE email = '${email}'`);
```

## Guidelines

- Always use template literals for queries (automatic escaping)
- Specify types with generics: `query<User>`, `queryRow<User>`
- Migrations are applied automatically on startup
- Use `queryRow` when expecting 0 or 1 result
- Use `query` with async iteration for multiple rows
- Database names should be lowercase, descriptive
- Each service typically has its own database

<!-- chapter:end slug=database -->

---

<!-- chapter:begin slug=frontend position=8 -->

## 8. encore-frontend

- **Source:** https://github.com/encoredev/skills/blob/main/encore/frontend/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/frontend/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/frontend.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-frontend
description: Connect a frontend application (React, Next.js, Vue, Svelte, etc.) to an Encore.ts backend.
when_to_use: >-
  User wants to generate a TypeScript API client for the browser (`encore gen client`), call Encore endpoints from React/Next.js/Vue/Svelte/SvelteKit/Remix/Astro, configure CORS in `encore.app`, or wire authentication tokens through a generated client. Trigger phrases: "TypeScript client", "API client", "Next.js frontend", "React frontend", "encore gen client", "CORS", "browser fetch", "SPA", "frontend can call this backend".
---

# Frontend Integration with Encore

## Instructions

Encore provides tools to connect your frontend applications to your backend APIs.

### Generate a TypeScript Client

```bash
# Generate client for local development
encore gen client --output=./frontend/src/client.ts --env=local

# Generate client for a deployed environment
encore gen client --output=./frontend/src/client.ts --env=staging
```

This generates a fully typed client based on your API definitions.

### Using the Generated Client

```typescript
// frontend/src/client.ts is auto-generated
import Client from "./client";

const client = new Client("http://localhost:4000");

// Fully typed API calls
const user = await client.user.getUser({ id: "123" });
console.log(user.email);

const newUser = await client.user.createUser({
  email: "new@example.com",
  name: "New User",
});
```

### React Example

```tsx
// frontend/src/components/UserProfile.tsx
import { useState, useEffect } from "react";
import Client from "../client";

const client = new Client(import.meta.env.VITE_API_URL);

export function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    client.user.getUser({ id: userId })
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}
```

### React with TanStack Query

```tsx
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import Client from "../client";

const client = new Client(import.meta.env.VITE_API_URL);

export function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ["user", userId],
    queryFn: () => client.user.getUser({ id: userId }),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return <div>{user.name}</div>;
}

export function CreateUserForm() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: (data: { email: string; name: string }) => 
      client.user.createUser(data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    mutation.mutate({
      email: formData.get("email") as string,
      name: formData.get("name") as string,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <input name="name" required />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? "Creating..." : "Create User"}
      </button>
    </form>
  );
}
```

### Next.js Server Components

```tsx
// app/users/[id]/page.tsx
import Client from "@/lib/client";

const client = new Client(process.env.API_URL);

export default async function UserPage({ params }: { params: { id: string } }) {
  const user = await client.user.getUser({ id: params.id });
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}
```

### CORS Configuration

Configure CORS in your `encore.app` file:

```json
{
    "id": "my-app",
    "global_cors": {
        "allow_origins_with_credentials": [
            "http://localhost:3000",
            "https://myapp.com",
            "https://*.myapp.com"
        ]
    }
}
```

### CORS Options

| Option | Description |
|--------|-------------|
| `allow_origins_without_credentials` | Origins allowed for non-credentialed requests (default: `["*"]`) |
| `allow_origins_with_credentials` | Origins allowed for credentialed requests (cookies, auth headers) |
| `allow_headers` | Additional request headers to allow |
| `expose_headers` | Additional response headers to expose |
| `debug` | Enable CORS debug logging |

### Authentication from Frontend

For authenticated requests, pass the Authorization header:

```typescript
// Using fetch
const response = await fetch("http://localhost:4000/profile", {
  headers: {
    "Authorization": `Bearer ${token}`,
  },
});

// Or include credentials for cookie-based auth
const response = await fetch("http://localhost:4000/profile", {
  credentials: "include",
});
```

With TanStack Query, configure a default fetcher:

```typescript
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      queryFn: async ({ queryKey }) => {
        const response = await fetch(queryKey[0] as string, {
          headers: { Authorization: `Bearer ${getToken()}` },
        });
        if (!response.ok) throw new Error("Request failed");
        return response.json();
      },
    },
  },
});
```

### Using Plain Fetch

If you prefer not to use the generated client:

```typescript
async function getUser(id: string) {
  const response = await fetch(`http://localhost:4000/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
  return response.json();
}

async function createUser(email: string, name: string) {
  const response = await fetch("http://localhost:4000/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, name }),
  });
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
  return response.json();
}
```

### Environment Variables

```bash
# .env.local (Next.js)
NEXT_PUBLIC_API_URL=http://localhost:4000

# .env (Vite)
VITE_API_URL=http://localhost:4000
```

### Guidelines

- Use `encore gen client` to generate typed API clients
- Regenerate the client when your API changes
- Configure CORS in `encore.app` for production domains
- Use `allow_origins_with_credentials` for authenticated requests
- Include `Authorization` header for token-based auth
- Use `credentials: "include"` for cookie-based auth
- Use environment variables for API URLs (different per environment)
- The generated client handles errors and types automatically

<!-- chapter:end slug=frontend -->

---

<!-- chapter:begin slug=getting-started position=9 -->

## 9. encore-getting-started

- **Source:** https://github.com/encoredev/skills/blob/main/encore/getting-started/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/getting-started/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/getting-started.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-getting-started
description: Bootstrap a brand-new Encore.ts project from zero. Only for first-time CLI install and `encore app create` — not for architecture or feature questions.
when_to_use: >-
  User has no Encore project yet and is asking how to install the Encore CLI, run `encore app create`, scaffold a hello-world app, or run their very first `encore run`. SKIP if the user already has an Encore project and is asking about architecture, services, endpoints, or specific features — those go to `encore-service`, `encore-api`, `encore-pubsub`, etc. Trigger phrases: "completely new to Encore", "first time", "install the CLI", "brew install encoredev", "encore app create", "hello world", "just starting out".
---

# Getting Started with Encore.ts

## Instructions

### Install Encore CLI

```bash
# macOS
brew install encoredev/tap/encore

# Linux/WSL
curl -L https://encore.dev/install.sh | bash

# Windows (PowerShell)
iwr https://encore.dev/install.ps1 | iex
```

### Create a New App

```bash
# Interactive - choose from templates
encore app create my-app

# Or start with a blank app
encore app create my-app --example=ts/hello-world
```

### Project Structure

A minimal Encore.ts app:

```
my-app/
├── encore.app           # App configuration
├── package.json         # Dependencies
├── tsconfig.json        # TypeScript config
├── encore.service.ts    # Service definition
└── api.ts               # API endpoints
```

### The encore.app File

```cue
// encore.app
{
    "id": "my-app"
}
```

This file marks the root of your Encore app. The `id` is your app's unique identifier.

### Define a Service

Create `encore.service.ts` to define a service:

```typescript
// encore.service.ts
import { Service } from "encore.dev/service";

export default new Service("my-service");
```

### Create Your First API

```typescript
// api.ts
import { api } from "encore.dev/api";

interface HelloResponse {
  message: string;
}

export const hello = api(
  { method: "GET", path: "/hello", expose: true },
  async (): Promise<HelloResponse> => {
    return { message: "Hello, World!" };
  }
);
```

### Run Your App

```bash
# Start the development server
encore run

# Your API is now available at http://localhost:4000
```

### Open the Local Dashboard

```bash
# Opens the local development dashboard
encore run
# Then visit http://localhost:9400
```

The dashboard shows:
- All your services and endpoints
- Request/response logs
- Database queries
- Traces and spans

### Common CLI Commands

| Command | Description |
|---------|-------------|
| `encore run` | Start the local development server |
| `encore test` | Run tests |
| `encore db shell <db>` | Open a psql shell to a database |
| `encore gen client` | Generate API client code |
| `encore app link` | Link to an existing Encore Cloud app |

### Add a Database

```typescript
// db.ts
import { SQLDatabase } from "encore.dev/storage/sqldb";

const db = new SQLDatabase("mydb", {
  migrations: "./migrations",
});
```

Create a migration:

```sql
-- migrations/1_create_table.up.sql
CREATE TABLE items (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL
);
```

### Next Steps

- Add more endpoints (see `encore-api` skill)
- Add authentication (see `encore-auth` skill)
- Add Pub/Sub topics (`encore-pubsub`), cron jobs (`encore-cron`), buckets (`encore-bucket`), secrets (`encore-secret`), or caching (`encore-cache`)
- Deploy to Encore Cloud: `encore app link` then `git push encore`

<!-- chapter:end slug=getting-started -->

---

<!-- chapter:begin slug=go-api position=10 -->

## 10. encore-go-api

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-api/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-api/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-api.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-api
description: Define typed API endpoints in Encore Go using `//encore:api` annotations. Covers typed request/response structs, path/query/header/cookie params, and error returns. For raw endpoints (`//encore:api raw`) and inbound webhooks, use `encore-go-webhook` instead.
when_to_use: >-
  User wants to define an endpoint, route, or REST handler in their own Go service — anything with a typed JSON request/response shape. Mentions of an endpoint, GET/POST/PUT/PATCH/DELETE, paths like `/orders` or `/users/:id`, request body, query parameters (`query:"name"` tag), path parameters, headers (`header:"Name"` tag), HTTP status codes, request validation, `errs.NotFound` / 4xx-5xx errors, or `//encore:api public`. Trigger phrases: "POST endpoint at /orders", "typed Go endpoint", "GET /users/:id", "request validation", "return 404", "JSON response shape".
---

# Encore Go API Endpoints

## Instructions

When creating API endpoints with Encore Go, follow these patterns:

### 1. Basic API Endpoint

Use the `//encore:api` annotation above your function:

```go
package user

import "context"

type GetUserParams struct {
    ID string
}

type User struct {
    ID    string `json:"id"`
    Email string `json:"email"`
    Name  string `json:"name"`
}

//encore:api public method=GET path=/users/:id
func GetUser(ctx context.Context, params *GetUserParams) (*User, error) {
    // Implementation
    return &User{ID: params.ID, Email: "user@example.com", Name: "John"}, nil
}
```

### 2. POST with Request Body

```go
type CreateUserParams struct {
    Email string `json:"email"`
    Name  string `json:"name"`
}

//encore:api public method=POST path=/users
func CreateUser(ctx context.Context, params *CreateUserParams) (*User, error) {
    // Implementation
    return &User{ID: "new-id", Email: params.Email, Name: params.Name}, nil
}
```

## API Annotation Options

| Option | Values | Description |
|--------|--------|-------------|
| `public` | - | Accessible from outside |
| `private` | - | Only callable from other services |
| `auth` | - | Requires authentication |
| `method` | GET, POST, PUT, PATCH, DELETE | HTTP method |
| `path` | string | URL path with `:param` for path params |
| `sensitive` | - | Redacts request/response payloads from traces |

### Examples

```go
//encore:api public method=GET path=/health
//encore:api private method=POST path=/internal/process
//encore:api auth method=GET path=/profile
//encore:api public sensitive method=POST path=/auth/login
```

## Sensitive Data

Mark sensitive fields to redact them from tracing logs:

```go
type LoginParams struct {
    Email    string `json:"email"`
    Password string `json:"password" encore:"sensitive"`
}
```

Or mark the entire endpoint as sensitive in the annotation:

```go
//encore:api public sensitive method=POST path=/auth/login
func Login(ctx context.Context, params *LoginParams) (*TokenResponse, error) {
    // Request and response will be redacted from traces
}
```

## Custom HTTP Status Codes

Return custom HTTP status codes using the `encore:"httpstatus"` tag:

```go
type CreateResponse struct {
    ID     string `json:"id"`
    Status int    `encore:"httpstatus"`
}

//encore:api public method=POST path=/items
func CreateItem(ctx context.Context, params *CreateParams) (*CreateResponse, error) {
    item := createItem(params)
    return &CreateResponse{
        ID:     item.ID,
        Status: 201,  // Returns HTTP 201 Created
    }, nil
}
```

## Request Parameter Sources

### Path Parameters

```go
// Path: /users/:id
type GetUserParams struct {
    ID string  // Automatically mapped from :id
}
```

### Query Parameters

```go
// Path: /users
type ListUsersParams struct {
    Limit  int `query:"limit"`
    Offset int `query:"offset"`
}

//encore:api public method=GET path=/users
func ListUsers(ctx context.Context, params *ListUsersParams) (*ListResponse, error) {
    // params.Limit and params.Offset come from query string
}
```

### Headers

```go
type WebhookParams struct {
    Signature string `header:"X-Webhook-Signature"`
    Payload   string `json:"payload"`
}
```

### Cookies

```go
import "net/http"

type AuthParams struct {
    SessionCookie *http.Cookie `cookie:"session"`
    CSRFToken     string       `header:"X-CSRF-Token"`
}

//encore:api auth method=POST path=/logout
func Logout(ctx context.Context, params *AuthParams) error {
    // Access params.SessionCookie.Value
    return nil
}
```

## Response Types

### Standard Response

```go
type Response struct {
    Message string `json:"message"`
}

//encore:api public method=GET path=/hello
func Hello(ctx context.Context) (*Response, error) {
    return &Response{Message: "Hello, World!"}, nil
}
```

### No Response Body

```go
//encore:api public method=DELETE path=/users/:id
func DeleteUser(ctx context.Context, params *DeleteParams) error {
    // Return only error (no response body on success)
    return nil
}
```

### No Request Parameters

```go
//encore:api public method=GET path=/health
func Health(ctx context.Context) (*HealthResponse, error) {
    return &HealthResponse{Status: "ok"}, nil
}
```

## Error Handling

Use `errs` package for proper HTTP error responses:

```go
import "encore.dev/beta/errs"

//encore:api public method=GET path=/users/:id
func GetUser(ctx context.Context, params *GetUserParams) (*User, error) {
    user, err := findUser(params.ID)
    if err != nil {
        return nil, err
    }
    if user == nil {
        return nil, &errs.Error{
            Code:    errs.NotFound,
            Message: "user not found",
        }
    }
    return user, nil
}
```

### Common Error Codes

| Code | HTTP Status | Usage |
|------|-------------|-------|
| `errs.NotFound` | 404 | Resource doesn't exist |
| `errs.InvalidArgument` | 400 | Bad input |
| `errs.Unauthenticated` | 401 | Missing/invalid auth |
| `errs.PermissionDenied` | 403 | Not allowed |
| `errs.AlreadyExists` | 409 | Duplicate resource |

## Guidelines

- Use `//encore:api` annotation above the function
- Request params must be a pointer to a struct or omitted
- Response must be a pointer to a struct (or omit for no body)
- Always return `error` as the last return value
- Use struct tags for JSON field names, query params, and headers
- Path parameters are automatically mapped to struct fields by name

<!-- chapter:end slug=go-api -->

---

<!-- chapter:begin slug=go-auth position=11 -->

## 11. encore-go-auth

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-auth/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-auth/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-auth.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-auth
description: Protect Encore Go endpoints with authentication and authorize callers. Covers `auth.AuthHandler`, `auth.UserID`, the `Authorization` header, and `//encore:api auth`.
when_to_use: >-
  User wants to require login on a Go endpoint, restrict an endpoint to authenticated/signed-in users, validate a bearer token / JWT / API key from an `Authorization` header, read the current user inside a handler (`auth.UserID()` / `auth.Data()`), define an `auth.AuthHandler`, return `errs.Unauthenticated` from a handler, or use `//encore:api auth` on a handler. Trigger phrases: "protect this endpoint", "only authenticated users", "require login", "Authorization header", "bearer token", "401", "403", "who is calling", "current user".
---

# Encore Go Authentication

## Instructions

Encore Go provides a built-in authentication system using the `//encore:authhandler` annotation.

### 1. Create an Auth Handler

```go
package auth

import (
    "context"
    "encore.dev/beta/auth"
    "encore.dev/beta/errs"
)

// AuthParams defines what the auth handler receives
type AuthParams struct {
    Authorization string `header:"Authorization"`
}

// AuthData defines what authenticated requests have access to
type AuthData struct {
    UserID string
    Email  string
    Role   string
}

//encore:authhandler
func Authenticate(ctx context.Context, params *AuthParams) (auth.UID, *AuthData, error) {
    token := strings.TrimPrefix(params.Authorization, "Bearer ")
    
    payload, err := verifyToken(token)
    if err != nil {
        return "", nil, &errs.Error{
            Code:    errs.Unauthenticated,
            Message: "invalid token",
        }
    }
    
    return auth.UID(payload.UserID), &AuthData{
        UserID: payload.UserID,
        Email:  payload.Email,
        Role:   payload.Role,
    }, nil
}
```

### 2. Protect Endpoints

```go
package user

import "context"

// Protected endpoint - requires authentication
//encore:api auth method=GET path=/profile
func GetProfile(ctx context.Context) (*Profile, error) {
    // Only authenticated users reach here
}

// Public endpoint - no authentication required
//encore:api public method=GET path=/health
func Health(ctx context.Context) (*HealthResponse, error) {
    return &HealthResponse{Status: "ok"}, nil
}
```

### 3. Access Auth Data in Endpoints

```go
package user

import (
    "context"
    "encore.dev/beta/auth"
    myauth "myapp/auth"  // Import your auth package
)

//encore:api auth method=GET path=/profile
func GetProfile(ctx context.Context) (*Profile, error) {
    // Get the user ID
    userID, ok := auth.UserID()
    if !ok {
        // Should not happen with auth endpoint
    }
    
    // Get full auth data
    data := auth.Data().(*myauth.AuthData)
    
    return &Profile{
        UserID: string(userID),
        Email:  data.Email,
        Role:   data.Role,
    }, nil
}
```

## Auth Handler Signature

The auth handler must:
1. Have the `//encore:authhandler` annotation
2. Accept `context.Context` and a params struct pointer
3. Return `(auth.UID, *YourAuthData, error)`

```go
//encore:authhandler
func MyAuthHandler(ctx context.Context, params *Params) (auth.UID, *AuthData, error)
```

## Auth Handler Behavior

| Scenario | Returns | Result |
|----------|---------|--------|
| Valid credentials | `(uid, data, nil)` | Request authenticated |
| Invalid credentials | `("", nil, err)` with `errs.Unauthenticated` | 401 response |
| Other error | `("", nil, err)` | Request aborted |

## Common Auth Patterns

### JWT Token Validation

```go
import "github.com/golang-jwt/jwt/v5"

var secrets struct {
    JWTSecret string
}

func verifyToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(t *jwt.Token) (interface{}, error) {
        return []byte(secrets.JWTSecret), nil
    })
    if err != nil {
        return nil, err
    }
    
    claims, ok := token.Claims.(*Claims)
    if !ok || !token.Valid {
        return nil, errors.New("invalid token")
    }
    
    return claims, nil
}
```

### API Key Authentication

```go
//encore:authhandler
func Authenticate(ctx context.Context, params *AuthParams) (auth.UID, *AuthData, error) {
    apiKey := params.Authorization
    
    user, err := db.QueryRow[User](ctx, `
        SELECT id, email, role FROM users WHERE api_key = $1
    `, apiKey)
    if err != nil {
        return "", nil, &errs.Error{
            Code:    errs.Unauthenticated,
            Message: "invalid API key",
        }
    }
    
    return auth.UID(user.ID), &AuthData{
        UserID: user.ID,
        Email:  user.Email,
        Role:   user.Role,
    }, nil
}
```

### Cookie-Based Auth

```go
type AuthParams struct {
    Cookie string `header:"Cookie"`
}

//encore:authhandler
func Authenticate(ctx context.Context, params *AuthParams) (auth.UID, *AuthData, error) {
    sessionID := parseCookie(params.Cookie, "session")
    if sessionID == "" {
        return "", nil, &errs.Error{
            Code:    errs.Unauthenticated,
            Message: "no session",
        }
    }

    session, err := getSession(ctx, sessionID)
    if err != nil || session.ExpiresAt.Before(time.Now()) {
        return "", nil, &errs.Error{
            Code:    errs.Unauthenticated,
            Message: "session expired",
        }
    }

    return auth.UID(session.UserID), &AuthData{
        UserID: session.UserID,
        Email:  session.Email,
        Role:   session.Role,
    }, nil
}
```

### Multi-Source Auth (Cookie + Header + Query)

Auth params can extract data from multiple sources:

```go
import "net/http"

type AuthParams struct {
    SessionCookie *http.Cookie `cookie:"session"`       // From cookie
    Authorization string       `header:"Authorization"` // From header
    ClientID      string       `query:"client_id"`      // From query string
}

//encore:authhandler
func Authenticate(ctx context.Context, params *AuthParams) (auth.UID, *AuthData, error) {
    // Try session cookie first
    if params.SessionCookie != nil {
        return authenticateWithSession(ctx, params.SessionCookie.Value)
    }

    // Fall back to Authorization header
    if params.Authorization != "" {
        return authenticateWithToken(ctx, params.Authorization)
    }

    return "", nil, &errs.Error{
        Code:    errs.Unauthenticated,
        Message: "no credentials provided",
    }
}
```

## Service-to-Service Auth

Auth data automatically propagates in internal service calls:

```go
package order

import (
    "context"
    "myapp/user"  // Import the user service
)

//encore:api auth method=GET path=/orders/:id
func GetOrderWithUser(ctx context.Context, params *GetOrderParams) (*OrderWithUser, error) {
    order, err := getOrder(ctx, params.ID)
    if err != nil {
        return nil, err
    }
    
    // Auth is automatically propagated to this call
    profile, err := user.GetProfile(ctx)
    if err != nil {
        return nil, err
    }
    
    return &OrderWithUser{Order: order, User: profile}, nil
}
```

## Testing with Auth

Override auth data in tests using `auth.WithContext`:

```go
package user_test

import (
    "context"
    "testing"

    "encore.dev/beta/auth"
    myauth "myapp/auth"
    "myapp/user"
)

func TestGetProfile(t *testing.T) {
    // Create a context with auth data
    ctx := auth.WithContext(
        context.Background(),
        auth.UID("test-user-123"),
        &myauth.AuthData{
            UserID: "test-user-123",
            Email:  "test@example.com",
            Role:   "user",
        },
    )

    // Call the endpoint with the authenticated context
    profile, err := user.GetProfile(ctx)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    if profile.Email != "test@example.com" {
        t.Errorf("expected test@example.com, got %s", profile.Email)
    }
}
```

## Guidelines

- Only one `//encore:authhandler` per application
- Return `auth.UID` as the first return value (user identifier)
- Return your custom `AuthData` struct as second value
- Use `auth.UserID()` to get the authenticated user ID
- Use `auth.Data()` and type assert to get full auth data
- Auth propagates automatically in service-to-service calls
- Use `auth.WithContext()` to override auth in tests
- Keep auth handlers fast - they run on every authenticated request

<!-- chapter:end slug=go-auth -->

---

<!-- chapter:begin slug=go-bucket position=12 -->

## 12. encore-go-bucket

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-bucket/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-bucket/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-bucket.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-bucket
description: Store unstructured files in Encore Go using `objects.NewBucket` from `encore.dev/storage/objects` — uploads, images, documents, blobs.
when_to_use: >-
  User wants to upload, download, list, or delete files from an Encore Go service — profile pictures, avatars, document uploads, image storage, media assets, generated reports, blob data. Covers public vs private buckets, signed upload/download URLs, bucket references with permission types (Uploader, Downloader, Lister, Attrser, Remover), and operations like `Upload`, `Download`, `List`, `SignedUploadURL`. Trigger phrases: "object storage", "bucket", "S3", "GCS", "blob", "user uploads", "profile picture", "image upload", "store a file", "file storage".
---

# Encore Go Object Storage

## Instructions

A `Bucket` is a logical store for files. Encore provisions the underlying object storage (S3 on AWS, GCS on GCP, in-memory locally). Declare buckets as package-level variables.

```go
package uploads

import "encore.dev/storage/objects"

// Private bucket (default)
var Uploads = objects.NewBucket("user-uploads", objects.BucketConfig{})

// Public bucket — files accessible via public URL
var PublicAssets = objects.NewBucket("public-assets", objects.BucketConfig{
    Public: true,
})
```

## Operations

```go
import (
    "fmt"
    "io"
)

// Upload (streaming pattern)
writer := Uploads.Upload(ctx, "path/to/file.jpg")
_, err := io.Copy(writer, dataReader)
if err != nil {
    writer.Abort()
    return err
}
err = writer.Close()

// Download
reader := Uploads.Download(ctx, "path/to/file.jpg")
if err := reader.Err(); err != nil {
    return err
}
defer reader.Close()
data, _ := io.ReadAll(reader)

// Existence check
exists, err := Uploads.Exists(ctx, "path/to/file.jpg")

// Attributes (size, content type, ETag)
attrs, err := Uploads.Attrs(ctx, "path/to/file.jpg")

// List
for err, entry := range Uploads.List(ctx, &objects.Query{}) {
    if err != nil {
        return err
    }
    fmt.Println(entry.Key, entry.Size)
}

// Delete
err := Uploads.Remove(ctx, "path/to/file.jpg")

// Public URL (only for public buckets)
url := PublicAssets.PublicURL("image.jpg")
```

## Signed URLs

Generate temporary URLs so clients can upload/download directly without going through your service:

```go
import "time"

// Signed upload URL (expires in 2 hours)
url, err := Uploads.SignedUploadURL(ctx, "user-uploads/avatar.jpg",
    objects.WithTTL(time.Duration(7200)*time.Second))

// Signed download URL
url, err := Uploads.SignedDownloadURL(ctx, "documents/report.pdf",
    objects.WithTTL(time.Duration(7200)*time.Second))
```

## Bucket References

Pass bucket access to library code with a specific permission set:

```go
// Create a reference with download permission only
ref := objects.BucketRef[objects.Downloader](Uploads)

// Combine permissions via an interface
type myPerms interface {
    objects.Downloader
    objects.Uploader
}
ref := objects.BucketRef[myPerms](Uploads)

// Permission types: Downloader, Uploader, Lister, Attrser, Remover,
// SignedDownloader, SignedUploader, ReadWriter
```

## Guidelines

- Declare buckets as package-level variables.
- Default to private buckets; opt into `Public: true` only for assets meant for unauthenticated download.
- Use signed URLs for browser uploads/downloads instead of streaming through your service.
- Use bucket references when passing access to helpers — they encode the permission contract in the type system.

<!-- chapter:end slug=go-bucket -->

---

<!-- chapter:begin slug=go-cache position=13 -->

## 13. encore-go-cache

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-cache/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-cache/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-cache.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-cache
description: Cache data in Redis from Encore Go using `cache.NewCluster` and typed keyspaces from `encore.dev/storage/cache`. Type-safe key/value access with TTLs, atomic increments, and per-keyspace data shapes.
when_to_use: >-
  User wants to cache values in a Go service, store ephemeral state, rate-limit by counter, build a leaderboard, speed up a hot read, or store short-lived tokens. Covers `cache.NewCluster`, `cache.NewStringKeyspace`, `cache.NewIntKeyspace`, `cache.NewStructKeyspace`, list/set keyspaces, TTL helpers (`cache.ExpireIn`, `cache.ExpireDailyAt`, `cache.NeverExpire`), atomic `Increment`/`Decrement`, `SetIfNotExists`, `Replace`, eviction policies, and `cache.Miss`/`cache.KeyExists` errors. Trigger phrases: "cache this", "Redis", "key-value store", "rate limit", "TTL", "expire after", "in-memory store", "session token store", "leaderboard counter".
---

# Encore Go Caching (Redis)

## Instructions

Encore Go's cache is a typed wrapper around Redis. Declare a `cache.Cluster` once, then create `Keyspace` objects for each shape of data you need.

### Cluster

```go
package mycache

import "encore.dev/storage/cache"

var Cluster = cache.NewCluster("my-cache", cache.ClusterConfig{
    EvictionPolicy: cache.AllKeysLRU,
})
```

Eviction policies: `cache.AllKeysLRU` (default), `cache.NoEviction`, `cache.AllKeysLFU`, `cache.AllKeysRandom`, `cache.VolatileLRU`, `cache.VolatileLFU`, `cache.VolatileTTL`, `cache.VolatileRandom`.

### Keyspace types

Each keyspace has a key shape (used to build the Redis key from `KeyPattern`) and a value type.

```go
package mycache

import (
    "context"
    "time"
    "encore.dev/storage/cache"
)

type TokenKey struct {
    TokenID string
}

// Strings
var Tokens = cache.NewStringKeyspace[TokenKey](Cluster, cache.KeyspaceConfig{
    KeyPattern:    "token/:TokenID",
    DefaultExpiry: cache.ExpireIn(time.Hour),
})

func example(ctx context.Context) {
    _ = Tokens.Set(ctx, TokenKey{TokenID: "abc"}, "value")
    val, err := Tokens.Get(ctx, TokenKey{TokenID: "abc"}) // cache.Miss on miss
    _ = Tokens.Delete(ctx, TokenKey{TokenID: "abc"})
    _ = val
    _ = err
}
```

```go
// Integers (atomic counters)
type CounterKey struct {
    UserID string
}

var Counters = cache.NewIntKeyspace[CounterKey](Cluster, cache.KeyspaceConfig{
    KeyPattern:    "requests/:UserID",
    DefaultExpiry: cache.ExpireIn(10 * time.Second),
})

func incr(ctx context.Context) {
    count, _ := Counters.Increment(ctx, CounterKey{UserID: "user123"}, 1)
    _, _ = Counters.Decrement(ctx, CounterKey{UserID: "user123"}, 1)
    _ = count
}
```

```go
// Structs (JSON-encoded)
type ProfileKey struct {
    UserID string
}

type UserProfile struct {
    Name  string
    Email string
}

var Profiles = cache.NewStructKeyspace[ProfileKey, UserProfile](Cluster, cache.KeyspaceConfig{
    KeyPattern:    "profile/:UserID",
    DefaultExpiry: cache.ExpireIn(time.Hour),
})

func setProfile(ctx context.Context) {
    _ = Profiles.Set(ctx, ProfileKey{UserID: "123"}, UserProfile{
        Name: "Alice", Email: "alice@example.com",
    })
}
```

### Other keyspace types

All from `encore.dev/storage/cache`:

- `NewFloatKeyspace` — float64 values, has `Increment`.
- `NewListKeyspace` — list values, with `PushLeft`/`PushRight`/`PopLeft`/`PopRight`/`GetRange`.
- `NewSetKeyspace` — set values, with `Add`/`Remove`/`Contains`/`Items`.

### Multi-field key patterns

```go
type ResourceKey struct {
    UserID       string
    ResourcePath string
}

var ResourceRequests = cache.NewIntKeyspace[ResourceKey](Cluster, cache.KeyspaceConfig{
    KeyPattern:    "requests/:UserID/:ResourcePath",
    DefaultExpiry: cache.ExpireIn(10 * time.Second),
})
```

### Expiry helpers

```go
import (
    "encore.dev/storage/cache"
    "time"
)

cache.ExpireIn(time.Hour)              // relative
cache.ExpireDailyAt(2, 0, 0, time.UTC) // specific UTC time each day
cache.NeverExpire                      // no expiry
cache.KeepTTL                          // keep existing TTL when updating
```

### Errors

```go
import "encore.dev/storage/cache"

val, err := keyspace.Get(ctx, key)
if errors.Is(err, cache.Miss) {
    // not in cache
}

err = keyspace.SetIfNotExists(ctx, key, value)
if errors.Is(err, cache.KeyExists) {
    // already there
}
```

## Guidelines

- Declare `cache.Cluster` and keyspaces as package-level variables.
- Pick the most specific keyspace type — `IntKeyspace` for counters gives you atomic `Increment`/`Decrement` for free.
- `Get()` returns `cache.Miss` on miss; `Replace()` and `SetIfNotExists()` return `cache.Miss`/`cache.KeyExists` on conflict.
- Local development uses an in-memory Redis with a ~100-key cap — don't load-test it.
- For durable storage, use `encore-go-database` (Postgres) or `encore-go-bucket` (object storage) instead.

<!-- chapter:end slug=go-cache -->

---

<!-- chapter:begin slug=go-code-review position=14 -->

## 14. encore-go-code-review

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-code-review/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-code-review/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-code-review.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-code-review
description: Review existing Encore Go code for best practices and common anti-patterns.
when_to_use: >-
  User is reviewing a pull request, auditing existing code, or checking for Encore-specific anti-patterns before merging — infrastructure declared inside functions, missing service files, wrong import paths, raw `errors.New(...)` thrown instead of `errs.B`, untyped APIs, panicking in handlers. SKIP for greenfield code being actively written. Trigger phrases: "audit", "review", "before merge", "PR review", "anti-patterns", "code smell", "lint this".
---

# Encore Go Code Review

## Instructions

When reviewing Encore Go code, check for these common issues:

## Critical Issues

### 1. Infrastructure Inside Functions

```go
// WRONG: Infrastructure declared inside function
func setup() {
    db := sqldb.NewDatabase("mydb", sqldb.DatabaseConfig{...})
    topic := pubsub.NewTopic[*Event]("events", pubsub.TopicConfig{...})
}

// CORRECT: Package level declaration
var db = sqldb.NewDatabase("mydb", sqldb.DatabaseConfig{
    Migrations: "./migrations",
})

var topic = pubsub.NewTopic[*Event]("events", pubsub.TopicConfig{
    DeliveryGuarantee: pubsub.AtLeastOnce,
})
```

### 2. Missing Context Parameter

```go
// WRONG: Missing context
//encore:api public method=GET path=/users/:id
func GetUser(params *GetUserParams) (*User, error) {
    // ...
}

// CORRECT: Context as first parameter
//encore:api public method=GET path=/users/:id
func GetUser(ctx context.Context, params *GetUserParams) (*User, error) {
    // ...
}
```

### 3. SQL Injection Risk

```go
// WRONG: String interpolation
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
rows, err := db.Query(ctx, query)

// CORRECT: Parameterized query
rows, err := sqldb.Query[User](ctx, db, `
    SELECT * FROM users WHERE email = $1
`, email)
```

### 4. Wrong Return Types

```go
// WRONG: Returning non-pointer struct
//encore:api public method=GET path=/users/:id
func GetUser(ctx context.Context, params *GetUserParams) (User, error) {
    // ...
}

// CORRECT: Return pointer to struct
//encore:api public method=GET path=/users/:id
func GetUser(ctx context.Context, params *GetUserParams) (*User, error) {
    // ...
}
```

### 5. Ignoring Errors

```go
// WRONG: Ignoring error
user, _ := sqldb.QueryRow[User](ctx, db, query, id)

// CORRECT: Handle error
user, err := sqldb.QueryRow[User](ctx, db, query, id)
if err != nil {
    return nil, err
}
```

## Warning Issues

### 6. Not Checking for ErrNoRows

```go
// RISKY: Returns nil without proper error
func getUser(ctx context.Context, id string) (*User, error) {
    user, err := sqldb.QueryRow[User](ctx, db, `
        SELECT * FROM users WHERE id = $1
    `, id)
    if err != nil {
        return nil, err  // ErrNoRows returns generic error
    }
    return user, nil
}

// BETTER: Check for not found specifically
import "errors"

func getUser(ctx context.Context, id string) (*User, error) {
    user, err := sqldb.QueryRow[User](ctx, db, `
        SELECT * FROM users WHERE id = $1
    `, id)
    if errors.Is(err, sqldb.ErrNoRows) {
        return nil, &errs.Error{
            Code:    errs.NotFound,
            Message: "user not found",
        }
    }
    if err != nil {
        return nil, err
    }
    return user, nil
}
```

### 7. Public Internal Endpoints

```go
// CHECK: Should this cron endpoint be public?
//encore:api public method=POST path=/internal/cleanup
func CleanupJob(ctx context.Context) error {
    // ...
}

// BETTER: Use private for internal endpoints
//encore:api private
func CleanupJob(ctx context.Context) error {
    // ...
}
```

### 8. Non-Idempotent Subscription Handlers

```go
// RISKY: Not idempotent (pubsub has at-least-once delivery)
var _ = pubsub.NewSubscription(OrderCreated, "process-order",
    pubsub.SubscriptionConfig[*OrderCreatedEvent]{
        Handler: func(ctx context.Context, event *OrderCreatedEvent) error {
            return chargeCustomer(ctx, event.OrderID)  // Could charge twice!
        },
    },
)

// SAFER: Check before processing
var _ = pubsub.NewSubscription(OrderCreated, "process-order",
    pubsub.SubscriptionConfig[*OrderCreatedEvent]{
        Handler: func(ctx context.Context, event *OrderCreatedEvent) error {
            order, err := getOrder(ctx, event.OrderID)
            if err != nil {
                return err
            }
            if order.Status != "pending" {
                return nil  // Already processed
            }
            return chargeCustomer(ctx, event.OrderID)
        },
    },
)
```

### 9. Not Closing Query Rows

```go
// WRONG: Rows not closed
func listUsers(ctx context.Context) ([]*User, error) {
    rows, err := sqldb.Query[User](ctx, db, `SELECT * FROM users`)
    if err != nil {
        return nil, err
    }
    // Missing: defer rows.Close()
    
    var users []*User
    for rows.Next() {
        users = append(users, rows.Value())
    }
    return users, nil
}

// CORRECT: Always close rows
func listUsers(ctx context.Context) ([]*User, error) {
    rows, err := sqldb.Query[User](ctx, db, `SELECT * FROM users`)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var users []*User
    for rows.Next() {
        users = append(users, rows.Value())
    }
    return users, rows.Err()
}
```

## Review Checklist

- [ ] All infrastructure at package level
- [ ] All API endpoints have `context.Context` as first parameter
- [ ] SQL uses parameterized queries (`$1`, `$2`, etc.)
- [ ] Response types are pointers
- [ ] Errors are handled, not ignored
- [ ] `sqldb.ErrNoRows` checked where appropriate
- [ ] Internal endpoints use `private` not `public`
- [ ] Subscription handlers are idempotent
- [ ] Query rows are closed with `defer rows.Close()`
- [ ] Migrations follow naming convention (`1_name.up.sql`)

## Output Format

When reviewing, report issues as:

```
[CRITICAL] [file:line] Description of issue
[WARNING] [file:line] Description of concern  
[GOOD] Notable good practice observed
```

<!-- chapter:end slug=go-code-review -->

---

<!-- chapter:begin slug=go-cron position=15 -->

## 15. encore-go-cron

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-cron/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-cron/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-cron.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-cron
description: >-
  Schedule periodic / recurring work in Encore Go using `cron.NewJob` from `encore.dev/cron`. Covers `Every: "1h"` interval syntax and `Schedule: "0 9 * * 1"` cron expressions.
when_to_use: >-
  User wants to run a Go job on a schedule — anything with the words schedule, scheduled, daily, hourly, weekly, periodic, recurring, every N minutes/hours, "at HH:MM UTC", midnight, batch job, aggregation job, nightly, cleanup job, or background work that runs on a timer rather than in response to a request. Trigger phrases: "every day at 02:00 UTC", "daily aggregation", "run hourly", "scheduled task", "cron", "nightly cleanup", "on a schedule".
---

# Encore Go Cron Jobs

## Instructions

A `cron.NewJob` declaration in Encore Go ties a schedule to an existing `//encore:api` endpoint. The endpoint runs at the chosen cadence. Declare the job as a package-level variable — not inside a function.

```go
package cleanup

import (
    "context"
    "encore.dev/cron"
)

// 1. The endpoint to call (typically private: //encore:api private)
//encore:api private
func CleanupExpiredSessions(ctx context.Context) error {
    // Cleanup logic
    return nil
}

// 2. Package-level cron declaration
var _ = cron.NewJob("cleanup-sessions", cron.JobConfig{
    Title:    "Clean up expired sessions",
    Schedule: "0 * * * *", // Every hour
    Endpoint: CleanupExpiredSessions,
})
```

## Schedule Formats

| Field | Example | Description |
|---|---|---|
| `Every` | `"1h"`, `"30m"`, `"6h"` | Simple interval. **Must divide 24h evenly** — `"7h"` is invalid. |
| `Schedule` | `"0 9 * * 1"` | Standard cron expression (5 fields, UTC). |

### Common cron expressions

| Cron | Meaning |
|---|---|
| `"0 * * * *"` | Every hour, on the hour |
| `"0 2 * * *"` | Daily at 02:00 UTC |
| `"0 0 * * 0"` | Weekly on Sunday at midnight UTC |
| `"0 4 15 * *"` | 04:00 UTC on the 15th of each month |

## Important behaviour

- **Cron jobs do not execute when running locally with `encore run`.** Only deployed environments fire crons.
- The cron endpoint should be `private` so it can't be triggered externally — only the cron scheduler should call it.
- All times in `Schedule` are UTC. Convert from local time when designing the schedule.
- The endpoint must be defined at module load — declare it before the `cron.NewJob` reference.

## Guidelines

- Use `Every` for "run on a regular interval" (must divide 24h).
- Use `Schedule` for specific times of day or days of week.
- Keep endpoint logic idempotent: a cron may fire late or be retried in a redeploy window.
- For event-driven background work (not time-driven), use the `encore-go-pubsub` skill instead.

<!-- chapter:end slug=go-cron -->

---

<!-- chapter:begin slug=go-database position=16 -->

## 16. encore-go-database

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-database/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-database/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-database.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-database
description: Work with PostgreSQL in Encore Go using `sqldb.NewDatabase` from `encore.dev/storage/sqldb` — schema migrations and SQL queries.
when_to_use: >-
  User wants to add a database table, write a migration, run a SQL query, insert/update/delete rows, or design a relational schema in an Encore Go service. Covers `sqldb.NewDatabase`, `db.QueryRow(ctx, ...)`, `db.Query(ctx, ...)`, `db.Exec(ctx, ...)`, `db.Stdlib()` for ORM integration, the `migrations/` directory, `*.up.sql` files, and sequential migration numbering. Trigger phrases: "Postgres table", "user_sessions table", "SQL", "migration", "QueryRow", "INSERT", "SELECT", "schema", "sqldb".
---

# Encore Go Database Operations

## Instructions

### Database Setup

```go
package user

import "encore.dev/storage/sqldb"

var db = sqldb.NewDatabase("userdb", sqldb.DatabaseConfig{
    Migrations: "./migrations",
})
```

## Query Methods

Encore's database API mirrors Go's standard `database/sql` package. Use `.Scan()` to read query results into variables.

### `Query` - Multiple Rows

```go
type User struct {
    ID    string
    Email string
    Name  string
}

func listActiveUsers(ctx context.Context) ([]*User, error) {
    rows, err := db.Query(ctx, `
        SELECT id, email, name FROM users WHERE active = true
    `)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []*User
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Email, &u.Name); err != nil {
            return nil, err
        }
        users = append(users, &u)
    }
    return users, rows.Err()
}
```

### `QueryRow` - Single Row

```go
func getUser(ctx context.Context, id string) (*User, error) {
    var u User
    err := db.QueryRow(ctx, `
        SELECT id, email, name FROM users WHERE id = $1
    `, id).Scan(&u.ID, &u.Email, &u.Name)

    if errors.Is(err, sqldb.ErrNoRows) {
        return nil, &errs.Error{
            Code:    errs.NotFound,
            Message: "user not found",
        }
    }
    if err != nil {
        return nil, err
    }
    return &u, nil
}
```

### `Exec` - No Return Value

For INSERT, UPDATE, DELETE operations:

```go
func createUser(ctx context.Context, email, name string) error {
    _, err := db.Exec(ctx, `
        INSERT INTO users (id, email, name)
        VALUES ($1, $2, $3)
    `, generateID(), email, name)
    return err
}

func updateUser(ctx context.Context, id, name string) error {
    _, err := db.Exec(ctx, `
        UPDATE users SET name = $1 WHERE id = $2
    `, name, id)
    return err
}

func deleteUser(ctx context.Context, id string) error {
    _, err := db.Exec(ctx, `
        DELETE FROM users WHERE id = $1
    `, id)
    return err
}
```

## Migrations

### File Structure

```
user/
└── migrations/
    ├── 1_create_users.up.sql
    ├── 2_add_posts.up.sql
    └── 3_add_indexes.up.sql
```

### Naming Convention

- Start with a number (1, 2, etc.)
- Followed by underscore and description
- End with `.up.sql`
- Numbers must be sequential

### Example Migration

```sql
-- migrations/1_create_users.up.sql
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email TEXT UNIQUE NOT NULL,
    name TEXT NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
```

## Transactions

```go
func transferFunds(ctx context.Context, fromID, toID string, amount int) error {
    tx, err := db.Begin(ctx)
    if err != nil {
        return err
    }
    defer tx.Rollback()  // No-op if committed
    
    _, err = tx.Exec(ctx, `
        UPDATE accounts SET balance = balance - $1 WHERE id = $2
    `, amount, fromID)
    if err != nil {
        return err
    }
    
    _, err = tx.Exec(ctx, `
        UPDATE accounts SET balance = balance + $1 WHERE id = $2
    `, amount, toID)
    if err != nil {
        return err
    }
    
    return tx.Commit()
}
```

## Using Scan

The `Scan` method reads columns from query results into variables. Columns are mapped by position, not by name - the order of arguments to `Scan` must match the order of columns in your SELECT statement.

```go
type User struct {
    ID        string
    Email     string
    Name      string
    CreatedAt time.Time
}

// Single row with QueryRow
func getUser(ctx context.Context, id string) (*User, error) {
    var u User
    err := db.QueryRow(ctx, `
        SELECT id, email, name, created_at FROM users WHERE id = $1
    `, id).Scan(&u.ID, &u.Email, &u.Name, &u.CreatedAt)
    if err != nil {
        return nil, err
    }
    return &u, nil
}

// You can also scan into an inline struct
func getItem(ctx context.Context, id int64) error {
    var item struct {
        ID    int64
        Title string
        Done  bool
    }
    err := db.QueryRow(ctx, `
        SELECT id, title, done FROM items WHERE id = $1
    `, id).Scan(&item.ID, &item.Title, &item.Done)
    return err
}
```

## SQL Injection Protection

Always use parameterized queries:

```go
// SAFE - values are parameterized
var u User
err := db.QueryRow(ctx, `
    SELECT id, email, name FROM users WHERE email = $1
`, email).Scan(&u.ID, &u.Email, &u.Name)

// WRONG - SQL injection risk
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
```

## Error Handling

```go
import (
    "errors"
    "encore.dev/storage/sqldb"
    "encore.dev/beta/errs"
)

func getUser(ctx context.Context, id string) (*User, error) {
    var u User
    err := db.QueryRow(ctx, `
        SELECT id, email, name FROM users WHERE id = $1
    `, id).Scan(&u.ID, &u.Email, &u.Name)

    if errors.Is(err, sqldb.ErrNoRows) {
        return nil, &errs.Error{
            Code:    errs.NotFound,
            Message: "user not found",
        }
    }
    if err != nil {
        return nil, err
    }
    return &u, nil
}
```

## Guidelines

- Always use parameterized queries (`$1`, `$2`, etc.)
- Use `Scan` to read query results - columns are mapped by position
- Check for `sqldb.ErrNoRows` when expecting a single row
- Migrations are applied automatically on startup
- Database names should be lowercase, descriptive
- Each service typically has its own database
- Use transactions for operations that must be atomic

<!-- chapter:end slug=go-database -->

---

<!-- chapter:begin slug=go-getting-started position=17 -->

## 17. encore-go-getting-started

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-getting-started/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-getting-started/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-getting-started.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-getting-started
description: Bootstrap a brand-new Encore Go project from zero. Only for first-time CLI install and `encore app create` — not for architecture or feature questions.
when_to_use: >-
  User has no Encore project yet and is asking how to install the Encore CLI, run `encore app create` for a Go app, scaffold a hello-world Go service, or run their very first `encore run`. SKIP if the user already has an Encore Go project and is asking about architecture, services, endpoints, or specific features — those go to `encore-go-service`, `encore-go-api`, `encore-go-pubsub`, etc. Trigger phrases: "completely new to Encore", "first time", "install the CLI", "brew install encoredev", "encore app create", "hello world Go", "just starting out".
---

# Getting Started with Encore Go

## Instructions

### Install Encore CLI

```bash
# macOS
brew install encoredev/tap/encore

# Linux/WSL
curl -L https://encore.dev/install.sh | bash

# Windows (PowerShell)
iwr https://encore.dev/install.ps1 | iex
```

### Create a New App

```bash
# Interactive - choose from templates
encore app create my-app

# Or start with a blank Go app
encore app create my-app --example=hello-world
```

### Project Structure

A minimal Encore Go app:

```
my-app/
├── encore.app           # App configuration
├── go.mod               # Go module
└── hello/               # A service (package with API)
    └── hello.go         # API endpoints
```

### The encore.app File

```cue
// encore.app
{
    "id": "my-app"
}
```

This file marks the root of your Encore app. The `id` is your app's unique identifier.

### Create Your First API

In Encore Go, any package with an `//encore:api` endpoint becomes a service:

```go
// hello/hello.go
package hello

import "context"

type Response struct {
    Message string `json:"message"`
}

//encore:api public method=GET path=/hello
func Hello(ctx context.Context) (*Response, error) {
    return &Response{Message: "Hello, World!"}, nil
}
```

### Run Your App

```bash
# Start the development server
encore run

# Your API is now available at http://localhost:4000
```

### Open the Local Dashboard

```bash
# Opens the local development dashboard
encore run
# Then visit http://localhost:9400
```

The dashboard shows:
- All your services and endpoints
- Request/response logs
- Database queries
- Traces and spans

### Common CLI Commands

| Command | Description |
|---------|-------------|
| `encore run` | Start the local development server |
| `encore test` | Run tests (uses `go test` under the hood) |
| `encore db shell <db>` | Open a psql shell to a database |
| `encore gen client` | Generate API client code |
| `encore app link` | Link to an existing Encore Cloud app |

### Add Path Parameters

```go
type GetUserParams struct {
    ID string
}

type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

//encore:api public method=GET path=/users/:id
func GetUser(ctx context.Context, params *GetUserParams) (*User, error) {
    return &User{ID: params.ID, Name: "John"}, nil
}
```

### Add a Database

```go
// db.go
package hello

import "encore.dev/storage/sqldb"

var db = sqldb.NewDatabase("mydb", sqldb.DatabaseConfig{
    Migrations: "./migrations",
})
```

Create a migration:

```sql
-- hello/migrations/1_create_table.up.sql
CREATE TABLE items (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL
);
```

### Query the Database

```go
import "encore.dev/storage/sqldb"

type Item struct {
    ID   int
    Name string
}

func getItem(ctx context.Context, id int) (*Item, error) {
    item, err := sqldb.QueryRow[Item](ctx, db, `
        SELECT id, name FROM items WHERE id = $1
    `, id)
    if err != nil {
        return nil, err
    }
    return item, nil
}
```

### Next Steps

- Add more endpoints (see `encore-go-api` skill)
- Add authentication (see `encore-go-auth` skill)
- Add Pub/Sub topics (`encore-go-pubsub`), cron jobs (`encore-go-cron`), buckets (`encore-go-bucket`), secrets (`encore-go-secret`), or caching (`encore-go-cache`)
- Deploy to Encore Cloud: `encore app link` then `git push encore`

<!-- chapter:end slug=go-getting-started -->

---

<!-- chapter:begin slug=go-pubsub position=18 -->

## 18. encore-go-pubsub

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-pubsub/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-pubsub/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-pubsub.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-pubsub
description: Asynchronous messaging in Encore Go via `pubsub.NewTopic` and `pubsub.NewSubscription` from `encore.dev/pubsub` — broadcast events, decouple producers from consumers, and run background handlers.
when_to_use: >-
  User wants to publish/broadcast events, fan out a single event to many handlers, fire-and-forget messages between Go services, react to something asynchronously, set up a worker that consumes events, configure delivery guarantees (at-least-once, exactly-once), or use ordering attributes. Trigger phrases: "publish an event", "broadcast", "subscribe to", "topic", "Pub/Sub", "pubsub", "event bus", "OrderCreated event", "send to anyone listening", "background event handler", "queue", "fan out".
---

# Encore Go Pub/Sub

## Instructions

Pub/Sub is for asynchronous messaging between services. Producers publish events to a `Topic`; consumers attach `Subscription`s to react. Resources must be declared as package-level variables — never inside functions.

## Topics

```go
package events

import "encore.dev/pubsub"

type OrderCreatedEvent struct {
    OrderID string `json:"order_id"`
    UserID  string `json:"user_id"`
    Total   int    `json:"total"`
}

// Package level declaration
var OrderCreated = pubsub.NewTopic[*OrderCreatedEvent]("order-created", pubsub.TopicConfig{
    DeliveryGuarantee: pubsub.AtLeastOnce,
})
```

### Publishing

```go
msgID, err := events.OrderCreated.Publish(ctx, &events.OrderCreatedEvent{
    OrderID: "123",
    UserID:  "user-456",
    Total:   9999,
})
```

### Subscriptions

```go
package notifications

import (
    "context"
    "myapp/events"
    "encore.dev/pubsub"
)

var _ = pubsub.NewSubscription(events.OrderCreated, "send-confirmation-email",
    pubsub.SubscriptionConfig[*events.OrderCreatedEvent]{
        Handler: sendConfirmationEmail,
    },
)

func sendConfirmationEmail(ctx context.Context, event *events.OrderCreatedEvent) error {
    // Send email...
    return nil
}
```

### Topic References

Pass topic access to library code while maintaining static analysis:

```go
// Create a reference with publish permission
ref := pubsub.TopicRef[pubsub.Publisher[*OrderCreatedEvent]](OrderCreated)

// Use the reference in library code
func publishEvent(ref pubsub.Publisher[*OrderCreatedEvent], event *OrderCreatedEvent) error {
    _, err := ref.Publish(ctx, event)
    return err
}
```

## Delivery Guarantees

- `pubsub.AtLeastOnce` (default): may deliver duplicates → handlers must be idempotent.
- `pubsub.ExactlyOnce`: stricter, capped throughput (AWS 300 msg/s/topic, GCP 3000+ msg/s/region). Does not deduplicate on the publish side.

## Guidelines

- Topics and subscriptions must be declared as package-level variables.
- Subscription handlers must be idempotent (at-least-once delivery is the default).
- Subscription handlers receive a `context.Context` and the event pointer; return an `error` to retry per the topic's retry policy.
- Don't do heavy synchronous work in `Publish` callers — `Publish` returns once the message is queued.

<!-- chapter:end slug=go-pubsub -->

---

<!-- chapter:begin slug=go-secret position=19 -->

## 19. encore-go-secret

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-secret/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-secret/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-secret.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-secret
description: Manage API keys, credentials, and other secrets in Encore Go using a package-level `secrets` struct.
when_to_use: >-
  User wants to load a private credential into a Go service without committing it to the repo — third-party API keys (Stripe, OpenAI, Twilio, SendGrid), database passwords, signing keys, OAuth client secrets, JWT signing keys, webhook signing secrets. Covers the `var secrets struct{...}` declaration, accessing secrets as struct fields, setting values via `encore secret set`, and `.secrets.local.cue` overrides. Trigger phrases: "API key", "third-party token", "credentials", "without committing", "private key", "signing secret", "secret manager", "encore secret set".
---

# Encore Go Secrets

## Instructions

Secrets are encrypted, environment-scoped values managed by Encore. Declare them as a package-level `secrets` struct — Encore reads the field names and resolves each to the right value at runtime.

```go
package email

var secrets struct {
    SendGridAPIKey string
    SMTPPassword   string
}

func sendEmail() error {
    apiKey := secrets.SendGridAPIKey
    // Use the secret...
    return nil
}
```

Secret keys are globally unique across the application — `SendGridAPIKey` resolves to the same value regardless of which package declares it.

## Setting values

```bash
# Set per environment type
encore secret set --type prod SendGridAPIKey
encore secret set --type dev  SendGridAPIKey
encore secret set --type local SendGridAPIKey
```

Environment types: `production` (alias `prod`), `development` (alias `dev`), `preview` (alias `pr`), `local`.

## Local overrides

For local development without going through `encore secret set`, create a `.secrets.local.cue` file at the repo root (gitignore it):

```cue
SendGridAPIKey: "SG.local-test-key"
GitHubAPIToken: "ghp_local_..."
```

## Common usage patterns

```go
package github

import (
    "context"
    "net/http"
)

var secrets struct {
    GitHubAPIToken string
}

func callGitHub(ctx context.Context) error {
    req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user", nil)
    req.Header.Set("Authorization", "token "+secrets.GitHubAPIToken)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    return nil
}
```

```go
package webhooks

var secrets struct {
    StripeWebhookSecret string
}
// Verify Stripe signature using secrets.StripeWebhookSecret in a raw endpoint.
```

## Guidelines

- Declare secrets as a package-level `secrets` struct, not as individual `secret(...)` calls.
- Field names must exactly match the secret name set via `encore secret set`.
- Set distinct values per environment via `encore secret set --type <env>`.
- Never commit secret values; use `.secrets.local.cue` for local overrides and gitignore it.
- For webhook signature secrets specifically, see also the `encore-go-webhook` skill.

<!-- chapter:end slug=go-secret -->

---

<!-- chapter:begin slug=go-service position=20 -->

## 20. encore-go-service

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-service/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-service/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-service.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-service
description: Plan how to split an Encore Go application into services and lay out its directory structure. Architecture and decomposition, not first-time CLI install (that's `encore-go-getting-started`).
when_to_use: >-
  User is deciding monolith vs. microservices in Go, weighing "one service or several", drawing service boundaries, planning a multi-service system (e.g. orders + payments + inventory + shipping), creating a Go package as an Encore service, naming directories/folders, designing systems-of-services hierarchies, or asking for a Go project layout recommendation. Trigger phrases: "lay out the directories", "directory structure", "service boundaries", "one service or several", "monolith vs microservices", "where to put", "systems of services", "Go package layout".
---

# Encore Go Service Structure

## Instructions

In Encore Go, **each package with an API endpoint is automatically a service**. No special configuration needed.

### Creating a Service

Simply create a package with at least one `//encore:api` endpoint:

```go
// user/user.go
package user

import "context"

type User struct {
    ID    string `json:"id"`
    Email string `json:"email"`
    Name  string `json:"name"`
}

//encore:api public method=GET path=/users/:id
func GetUser(ctx context.Context, params *GetUserParams) (*User, error) {
    // This makes "user" a service
}
```

### Minimal Service Structure

```
user/
├── user.go          # API endpoints
├── db.go            # Database (if needed)
└── migrations/      # SQL migrations
    └── 1_create_users.up.sql
```

## Application Patterns

### Single Service (Recommended Start)

Best for new projects - start simple, split later if needed:

```
my-app/
├── encore.app
├── go.mod
├── api.go           # All endpoints
├── db.go            # Database
└── migrations/
    └── 1_initial.up.sql
```

### Multi-Service

For distributed systems with clear domain boundaries:

```
my-app/
├── encore.app
├── go.mod
├── user/
│   ├── user.go
│   ├── db.go
│   └── migrations/
├── order/
│   ├── order.go
│   ├── db.go
│   └── migrations/
└── notification/
    └── notification.go
```

### Large Application (System-based)

Group related services into systems:

```
my-app/
├── encore.app
├── go.mod
├── commerce/
│   ├── order/
│   │   └── order.go
│   ├── cart/
│   │   └── cart.go
│   └── payment/
│       └── payment.go
├── identity/
│   ├── user/
│   │   └── user.go
│   └── auth/
│       └── auth.go
└── comms/
    ├── email/
    │   └── email.go
    └── push/
        └── push.go
```

## Service-to-Service Calls

Just import and call the function directly - Encore handles the RPC:

```go
package order

import (
    "context"
    "myapp/user"  // Import the user service
)

//encore:api auth method=GET path=/orders/:id
func GetOrderWithUser(ctx context.Context, params *GetOrderParams) (*OrderWithUser, error) {
    order, err := getOrder(ctx, params.ID)
    if err != nil {
        return nil, err
    }
    
    // This becomes an RPC call - Encore handles it
    orderUser, err := user.GetUser(ctx, &user.GetUserParams{ID: order.UserID})
    if err != nil {
        return nil, err
    }
    
    return &OrderWithUser{Order: order, User: orderUser}, nil
}
```

## When to Split Services

Split when you have:

| Signal | Action |
|--------|--------|
| Different scaling needs | Split (e.g., auth vs analytics) |
| Different deployment cycles | Split |
| Clear domain boundaries | Split |
| Shared database tables | Keep together |
| Tightly coupled logic | Keep together |
| Just organizing code | Use sub-packages, not services |

## Internal Helpers (Non-Service Packages)

Create packages without `//encore:api` endpoints for shared code:

```
my-app/
├── user/
│   └── user.go       # Service (has API)
├── order/
│   └── order.go      # Service (has API)
└── internal/
    ├── util/
    │   └── util.go   # Not a service (no API)
    └── validation/
        └── validate.go
```

## Guidelines

- A package becomes a service when it has `//encore:api` endpoints
- Services cannot be nested within other services
- Start with one service, split when there's a clear reason
- Cross-service calls look like regular function calls
- Each service can have its own database
- Package names should be lowercase, descriptive
- Don't create services just for code organization - use sub-packages instead

<!-- chapter:end slug=go-service -->

---

<!-- chapter:begin slug=go-testing position=21 -->

## 21. encore-go-testing

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-testing/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-testing/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-testing.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-testing
description: Write or run automated tests for Encore Go code with `encore test` and the standard library `testing` package. Covers isolated per-test databases, calling handlers directly, and `*testing.T` patterns.
when_to_use: >-
  User wants to add/write/fix a test in Go, write a `*_test.go` file, test an endpoint or service, set up `encore test`, configure isolated test databases, write `t.Cleanup`/`t.Helper` for db cleanup, mock external dependencies, or assert on API request/response behaviour. Trigger phrases: "write a Go test", "add tests for", "go test", "encore test", "test the endpoint", "test the service", "integration test", "isolated database".
---

# Testing Encore Go Applications

## Instructions

Encore Go uses standard Go testing with `encore test`.

### Run Tests

```bash
# Run all tests with Encore (recommended)
encore test ./...

# Run tests for a specific package
encore test ./user/...

# Run with verbose output
encore test -v ./...
```

Using `encore test` instead of `go test` is recommended because it:
- Sets up test databases automatically
- Provides isolated infrastructure per test
- Handles service dependencies

### Test an API Endpoint

```go
// hello/hello_test.go
package hello

import (
    "context"
    "testing"
)

func TestHello(t *testing.T) {
    ctx := context.Background()
    
    resp, err := Hello(ctx)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    
    if resp.Message != "Hello, World!" {
        t.Errorf("expected 'Hello, World!', got '%s'", resp.Message)
    }
}
```

### Test with Request Parameters

```go
// user/user_test.go
package user

import (
    "context"
    "testing"
)

func TestGetUser(t *testing.T) {
    ctx := context.Background()
    
    user, err := GetUser(ctx, &GetUserParams{ID: "123"})
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    
    if user.ID != "123" {
        t.Errorf("expected ID '123', got '%s'", user.ID)
    }
}
```

### Test Database Operations

Encore provides isolated test databases:

```go
// user/user_test.go
package user

import (
    "context"
    "testing"
    
    "encore.dev/storage/sqldb"
)

func TestCreateUser(t *testing.T) {
    ctx := context.Background()
    
    // Clean up
    _, _ = sqldb.Exec(ctx, db, "DELETE FROM users")
    
    // Create user
    created, err := CreateUser(ctx, &CreateUserParams{
        Email: "test@example.com",
        Name:  "Test User",
    })
    if err != nil {
        t.Fatalf("failed to create user: %v", err)
    }
    
    // Retrieve and verify
    retrieved, err := GetUser(ctx, &GetUserParams{ID: created.ID})
    if err != nil {
        t.Fatalf("failed to get user: %v", err)
    }
    
    if retrieved.Email != "test@example.com" {
        t.Errorf("expected email 'test@example.com', got '%s'", retrieved.Email)
    }
}
```

### Test Service-to-Service Calls

```go
// order/order_test.go
package order

import (
    "context"
    "testing"
)

func TestCreateOrder(t *testing.T) {
    ctx := context.Background()
    
    // Service calls work normally in tests
    order, err := CreateOrder(ctx, &CreateOrderParams{
        UserID: "user-123",
        Items: []OrderItem{
            {ProductID: "prod-1", Quantity: 2},
        },
    })
    if err != nil {
        t.Fatalf("failed to create order: %v", err)
    }
    
    if order.Status != "pending" {
        t.Errorf("expected status 'pending', got '%s'", order.Status)
    }
}
```

### Test Error Cases

```go
package user

import (
    "context"
    "errors"
    "testing"
    
    "encore.dev/beta/errs"
)

func TestGetUser_NotFound(t *testing.T) {
    ctx := context.Background()
    
    _, err := GetUser(ctx, &GetUserParams{ID: "nonexistent"})
    if err == nil {
        t.Fatal("expected error, got nil")
    }
    
    // Check error code
    var e *errs.Error
    if errors.As(err, &e) {
        if e.Code != errs.NotFound {
            t.Errorf("expected NotFound, got %v", e.Code)
        }
    } else {
        t.Errorf("expected errs.Error, got %T", err)
    }
}
```

### Test Pub/Sub

```go
// notifications/notifications_test.go
package notifications

import (
    "context"
    "testing"
    
    "myapp/events"
)

func TestPublishOrderCreated(t *testing.T) {
    ctx := context.Background()
    
    msgID, err := events.OrderCreated.Publish(ctx, &events.OrderCreatedEvent{
        OrderID: "order-123",
        UserID:  "user-456",
        Total:   9999,
    })
    if err != nil {
        t.Fatalf("failed to publish: %v", err)
    }
    
    if msgID == "" {
        t.Error("expected message ID, got empty string")
    }
}
```

### Test Cron Jobs

Test the underlying function, not the cron schedule:

```go
// cleanup/cleanup_test.go
package cleanup

import (
    "context"
    "testing"
)

func TestCleanupExpiredSessions(t *testing.T) {
    ctx := context.Background()
    
    // Create some expired sessions first
    createExpiredSession(ctx)
    
    // Call the endpoint directly
    err := CleanupExpiredSessions(ctx)
    if err != nil {
        t.Fatalf("cleanup failed: %v", err)
    }
    
    // Verify cleanup happened
    count := countSessions(ctx)
    if count != 0 {
        t.Errorf("expected 0 sessions, got %d", count)
    }
}
```

### Table-Driven Tests

```go
func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        wantErr bool
    }{
        {"valid email", "user@example.com", false},
        {"missing @", "userexample.com", true},
        {"empty", "", true},
        {"valid with subdomain", "user@mail.example.com", false},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := validateEmail(tt.email)
            if (err != nil) != tt.wantErr {
                t.Errorf("validateEmail(%q) error = %v, wantErr %v", tt.email, err, tt.wantErr)
            }
        })
    }
}
```

### Test with Subtests

```go
func TestUserCRUD(t *testing.T) {
    ctx := context.Background()
    var userID string
    
    t.Run("create", func(t *testing.T) {
        user, err := CreateUser(ctx, &CreateUserParams{
            Email: "test@example.com",
            Name:  "Test",
        })
        if err != nil {
            t.Fatalf("create failed: %v", err)
        }
        userID = user.ID
    })
    
    t.Run("read", func(t *testing.T) {
        user, err := GetUser(ctx, &GetUserParams{ID: userID})
        if err != nil {
            t.Fatalf("read failed: %v", err)
        }
        if user.Email != "test@example.com" {
            t.Errorf("wrong email: %s", user.Email)
        }
    })
    
    t.Run("delete", func(t *testing.T) {
        err := DeleteUser(ctx, &DeleteUserParams{ID: userID})
        if err != nil {
            t.Fatalf("delete failed: %v", err)
        }
    })
}
```

### Test Database Isolation

Create isolated, fully-migrated test databases using `et.NewTestDatabase()`:

```go
import "encore.dev/et"

func TestWithFreshDatabase(t *testing.T) {
    // Creates a new database with all migrations applied
    testDB := et.NewTestDatabase(t, db)

    // Use testDB for queries - it's completely isolated
    _, err := testDB.Exec(ctx, "INSERT INTO users (email) VALUES ($1)", "test@example.com")
    if err != nil {
        t.Fatal(err)
    }
}
```

### Service Instance Isolation

By default, service structs are shared across tests for performance. Enable isolation when tests modify service state:

```go
import "encore.dev/et"

func TestWithServiceIsolation(t *testing.T) {
    // Enable service instance isolation for this test
    et.EnableServiceInstanceIsolation()

    // Now this test gets its own service struct instance
    // preventing state interference with other tests
}
```

### Test Tracing Dashboard

View test execution traces in the development dashboard at `http://localhost:9400` while tests run. This helps diagnose failures by showing:
- Request/response data
- Database queries
- Service-to-service calls
- Errors and stack traces

### Mocking Endpoints and Services

Mock endpoints or entire services for isolated unit testing:

```go
import "encore.dev/et"

func TestWithMockedEndpoint(t *testing.T) {
    // Mock a specific endpoint
    et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {
        return &products.PriceResponse{Price: 100}, nil
    })

    // Mock an entire service
    et.MockService("products", &mockProductService{})
}
```

### Guidelines

- Use `encore test` to run tests with infrastructure setup
- Each test gets access to real infrastructure (databases, Pub/Sub)
- Test API endpoints by calling them directly as functions
- Service-to-service calls work normally in tests
- Use table-driven tests for testing multiple cases
- Use `et.NewTestDatabase()` for isolated database testing
- Use `et.EnableServiceInstanceIsolation()` when tests modify service state
- Don't mock Encore infrastructure - use the real thing
- Mock external dependencies (third-party APIs, email services, etc.)

<!-- chapter:end slug=go-testing -->

---

<!-- chapter:begin slug=go-webhook position=22 -->

## 22. encore-go-webhook

- **Source:** https://github.com/encoredev/skills/blob/main/encore/go-webhook/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/go-webhook/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/go-webhook.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-go-webhook
description: Receive inbound webhooks from external services (Stripe, GitHub, Slack, Twilio, etc.) in Encore Go using `//encore:api raw`. The right skill any time the user names a third-party provider that POSTs events to a URL you own.
when_to_use: >-
  User mentions a webhook, a /webhooks/* path, raw HTTP, `//encore:api raw`, accepting external callbacks, verifying webhook signatures (Stripe-Signature, X-Hub-Signature-256), reading the raw request body, parsing form-encoded payloads, or any time the user names a third-party provider that posts events — Stripe, GitHub, GitLab, Bitbucket, Shopify, Twilio, SendGrid, Mailgun, Auth0, Clerk, Slack, Discord, PayPal, Square. Use `encore-go-api` instead for typed JSON endpoints in your own service. Trigger phrases: "Stripe webhook", "GitHub webhook", "/webhooks/stripe", "raw HTTP endpoint", "raw endpoint", "verify the signature", "inbound webhook", "external callback".
---

# Encore Go Webhook Endpoints

## Instructions

Use `//encore:api raw` to receive inbound webhooks from third-party services. Raw endpoints give you direct access to `http.ResponseWriter` and `*http.Request`, which you need for signature verification (the verification typically requires the unparsed raw body).

### 1. Define the endpoint with `//encore:api raw`

```go
package webhooks

import (
    "io"
    "net/http"
)

//encore:api public raw path=/webhooks/stripe method=POST
func StripeWebhook(w http.ResponseWriter, req *http.Request) {
    sig := req.Header.Get("Stripe-Signature")

    // Read the raw body — needed for signature verification.
    body, err := io.ReadAll(req.Body)
    if err != nil {
        http.Error(w, "could not read body", http.StatusBadRequest)
        return
    }

    // Verify signature, parse event, handle, then respond...

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"received":true}`))
}
```

### 2. Verify the signature

Most providers sign webhooks. Read the secret with `secrets` (see the `encore-go-secret` skill) and verify before trusting the payload.

```go
package webhooks

import (
    "github.com/stripe/stripe-go/v76/webhook"
)

var secrets struct {
    StripeWebhookSecret string
}

//encore:api public raw path=/webhooks/stripe method=POST
func StripeWebhook(w http.ResponseWriter, req *http.Request) {
    body, _ := io.ReadAll(req.Body)
    sig := req.Header.Get("Stripe-Signature")

    event, err := webhook.ConstructEvent(body, sig, secrets.StripeWebhookSecret)
    if err != nil {
        http.Error(w, "signature verification failed", http.StatusBadRequest)
        return
    }

    // event is now trusted — handle it.
    _ = event
    w.WriteHeader(http.StatusOK)
}
```

For GitHub, verify the HMAC-SHA256 in the `X-Hub-Signature-256` header against the raw body using your webhook secret.

## Common providers

| Provider | Signature header | Verification |
|---|---|---|
| Stripe | `Stripe-Signature` | `stripe.webhook.ConstructEvent(rawBody, sig, secret)` |
| GitHub | `X-Hub-Signature-256` | HMAC-SHA256 over the raw body |
| Slack | `X-Slack-Signature` | HMAC-SHA256 over `v0:{timestamp}:{rawBody}` |
| Shopify | `X-Shopify-Hmac-Sha256` | HMAC-SHA256 (base64) over the raw body |
| Twilio | `X-Twilio-Signature` | HMAC-SHA1 over URL + sorted form fields |

## Always respond quickly

Webhook senders retry on non-2xx or slow responses. Acknowledge with a 2xx as soon as the payload is verified, then enqueue the actual work via Pub/Sub (see `encore-go-pubsub`) instead of doing it in the request handler.

```go
package webhooks

import (
    "encore.dev/pubsub"
    "net/http"
)

type StripeEvent struct {
    ID   string `json:"id"`
    Type string `json:"type"`
    Data any    `json:"data"`
}

var StripeEvents = pubsub.NewTopic[*StripeEvent]("stripe-events", pubsub.TopicConfig{
    DeliveryGuarantee: pubsub.AtLeastOnce,
})

//encore:api public raw path=/webhooks/stripe method=POST
func StripeWebhook(w http.ResponseWriter, req *http.Request) {
    // ... verify signature, parse event ...

    _, _ = StripeEvents.Publish(req.Context(), &StripeEvent{
        ID: event.ID, Type: string(event.Type), Data: event.Data,
    })
    w.WriteHeader(http.StatusOK)
}
```

## Guidelines

- Use `//encore:api raw` *only* for webhooks and other low-level HTTP integrations.
- Always verify the provider's signature before trusting the payload.
- Always respond 2xx fast — push slow work onto Pub/Sub.
- Store the signing secret in `secrets struct{...}`; never inline it.
- For typed JSON endpoints in your own service, use plain `//encore:api` from the `encore-go-api` skill.

<!-- chapter:end slug=go-webhook -->

---

<!-- chapter:begin slug=migrate position=23 -->

## 23. encore-migrate

- **Source:** https://github.com/encoredev/skills/blob/main/encore/migrate/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/migrate/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/migrate.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-migrate
description: Migrate an existing backend application to Encore. Supports any source framework, targets Encore.ts or Encore Go. Drives a structured DISCOVER → PLAN → MIGRATE workflow with `migration-plan.md` tracking.
when_to_use: >-
  User wants to convert an existing app from Express, Fastify, Hono, Koa, NestJS, Restify, gin, Echo, Chi, Fiber, FastAPI, Flask, Django, Rails, Spring Boot, or vanilla Node.js / Go to Encore. Trigger phrases: "convert from Express", "port to Encore", "migrate this Fastify app", "rewrite my Hono backend", "switch from FastAPI to Encore", "I have an existing X app I'd like to convert".
---

# Migrate to Encore

This skill guides migrating any existing backend application to Encore, one migration unit at a time. It supports any source language or framework and targets both Encore.ts and Encore Go. A `migration-plan.md` summary file and `migration-plan/` directory of per-unit detail files are created at the Encore project root to track progress across sessions. This skill contains no Encore code examples — it delegates all Encore-specific implementation to the appropriate language-specific skills.

## Phase Detection

Before doing anything, determine which phase to enter:

- **No `migration-plan.md` exists** in the Encore project directory → Start at **Phase 1: DISCOVER**
- **`migration-plan.md` exists but no `migration-plan/` directory** → Resume at **Phase 2: PLAN** (discovery done, detail files not yet written)
- **`migration-plan/` directory exists with pending units** (any unit in the summary with status `pending` or `in progress`) → Resume at **Phase 3: MIGRATE**
- **All units in the summary are `migrated`, `skipped`, or `manual validation needed`** → Go to **Phase 4: COMPLETE**

### Resuming a Migration (Phase 3)

When `migration-plan.md` and `migration-plan/` exist with pending units:

1. Read `migration-plan.md` (summary only — do NOT read all detail files)
2. Report current status to the user — for example: "3 of 7 units migrated, next suggested: billing (all its dependencies are migrated)"
3. Ask the user what they would like to work on next, offering a suggestion based on the dependency order in the plan

## Phase 1 — Discover

### 1. Gather Information

Ask the user for:

- **Path to the source system** (the existing codebase being migrated)
- **Local URL where the source system runs** (if applicable — needed for HTTP comparison validation later)
- **Target language:** Encore.ts or Encore Go

### 2. Analyze the Source Codebase

Read the source codebase and inventory all entities:

| Category | What to look for |
|----------|-----------------|
| Services / modules / domains | Distinct bounded contexts, separate deployable units, route groupings |
| API endpoints | Method, path, handler function, request/response shapes |
| Databases | Type (Postgres, MySQL, etc.), tables, schemas, migration files |
| Pub/Sub topics and subscriptions | Topic names, publishers, subscribers, message shapes |
| Cron jobs / scheduled tasks | Schedule expressions, handler functions |
| Auth middleware / handlers | Authentication strategies, token validation, session management |
| Secrets / environment variables | All referenced env vars and secrets, noting which are sensitive |
| Existing tests | Test files, which entities they cover, test framework used |
| Frontend code | React/Vue/Angular components, static HTML, CSS, client-side JS — these are out of scope |

### 3. Identify Frontend Code

Full-stack repos and monorepos often mix backend and frontend code. The migration targets backend only — frontend code is out of scope.

**Detect frontend directories and mark them as out of scope.** Common indicators:

| Pattern | Examples |
|---------|----------|
| Dedicated frontend directories | `frontend/`, `client/`, `web/`, `app/` (when it contains React/Vue/Angular), `src/components/`, `public/` |
| Frontend config files | `next.config.js`, `vite.config.ts`, `nuxt.config.ts`, `angular.json`, `.svelte-kit/`, `remix.config.js` |
| Package dependencies | `react`, `vue`, `@angular/core`, `svelte` in `package.json` |

**Flag framework server-side code that *should* be migrated.** Some frontend frameworks embed backend logic that contains API endpoints, database queries, or server-side business logic:

| Framework | Server-side locations | What to look for |
|-----------|----------------------|-----------------|
| Next.js | `pages/api/`, `app/*/route.ts` | API route handlers — these are backend endpoints |
| Remix | `app/routes/*.tsx` (loader/action exports) | `loader` and `action` functions contain server logic |
| Nuxt | `server/api/`, `server/routes/` | Server API routes |
| SvelteKit | `src/routes/+server.ts`, `+page.server.ts` | Server endpoints and load functions |
| Astro | `src/pages/*.ts` (non-`.astro`) | API endpoints |

When framework server-side code is found, **ask the user what to do with it.** Not all server-side code should move to Encore — sometimes a thin backend layer (BFF, auth proxy, SSR data fetching) should stay in the frontend framework alongside an Encore backend.

Present the user with what was found and ask:

> "I found <N> server-side routes in your <framework> app (e.g., `pages/api/users.ts`, `app/billing/route.ts`). These contain backend logic that *could* be migrated to Encore, but some teams prefer to keep a thin server layer in their frontend framework for things like SSR data fetching or BFF proxying. Would you like to:
> 1. **Migrate all** server-side routes to Encore
> 2. **Migrate some** — I'll list them and you pick which ones move
> 3. **Keep all in <framework>** — only migrate the standalone backend code"

Based on the user's choice:

- **Migrate all:** Extract the backend logic into migration units. Leave frontend rendering code out of scope. Note in the migration plan which source files contain mixed frontend/backend code.
- **Migrate some:** Present the list of server-side routes and let the user select. Include selected routes in migration units, mark the rest as out of scope.
- **Keep all:** Mark all framework server-side code as out of scope alongside the frontend. Only standalone backend code (Express routes, standalone API servers, etc.) enters migration units.

**Report to the user:** List all detected frontend directories and the decision made about framework server-side code. Example: "I found a Next.js frontend in `app/` — the React components are out of scope. You chose to migrate 8 of the 12 API routes from `pages/api/` to Encore and keep 4 thin proxy routes in Next.js."

### 4. Group Entities into Migration Units

Group the discovered entities into migration units using these heuristics in priority order:

1. **Existing service boundaries** — If the source app already has services, modules, or packages, use those as the starting point for chunks
2. **URL path prefixes** — Group endpoints sharing a path prefix (e.g., `/users/*`, `/billing/*`)
3. **Shared database tables** — Endpoints that read/write the same tables belong together
4. **Shared types/models** — Endpoints that share request/response types or domain models

**Chunk sizing:** Aim for 5-15 endpoints per migration unit. If a group exceeds ~15 endpoints, suggest splitting it further (e.g., `users-crud` and `users-admin`). If a group has fewer than 3 endpoints, consider merging it with a related chunk.

**Cross-cutting concerns** get their own migration units: auth, secrets, and standalone infrastructure (pub/sub topics, cron jobs not tightly coupled to one service) are separate units since they follow different dependency tiers.

**For monoliths with no clear boundaries:** Fall back to URL path prefix grouping, then ask: "These groupings are based on URL paths — would you like to reorganize them by domain?"

### 5. Present the Migration Units

Present the migration units to the user as a summary table:

| Unit | Endpoints | DB Tables | Other | Complexity |
|------|-----------|-----------|-------|------------|

Include total counts (e.g., "7 migration units covering 42 endpoints, 3 databases"). For each unit, assess overall migration complexity:

- **Low** — direct Encore equivalents exist, straightforward mapping
- **Medium** — requires restructuring or has partial Encore equivalents
- **High** — no direct equivalent, needs redesign or custom solution

Offer to show the detail of any unit if the user wants to inspect what's inside before confirming.

### 6. Show Code Previews

For 2-3 representative entities (pick a mix of simple and complex from different units), show a short "before and after" preview of what the source code looks like now and what the Encore version will look like. Use the appropriate language-specific skill to inform the preview. Keep previews brief — one endpoint, one query, or one topic declaration is enough per preview.

### 7. Confirm with the User

Ask the user to confirm the migration units are correct. Specifically ask:

- "Are there any services, endpoints, or other entities I missed?"
- "Would you like to split, merge, or rename any of these migration units?"
- "Is there anything you want to exclude from the migration?"

### 8. Iterate if Needed

If the user identifies missing entities or wants to adjust chunk boundaries, update the units and re-present the summary table. Repeat until the user confirms the migration units are accurate.

## Phase 2 — Plan

### 1. Check for Existing Encore Project

Check if an Encore project already exists at the target path (look for `encore.app` file). If yes, confirm with the user that this is the correct project. If no, help create one by invoking the `encore-getting-started` skill (or `encore-go-getting-started` for Go).

### 2. Gather Target Information

Ask the user for:

- **Path to the Encore project** (where the migrated code will live)
- **Local URL where the Encore app will run** (default: `http://localhost:4000`)

### 3. Determine Dependency Order

Order migration units based on dependencies. Follow this tier order:

1. **Secrets / config** — no dependencies, needed by everything
2. **Databases** — schema and migrations must exist before services can use them
3. **Auth** — auth handlers are needed before protected endpoints
4. **Leaf units** — units with no cross-service dependencies
5. **Dependent units** — units that depend on already-migrated units
6. **Pub/Sub topics and subscriptions** — often depend on services being in place
7. **Cron jobs** — typically depend on service endpoints

Within each tier, suggest the simplest unit first (fewest endpoints, smallest schema, least complexity).

### 4. Write migration-plan.md (Summary)

Write the `migration-plan.md` summary file to the Encore project root using the template in the "migration-plan.md Format" section below. Fill in all migration units with status `pending`.

### 5. Write Detail Files

Create a `migration-plan/` directory at the Encore project root. Write one detail file per migration unit using the template in the "Detail File Format" section below. Each file is named `migration-plan/<unit-name>.md`.

### 6. Propose First Unit

Propose the first migration unit, explaining why it should go first based on the dependency order. Wait for user approval before proceeding to Phase 3.

## Phase 3 — Migrate (Loop)

### 1. Identify Next Unit

Read `migration-plan.md` (summary only) and identify the next pending migration unit based on the dependency order.

### 2. Suggest and Confirm

Suggest the next unit to migrate and explain why this one is next (e.g., "This unit has no dependencies on unmigrated units" or "The database must exist before we can migrate the service that uses it"). Ask the user if they want to proceed with this unit or pick a different one.

### 3. Load the Unit Detail

Read the detail file for the chosen unit (`migration-plan/<unit-name>.md`). Do NOT read detail files for other units.

### 4. Migrate Each Entity

For each entity in the unit:

#### a. Implement

Invoke the appropriate language-specific skill based on the entity type and target language:

| Migrating... | Encore.ts skill | Encore Go skill |
|---|---|---|
| Service structure | `encore-service` | `encore-go-service` |
| API endpoints | `encore-api` | `encore-go-api` |
| Auth | `encore-auth` | `encore-go-auth` |
| Database + migrations | `encore-database` | `encore-go-database` |
| Pub/Sub topics & subscriptions | `encore-pubsub` | `encore-go-pubsub` |
| Cron jobs / scheduled tasks | `encore-cron` | `encore-go-cron` |
| Object storage / file uploads | `encore-bucket` | `encore-go-bucket` |
| Caching (Redis) | `encore-cache` | `encore-go-cache` |
| Secrets / API keys / credentials | `encore-secret` | `encore-go-secret` |
| Webhooks (Stripe, GitHub, etc.) | `encore-webhook` | `encore-go-webhook` |
| Tests | `encore-testing` | `encore-go-testing` |

#### b. Migrate Tests

If the source entity has associated tests, migrate them using the appropriate testing skill (`encore-testing` or `encore-go-testing`). Adapt test assertions to match Encore API patterns. If the source entity has no tests, note this in the detail file.

#### c. Validate

Three validation layers are applied to each entity before it can be marked as `migrated`. Every entity must go through all applicable layers.

##### Layer 1: Test Migration (Primary)

- When migrating an entity, also migrate its associated tests
- Use the `encore-testing` skill (or `encore-go-testing` for Go) to implement the tests
- Run the tests — they must pass before the entity can be marked as `migrated`
- If the source entity had no tests, note "no source tests" in the plan and rely on the other layers

##### Layer 2: HTTP Comparison (Endpoints Only, Best-Effort)

When both systems are running locally, call the same endpoint on both the source system and the Encore app, then compare:

- **HTTP status code** — must match
- **Response body structure** — keys and shape must match (values may differ for dynamic data like timestamps or IDs)

**Skip this layer when:**

- The endpoint requires auth credentials the agent cannot obtain (ask the user — allow skip)

**If a request to either system fails to connect**, ask the user to start the app before retrying. Do not silently skip — the user may have simply forgotten to start it.

**Always ask the user before making any HTTP call that could have side effects.**

##### Layer 3: Verification-Before-Completion Gate

Before marking ANY entity as `migrated`, the agent MUST have fresh evidence from the current session:

- Test command output showing pass count and exit code, OR
- HTTP comparison results showing a match, OR
- Explicit user approval to skip validation

**Rules:**

- No "should work", "looks correct", or "seems fine" — only evidence-backed claims
- The agent must state exactly what it verified and what the output was
- If evidence is insufficient, mark the entity as `manual validation needed`, not `migrated`
- Stale evidence from a previous session does not count — re-run validation if resuming

#### d. Update the Detail File

Update the entity's status in the unit's detail file (`migration-plan/<unit-name>.md`) and record validation evidence in that file's Validation Log table.

#### e. Update the Summary

When all entities in a unit are complete, update the unit's status in `migration-plan.md` to `migrated`. If some entities are pending, set the unit status to `in progress`.

### 5. Continue or Pause

After completing a unit, ask "What would you like to migrate next?" and suggest the next unit based on dependency order.

### 6. Batching

The default is one unit at a time. If the user says "keep going", "do them all", or similar, batch multiple units but still validate each entity individually before marking it as migrated.

## Phase 4 — Complete

When all units in `migration-plan.md` are `migrated`, `skipped`, or `manual validation needed`:

1. **Present a final summary** from `migration-plan.md`:
   - Total units migrated
   - Units marked as `manual validation needed` — read those specific detail files and list the entities that need attention with reasons
   - Units skipped (list them with reasons)
2. **Suggest running the full test suite** one final time to catch any integration issues
3. **Note any manual validation items** that still need human attention
4. **If the source system had frontend code**, suggest using the `encore-frontend` skill to reconnect the frontend to the new Encore backend (generate a typed API client, configure CORS, update base URLs)
5. **Suggest removing `migration-plan.md` and `migration-plan/`** from the project once the user is satisfied with the migration

## Asking Questions

Ask the user before acting when:

- **Service boundaries are unclear** — e.g., "These route files could be 1 service or 3 — how would you like to split them?"
- **No clean Encore equivalent exists** — e.g., Redis caching layer, custom middleware chains, WebSocket handlers
- **Multiple valid migration strategies exist** — present the options with tradeoffs
- **Before making any HTTP call that could have side effects** — always ask first
- **Source code is ambiguous** — when the agent is not confident about what the code does, ask rather than guess
- **Source system appears to have changed** — if files referenced in a detail file no longer exist or have changed significantly

## Source System Protection

The source system must never be modified during migration. Follow these rules:

- **Never modify source files** — read them, don't edit them
- **Never delete source files** — even after migration is complete
- **Never write to the source directory** — all output goes to the Encore project
- **Never run destructive commands against the source system** (drop tables, delete queues, etc.)
- **Ask before any HTTP call that mutates state** on the source system (POST, PUT, DELETE)

If the user asks to "clean up" or "remove" the old system, confirm explicitly before taking any action. The source system may still be serving production traffic.

## Edge Cases

### Moving Endpoints Between Units

If the user realizes an endpoint belongs in a different migration unit:

1. Remove the endpoint row from the source unit's detail file
2. Add it to the target unit's detail file
3. Update endpoint counts in `migration-plan.md`

### Splitting a Unit Mid-Migration

If a unit turns out to be too large while working on it:

1. Create a new detail file for the split-off portion (`migration-plan/<new-unit>.md`)
2. Move pending entities to the new file (already-migrated entities stay in the original)
3. Add the new unit to the `migration-plan.md` summary table
4. Insert it in the dependency order (same tier, after the original)

### Monolith to Multiple Encore Services

A single migration unit might map to multiple Encore services. The detail file tracks the source grouping, but the "Notes" column can indicate the target Encore service. Ask during migration if the unit maps to one service or should be split across Encore services.

### Source Code Changed Since Discovery

If files referenced in a detail file no longer exist or have changed significantly since discovery:

1. Flag the discrepancy to the user
2. Ask whether to update the detail file with the new state or skip the affected entities
3. If updating, re-assess complexity and adjust the plan accordingly

## Troubleshooting

Common issues during migration and how to resolve them:

| Problem | Cause | Resolution |
|---------|-------|------------|
| Import errors across services | Direct imports between services | Use `~encore/clients` (TS) or service client packages (Go) instead |
| Database migration fails | Incompatible SQL syntax | Check Encore uses PostgreSQL — adapt MySQL/SQLite syntax |
| Pub/Sub messages not received | Subscription not registered | Ensure subscription is declared at package level, not inside a function |
| Cron job not firing | Invalid schedule expression | Encore uses standard cron expressions — verify syntax |
| `encore run` errors on infrastructure | Infrastructure declared inside functions | Move all infrastructure declarations to package level |
| Source and Encore responses differ | Missing business logic or different error handling | Compare response shapes carefully, check edge cases |
| Cannot validate endpoint | Auth required or side effects | Ask user for test credentials, or mark as `manual validation needed` |

## migration-plan.md Format

Use this exact template for the summary plan file. Fill in values from the discovery phase.

```markdown
# Migration Plan

## Source System
- **Path:** <source system path>
- **URL:** <source system local URL>
- **Framework:** <detected framework>
- **Language:** <detected language>

## Frontend (Out of Scope)
- **Detected:** <Yes/No>
- **Directories:** <list of frontend directories, or "None">
- **Framework:** <frontend framework if detected, or "N/A">
- **Note:** <any framework server-side code that WAS included in migration units>

## Target System
- **Path:** <encore project path>
- **URL:** <encore local URL>
- **Type:** Encore.ts | Encore Go

## Migration Units

| Unit | Endpoints | DB Tables | Other | Complexity | Status |
|------|-----------|-----------|-------|------------|--------|

## Dependency Order
1. <ordered list of migration units>
```

**Status values:** `pending`, `in progress`, `migrated`, `skipped`, `manual validation needed`

**Complexity values:** `Low` (direct equivalent), `Medium` (requires restructuring), `High` (needs redesign)

## Detail File Format

Create one file per migration unit at `migration-plan/<unit-name>.md`. Use this exact template:

```markdown
# Migration Unit: <unit-name>

## Source
- **Files:** <list of source files in this unit>
- **Depends on:** <other migration units, with their current status>

## Endpoints
| Endpoint | Method | Path | Status | Notes |
|----------|--------|------|--------|-------|

## Database
| Table | Complexity | Status | Notes |
|-------|------------|--------|-------|

## Tests
- **Source tests:** <test files and count>
- **Migrated:** <count of migrated tests>

## Validation Log
| Entity | Tests | HTTP Match | Evidence | Status |
|--------|-------|------------|----------|--------|
```

Not all sections are required — omit sections that don't apply to a given unit (e.g., a secrets unit won't have Endpoints or Database sections).

<!-- chapter:end slug=migrate -->

---

<!-- chapter:begin slug=pubsub position=24 -->

## 24. encore-pubsub

- **Source:** https://github.com/encoredev/skills/blob/main/encore/pubsub/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/pubsub/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/pubsub.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-pubsub
description: Asynchronous messaging in Encore.ts via `Topic` and `Subscription` from `encore.dev/pubsub` — broadcast events, decouple producers from consumers, and run background handlers.
when_to_use: >-
  User wants to publish/broadcast events, fan out a single event to many handlers, fire-and-forget messages between services, react to something asynchronously, set up a worker that consumes events, configure delivery guarantees (at-least-once, exactly-once), or use ordering attributes. Trigger phrases: "publish an event", "broadcast", "subscribe to", "topic", "Pub/Sub", "pubsub", "event bus", "order_created event", "send to anyone listening", "background event handler", "queue", "fan out".
---

# Encore Pub/Sub

## Instructions

Pub/Sub is for asynchronous messaging between services. Producers publish events to a `Topic`; consumers attach `Subscription`s to react. Resources must be declared at package level — never inside functions.

## Topics

```typescript
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",
});
```

### Publishing

```typescript
await orderCreated.publish({
  orderId: "123",
  userId: "user-456",
  total: 99.99,
});
```

### Subscriptions

```typescript
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 treated as message attributes (for filtering/ordering):

```typescript
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",
});
```

### Topic References

Pass topic access to other code while maintaining static analysis:

```typescript
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 });
}
```

## Delivery Guarantees

- `at-least-once` (default): may deliver duplicates → handlers must be idempotent.
- `exactly-once`: stricter, capped throughput (AWS 300 msg/s/topic, GCP 3000+ msg/s/region). Does not deduplicate on the publish side.

## Guidelines

- Topics and subscriptions must be declared at package level.
- Subscription handlers must be idempotent (at-least-once delivery is the default).
- Use `Attribute<T>` for fields meant for filtering/ordering, not for arbitrary metadata.
- Don't do heavy synchronous work in `publish` callers — `publish` returns once the message is queued.

<!-- chapter:end slug=pubsub -->

---

<!-- chapter:begin slug=secret position=25 -->

## 25. encore-secret

- **Source:** https://github.com/encoredev/skills/blob/main/encore/secret/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/secret/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/secret.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-secret
description: Manage API keys, credentials, and other secrets in Encore.ts using `secret(...)` from `encore.dev/config`.
when_to_use: >-
  User wants to load a private credential into the service without committing it to the repo — third-party API keys (Stripe, OpenAI, Twilio, SendGrid), database passwords, signing keys, OAuth client secrets, JWT signing keys, webhook signing secrets. Covers `secret()` declarations, calling the secret as a function to read its value, setting values via `encore secret set`, and `.secrets.local.cue` for local overrides. Trigger phrases: "API key", "third-party token", "credentials", "without committing", "private key", "signing secret", "secret manager", ".env replacement", "encore secret set".
---

# Encore Secrets

## Instructions

Secrets are encrypted, environment-scoped values managed by Encore. Declare them at package level by calling `secret(name)` and read them by calling the returned function.

```typescript
import { secret } from "encore.dev/config";

// Package-level declaration
const stripeKey = secret("StripeSecretKey");

// Read inside a handler
async function chargeCustomer() {
  const key = stripeKey();        // <-- function call returns the value
  const stripe = new Stripe(key);
  // ...
}
```

Secret names are globally unique across the application (the same name resolves to the same value everywhere).

## Setting values

```bash
# Set per environment type
encore secret set --type prod StripeSecretKey
encore secret set --type dev StripeSecretKey
encore secret set --type local StripeSecretKey
```

Environment types: `production` (alias `prod`), `development` (alias `dev`), `preview` (alias `pr`), `local`.

## Local overrides

For local development without going through `encore secret set`, create a `.secrets.local.cue` file at the repo root (gitignore it):

```cue
StripeSecretKey: "sk_test_local_..."
GitHubAPIToken:  "ghp_local_..."
```

## Common usage patterns

```typescript
// HTTP headers
const githubToken = secret("GitHubAPIToken");
const resp = await fetch("https://api.github.com/user", {
  headers: { Authorization: `token ${githubToken()}` },
});

// Webhook signature verification
const stripeWebhookSecret = secret("StripeWebhookSecret");
stripe.webhooks.constructEvent(rawBody, sig, stripeWebhookSecret());

// Connecting to a third-party SDK
const openaiKey = secret("OpenAIKey");
const openai = new OpenAI({ apiKey: openaiKey() });
```

## Guidelines

- Always declare `secret(...)` at package level, never inside functions.
- Read with a function call: `stripeKey()` not `stripeKey`.
- Set distinct values per environment via `encore secret set --type <env>`.
- Never commit secret values; use `.secrets.local.cue` for local overrides and gitignore it.
- For webhook signature secrets specifically, see also the `encore-webhook` skill.

<!-- chapter:end slug=secret -->

---

<!-- chapter:begin slug=service position=26 -->

## 26. encore-service

- **Source:** https://github.com/encoredev/skills/blob/main/encore/service/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/service/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/service.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-service
description: Plan how to split an Encore.ts application into services and lay out its directory structure. Architecture and decomposition, not first-time CLI install (that's `encore-getting-started`).
when_to_use: >-
  User is deciding monolith vs. microservices, weighing "one service or several", drawing service boundaries, planning a multi-service system (e.g. orders + payments + inventory + shipping), creating an `encore.service.ts`, naming directories/folders, designing systems-of-services hierarchies, or asking for an application architecture / project layout recommendation. Trigger phrases: "lay out the directories", "directory structure", "service boundaries", "one service or several", "monolith vs microservices", "where to put", "systems of services".
---

# Encore Service Structure

## Instructions

### Creating a Service

Every Encore service needs an `encore.service.ts` file:

```typescript
// encore.service.ts
import { Service } from "encore.dev/service";

export default new Service("my-service");
```

### Minimal Service Structure

```
my-service/
├── encore.service.ts    # Service definition (required)
├── api.ts               # API endpoints
└── db.ts                # Database (if needed)
```

## Application Patterns

### Single Service (Recommended Start)

Best for new projects - start simple, split later if needed:

```
my-app/
├── package.json
├── encore.app
├── encore.service.ts
├── api.ts
├── db.ts
└── migrations/
    └── 001_initial.up.sql
```

### Multi-Service

For distributed systems with clear domain boundaries:

```
my-app/
├── encore.app
├── package.json
├── user/
│   ├── encore.service.ts
│   ├── api.ts
│   └── db.ts
├── order/
│   ├── encore.service.ts
│   ├── api.ts
│   └── db.ts
└── notification/
    ├── encore.service.ts
    └── api.ts
```

### Large Application (System-based)

Group related services into systems:

```
my-app/
├── encore.app
├── commerce/
│   ├── order/
│   │   └── encore.service.ts
│   ├── cart/
│   │   └── encore.service.ts
│   └── payment/
│       └── encore.service.ts
├── identity/
│   ├── user/
│   │   └── encore.service.ts
│   └── auth/
│       └── encore.service.ts
└── comms/
    ├── email/
    │   └── encore.service.ts
    └── push/
        └── encore.service.ts
```

## Service-to-Service Calls

Import other services from `~encore/clients`:

```typescript
import { user } from "~encore/clients";

export const getOrderWithUser = api(
  { method: "GET", path: "/orders/:id", expose: true },
  async ({ id }): Promise<OrderWithUser> => {
    const order = await getOrder(id);
    const orderUser = await user.get({ id: order.userId });
    return { ...order, user: orderUser };
  }
);
```

## When to Split Services

Split when you have:

| Signal | Action |
|--------|--------|
| Different scaling needs | Split (e.g., auth vs analytics) |
| Different deployment cycles | Split |
| Clear domain boundaries | Split |
| Shared database tables | Keep together |
| Tightly coupled logic | Keep together |
| Just organizing code | Use folders, not services |

## Service with Middleware

```typescript
import { Service } from "encore.dev/service";
import { middleware } from "encore.dev/api";

const loggingMiddleware = middleware(
  { target: { all: true } },
  async (req, next) => {
    console.log(`Request: ${req.requestMeta?.path}`);
    return next(req);
  }
);

export default new Service("my-service", {
  middlewares: [loggingMiddleware],
});
```

### Middleware Targeting

Control which endpoints middleware applies to:

```typescript
// Apply to all endpoints
middleware({ target: { all: true } }, handler);

// Apply only to authenticated endpoints
middleware({ target: { auth: true } }, handler);

// Apply only to exposed (public) endpoints
middleware({ target: { expose: true } }, handler);

// Apply to raw endpoints only
middleware({ target: { isRaw: true } }, handler);

// Apply to streaming endpoints only
middleware({ target: { isStream: true } }, handler);

// Apply to endpoints with specific tags
middleware({ target: { tags: ["admin", "internal"] } }, handler);
```

### Middleware Request Object

The request object provides access to:

```typescript
const myMiddleware = middleware(
  { target: { all: true } },
  async (req, next) => {
    // For typed and streaming APIs
    const meta = req.requestMeta;  // { method, path, pathParams }

    // For raw endpoints
    const rawReq = req.rawRequest;
    const rawRes = req.rawResponse;

    // For streaming endpoints
    const stream = req.stream;

    // Custom data to pass to handlers
    req.data = { startTime: Date.now() };

    const resp = await next(req);

    // Modify response headers
    resp.header.set("X-Response-Time", `${Date.now() - req.data.startTime}ms`);

    return resp;
  }
);
```

## Guidelines

- Services cannot be nested within other services
- Start with one service, split when there's a clear reason
- Use `~encore/clients` for cross-service calls (never direct imports)
- Each service can have its own database
- Service names should be lowercase, descriptive
- Don't create services just for code organization - use folders instead

<!-- chapter:end slug=service -->

---

<!-- chapter:begin slug=testing position=27 -->

## 27. encore-testing

- **Source:** https://github.com/encoredev/skills/blob/main/encore/testing/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/testing/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/testing.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-testing
description: Write or run automated tests for Encore.ts code with `encore test` and vitest/jest. Covers isolated per-test databases, calling handlers directly, and `describe`/`it`/`expect`.
when_to_use: >-
  User wants to add/write/fix a test, write a vitest or jest spec, test an endpoint or service, set up `encore test`, configure isolated test databases, write `beforeEach`/`afterEach` for db cleanup, mock external dependencies, or assert on API request/response behaviour. Trigger phrases: "write a test", "write a vitest test", "add tests for", "vitest", "jest", "encore test", "test the endpoint", "test the service", "integration test", "isolated database".
---

# Testing Encore.ts Applications

## Instructions

Encore.ts uses standard TypeScript testing tools. The recommended setup is Vitest.

### Setup Vitest

```bash
npm install -D vitest
```

Add to `package.json`:

```json
{
  "scripts": {
    "test": "vitest"
  }
}
```

### Test an API Endpoint

```typescript
// api.test.ts
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!");
  });
});
```

### Run Tests

```bash
# Run with Encore (recommended - sets up infrastructure)
encore test

# Or run directly with npm
npm test
```

Using `encore test` is recommended because it:
- Sets up test databases automatically
- Provides isolated infrastructure per test
- Handles service dependencies

### Test with Request Parameters

```typescript
// api.test.ts
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 Database Operations

Encore provides isolated test databases:

```typescript
// user.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { createUser, getUser, db } from "./user";

describe("user operations", () => {
  beforeEach(async () => {
    // Clean up before each test
    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 Service-to-Service Calls

```typescript
// order.test.ts
import { describe, it, expect } from "vitest";
import { createOrder } from "./order";

describe("order service", () => {
  it("creates an order and notifies user service", async () => {
    // Service calls work normally in tests
    const order = await createOrder({
      userId: "user-123",
      items: [{ productId: "prod-1", quantity: 2 }],
    });
    
    expect(order.id).toBeDefined();
    expect(order.status).toBe("pending");
  });
});
```

### Test Error Cases

```typescript
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

```typescript
// notifications.test.ts
import { describe, it, expect, vi } 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 the underlying function, not the cron schedule:

```typescript
// cleanup.test.ts
import { describe, it, expect } from "vitest";
import { cleanupExpiredSessions } from "./cleanup";

describe("cleanup job", () => {
  it("removes expired sessions", async () => {
    // Create some expired sessions first
    await createExpiredSession();
    
    // Call the endpoint directly
    await cleanupExpiredSessions();
    
    // Verify cleanup happened
    const remaining = await countSessions();
    expect(remaining).toBe(0);
  });
});
```

### Mocking External Services

```typescript
import { describe, it, expect, vi, beforeEach } from "vitest";
import { sendWelcomeEmail } from "./email";

// Mock external API
vi.mock("./external-email-client", () => ({
  send: vi.fn().mockResolvedValue({ success: true }),
}));

describe("email service", () => {
  it("sends welcome email", async () => {
    const result = await sendWelcomeEmail({ userId: "123" });
    expect(result.sent).toBe(true);
  });
});
```

### Test Configuration

Create `vite.config.ts` (required for `~encore` imports):

```typescript
/// <reference types="vitest" />
import { defineConfig } from "vite";
import path from "path";

export default defineConfig({
  resolve: {
    alias: {
      "~encore": path.resolve(__dirname, "./encore.gen"),
    },
  },
  test: {
    globals: true,
    environment: "node",
    include: ["**/*.test.ts"],
    coverage: {
      reporter: ["text", "json", "html"],
    },
  },
});
```

### VS Code Integration

Install the [Vitest extension](https://marketplace.visualstudio.com/items?itemName=vitest.explorer) and add to `.vscode/settings.json`:

```json
{
  "vitest.commandLine": "encore test"
}
```

**Note:** For VS Code test explorer, disable file-level parallelism to avoid port conflicts:

```typescript
// vite.config.ts
export default defineConfig({
  // ...
  test: {
    fileParallelism: false,  // Disable for VS Code
    // ...
  },
});
```

Re-enable for CI: `encore test --fileParallelism=true`

### Guidelines

- Use `encore test` to run tests with infrastructure setup
- Each test file gets an isolated database transaction (rolled back after)
- Test API endpoints by calling them directly as functions
- Service-to-service calls work normally in tests
- Mock external dependencies (third-party APIs, email services, etc.)
- Don't mock Encore infrastructure (databases, Pub/Sub) - use the real thing

<!-- chapter:end slug=testing -->

---

<!-- chapter:begin slug=webhook position=28 -->

## 28. encore-webhook

- **Source:** https://github.com/encoredev/skills/blob/main/encore/webhook/SKILL.md
- **Raw:** https://raw.githubusercontent.com/encoredev/skills/main/encore/webhook/SKILL.md
- **Markdown:** https://skillsdocs.com/encoredev/skills/webhook.md
- **Licence:** Apache-2.0 — https://spdx.org/licenses/Apache-2.0.html

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: encore-webhook
description: Receive inbound webhooks from external services (Stripe, GitHub, Slack, Twilio, etc.) using `api.raw(...)` from `encore.dev/api`. The right skill any time the user names a third-party provider that POSTs events to a URL you own.
when_to_use: >-
  User mentions a webhook, a /webhooks/* path, raw HTTP, `api.raw()`, accepting external callbacks, verifying webhook signatures (Stripe-Signature, X-Hub-Signature-256), reading the raw request body, parsing form-encoded payloads, or any time the user names a third-party provider that posts events — Stripe, GitHub, GitLab, Bitbucket, Shopify, Twilio, SendGrid, Mailgun, Auth0, Clerk, Slack, Discord, PayPal, Square. Use `encore-api` instead for typed JSON endpoints in your own service. Trigger phrases: "Stripe webhook", "GitHub webhook", "/webhooks/stripe", "raw HTTP endpoint", "api.raw", "verify the signature", "inbound webhook", "external callback".
---

# Encore Webhook Endpoints

## Instructions

Use `api.raw(...)` to receive inbound webhooks from third-party services. Raw endpoints give you direct access to the Node.js-style request and response objects, which you need for signature verification (the verification typically requires the unparsed raw body).

### 1. Import

```typescript
import { api } from "encore.dev/api";
```

### 2. Define the endpoint with `api.raw`

```typescript
export const stripeWebhook = api.raw(
  { expose: true, path: "/webhooks/stripe", method: "POST" },
  async (req, res) => {
    const sig = req.headers["stripe-signature"];
    // Read raw body
    const chunks: Buffer[] = [];
    for await (const chunk of req) chunks.push(chunk);
    const rawBody = Buffer.concat(chunks).toString("utf8");

    // Verify signature, parse event...

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ received: true }));
  }
);
```

### 3. Verify the signature

Most providers sign webhooks. Read the secret with `secret(...)` from `encore.dev/config` (see the `encore-secret` skill) and verify before trusting the payload:

```typescript
import { secret } from "encore.dev/config";
const stripeWebhookSecret = secret("StripeWebhookSecret");

// inside handler:
import Stripe from "stripe";
const stripe = new Stripe(stripeApiKey());
const event = stripe.webhooks.constructEvent(rawBody, sig, stripeWebhookSecret());
```

For GitHub, verify the HMAC-SHA256 in the `X-Hub-Signature-256` header against the raw body using your webhook secret.

## Common providers

| Provider | Signature header | Verification |
|---|---|---|
| Stripe | `Stripe-Signature` | `stripe.webhooks.constructEvent(rawBody, sig, secret)` |
| GitHub | `X-Hub-Signature-256` | HMAC-SHA256 over the raw body |
| Slack | `X-Slack-Signature` | HMAC-SHA256 over `v0:{timestamp}:{rawBody}` |
| Shopify | `X-Shopify-Hmac-Sha256` | HMAC-SHA256 (base64) over the raw body |
| Twilio | `X-Twilio-Signature` | HMAC-SHA1 over URL + sorted form fields |

## Always respond quickly

Webhook senders retry on non-2xx or slow responses. Acknowledge with a 2xx as soon as the payload is verified, then enqueue the actual work via Pub/Sub (see `encore-pubsub`) instead of doing it in the request handler.

```typescript
import { Topic } from "encore.dev/pubsub";

interface StripeEvent { id: string; type: string; data: unknown; }
const stripeEvents = new Topic<StripeEvent>("stripe-events", {
  deliveryGuarantee: "at-least-once",
});

// inside the raw handler, after verification:
await stripeEvents.publish({ id: event.id, type: event.type, data: event.data });
res.writeHead(200); res.end();
```

## Guidelines

- Use `api.raw` *only* for webhooks and other low-level HTTP integrations.
- Always verify the provider's signature before trusting the payload.
- Always respond 2xx fast — push slow work onto Pub/Sub.
- Store the signing secret with `secret(...)`; never inline it.
- For typed JSON endpoints in your own service, use plain `api(...)` from the `encore-api` skill.

<!-- chapter:end slug=webhook -->
