Quickback Docs

Custom Bindings

Declare extra Cloudflare bindings (send_email, r2_buckets, queues, services, durable_objects, vars, ai) in quickback.config.ts so they survive every compile.

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:

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.

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.

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.

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.

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.

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:

FieldDefaultNotes
nameWrangler binding name. Becomes c.env.<NAME> (typed as DurableObjectNamespace).
classNameThe exported TypeScript class.
fromModule 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.
migrationTagqb-do-${className}Wrangler migration tag. Stable across regenerations — never remove or rename a deployed tag, or wrangler will reject the deploy.
useSqlitetrueSQLite-backed DO storage. Set to false for the legacy KV-backed mode.
scriptNameCross-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.

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:

FieldNotes
tagMigration tag. Once deployed, never change it. Convention: qb-do-<verb>-<class>-<YYYY-MM-DD>.
newClassesKV-backed creations. Rare — most DOs use new_sqlite_classes.
newSqliteClassesSQLite-backed creations. Use this if you ever need to declare a creation manually instead of letting the auto-emit handle it.
deletedClassesArray 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.

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:

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.

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:

NameReserved when
AUTH_DB, DBsplitDatabases: true (default)
DATABASEsplitDatabases: false
AUDIT_DBalways (cross-tenant unsafe-action audit)
WEBHOOKS_DB, WEBHOOKS_QUEUEa webhooks binding is configured
KValways
R2_BUCKET, FILES_DBfileStorage: defineFileStorage("cloudflare-r2")
VECTORIZEa Vectorize index is configured
BROADCASTERrealtime is enabled
EMAILemail: { provider: "cloudflare" }
ASSETScms: true or account: true
AI, EMBEDDINGS_QUEUEembeddings are configured

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

See also

On this page