# Custom Bindings

Quickback's compiler owns the D1/KV/R2/ASSETS/queue blocks it needs for its own features. Anything else your worker binds to — Cloudflare Email, an extra R2 bucket, a custom queue, a service binding, your own Durable Object, Workers AI, or non-secret `[vars]` — goes under the top-level `bindings` key:

```ts
import { defineConfig, defineRuntime, defineDatabase, defineAuth } from "@quickback-dev/cli";

export default defineConfig({
  name: "studio-mail",
  template: "hono",
  providers: {
    runtime: defineRuntime("cloudflare"),
    database: defineDatabase("cloudflare-d1", { splitDatabases: true }),
    auth: defineAuth("better-auth"),
  },
  bindings: {
    sendEmail: [{ name: "EMAIL" }],
    r2Buckets: [
      { binding: "MAIL_RAW",         bucketName: "studio-mail-raw" },
      { binding: "MAIL_ATTACHMENTS", bucketName: "studio-mail-attachments" },
    ],
    vars: {
      MAIL_SENDING_DOMAIN: "studio.example.com",
    },
  },
});
```

camelCase field names map 1:1 to the snake_case TOML blocks in `wrangler.toml`. Every compile regenerates `wrangler.toml` from your config, so these entries are the source of truth — no more post-compile patch scripts.

## Fields

### `sendEmail`

Emits `[[send_email]]`. Use when your worker calls `env.<NAME>.send(...)` via Cloudflare Email Service.

```ts
sendEmail: [
  {
    name: "EMAIL",
    // Optional: restrict who the worker is allowed to send to.
    destinationAddress: "alerts@example.com",
    allowedDestinationAddresses: ["alerts@example.com", "ops@example.com"],
  },
]
```

### `r2Buckets`

Emits additional `[[r2_buckets]]` blocks alongside the compiler-owned files bucket.

```ts
r2Buckets: [
  {
    binding: "MAIL_RAW",
    bucketName: "studio-mail-raw",
    previewBucketName: "studio-mail-raw-preview", // optional
    jurisdiction: "eu",                           // optional
  },
]
```

### `queueProducers` / `queueConsumers`

Emits `[[queues.producers]]` and `[[queues.consumers]]`. Useful for app-owned queues beyond the compiler's webhook/embedding queues.

```ts
queueProducers: [
  { binding: "MAIL_QUEUE", queue: "mail-send" },
],
queueConsumers: [
  {
    queue: "mail-send",
    maxBatchSize: 5,
    maxBatchTimeout: 30,
    maxRetries: 3,
    maxConcurrency: 2,
    deadLetterQueue: "mail-dlq",
  },
]
```

### `services`

Emits `[[services]]` — service bindings to sibling workers.

```ts
services: [
  { binding: "INBOUND", service: "studio-mail-inbound", environment: "production" },
]
```

### `durableObjects`

Emits `[[durable_objects.bindings]]` for app-owned Durable Objects. Don't use this for Quickback's built-in `BROADCASTER` realtime DO — that one is compiler-owned.

A DO entry has two flavors: **in-worker** (the class lives in this worker — set `from`) or **cross-worker** (the class lives in another worker — set `scriptName`). The two are mutually exclusive.

```ts
durableObjects: [
  // In-worker DO — Quickback re-exports the class from src/index.ts
  // and emits a [[migrations]] block. Class file lives at
  // quickback/features/loops/lib/ItemStream.ts (copied verbatim to src/).
  { name: "ITEM_STREAM", className: "ItemStream", from: "features/loops/lib/ItemStream" },

  // Cross-worker DO — the exporting worker owns the migration; this
  // worker only declares the binding.
  { name: "RATE_LIMITER", className: "RateLimiter", scriptName: "rate-limit-worker" },
]
```

Each in-worker DO entry accepts:

| Field | Default | Notes |
|-------|---------|-------|
| `name` | — | Wrangler binding name. Becomes `c.env.<NAME>` (typed as `DurableObjectNamespace`). |
| `className` | — | The exported TypeScript class. |
| `from` | — | Module path under `src/` (no `./`, no `.ts`). The compiler copies `quickback/lib/...` and `quickback/features/<f>/lib/...` verbatim into `src/`, so use that same relative path. |
| `migrationTag` | `qb-do-${className}` | Wrangler migration tag. Stable across regenerations — **never remove or rename a deployed tag**, or wrangler will reject the deploy. |
| `useSqlite` | `true` | SQLite-backed DO storage. Set to `false` for the legacy KV-backed mode. |
| `scriptName` | — | Cross-worker only. Mutually exclusive with `from`. |

Where the class file lives: in-worker DO classes go under `quickback/lib/` or `quickback/features/<feature>/lib/` so they survive every compile (the rest of `src/` is wiped). Both copy verbatim into the generated `src/` tree.

### `durableObjectMigrations`

Cloudflare's `[[migrations]]` list is **append-only** — once a tag is deployed, removing it from `wrangler.toml` causes the next deploy to fail with code 10061 ("Cannot apply migration"). Quickback regenerates `wrangler.toml` from your config on every compile, so any historical lifecycle entries (deleted, renamed, transferred classes) need to live in the config too — otherwise they'd be wiped.

`bindings.durableObjectMigrations[]` is the slot for those entries. Compiler emits them **first** in the [[migrations]] list, before the auto-emitted current-class creation tags, so CF sees the chronology it needs.

```ts
bindings: {
  durableObjects: [
    { name: "NOTEPAD", className: "Notepad", from: "lib/Notepad" },
  ],
  // Historical lifecycle: a previous LegacyNotes class was deleted on
  // 2026-05-09. Without this entry, a recompile would drop the
  // deleted_classes block and the next deploy would 10061.
  durableObjectMigrations: [
    {
      tag: "qb-do-deleted-legacy-notes-2026-05-09",
      deletedClasses: ["LegacyNotes"],
    },
  ],
}
```

Each entry accepts:

| Field | Notes |
|-------|-------|
| `tag` | Migration tag. Once deployed, never change it. Convention: `qb-do-<verb>-<class>-<YYYY-MM-DD>`. |
| `newClasses` | KV-backed creations. Rare — most DOs use `new_sqlite_classes`. |
| `newSqliteClasses` | SQLite-backed creations. Use this if you ever need to declare a creation manually instead of letting the auto-emit handle it. |
| `deletedClasses` | Array of class names to tear down. **Destroys the DO's storage** on deploy — only use when you're sure. |
| `renamedClasses` | `[{ from, to }]` — class rename without losing storage. |
| `transferredClasses` | `[{ from, from_script, to }]` — move a class across workers. |

**When you remove a DO from `bindings.durableObjects`**, declare the teardown here. Don't hand-edit `wrangler.toml` — those edits get wiped on the next compile.

### `vars`

Merged into the compiler's own `[vars]` table — emitting a second `[vars]` block would be invalid TOML. Values must be `string`, `number`, or `boolean`.

```ts
vars: {
  MAIL_SENDING_DOMAIN: "studio.example.com",
  MAIL_MAX_ATTACHMENTS: 10,
  MAIL_DEBUG: false,
}
```

Secrets do **not** go here. Use Wrangler's secret store instead:

```bash
wrangler secret put MAIL_INBOUND_SECRET
```

### `ai`

Emits a top-level `[ai]` block. Use this if you need Workers AI bound under a name different from Quickback's internal `AI`.

```ts
ai: { binding: "USER_AI" }
```

## Reserved binding names

Binding names that clash with compiler-owned bindings are rejected at validation time, with the offending field path in the error message. Reserved names depend on what your config enables:

| Name | Reserved when |
|------|---------------|
| `AUTH_DB`, `DB` | `splitDatabases: true` (default) |
| `DATABASE` | `splitDatabases: false` |
| `AUDIT_DB` | always (cross-tenant unsafe-action audit) |
| `WEBHOOKS_DB`, `WEBHOOKS_QUEUE` | a webhooks binding is configured |
| `KV` | always |
| `R2_BUCKET`, `FILES_DB` | `fileStorage: defineFileStorage("cloudflare-r2")` |
| `VECTORIZE` | a Vectorize index is configured |
| `BROADCASTER` | realtime is enabled |
| `EMAIL` | `email: { provider: "cloudflare" }` |
| `ASSETS` | `cms: true` or `account: true` |
| `AI`, `EMBEDDINGS_QUEUE` | embeddings are configured |

Pick a different name — the generated `env` on the handler side takes whatever binding you pick.

## See also

- [Environment Variables](/compiler/config/variables) — the non-secret `[vars]` / secret split
- [Providers](/compiler/config/providers) — runtime / database / auth selection
