Configuration
Reference for quickback.config.ts — project name, template, features, providers, and compiler options.
The quickback.config.ts file configures your Quickback project. It defines which providers to use, which features to enable, and how the compiler generates code.
import { defineConfig, defineRuntime, defineDatabase, defineAuth } from "@quickback/compiler";
export default defineConfig({
name: "my-app",
template: "hono",
providers: {
runtime: defineRuntime("cloudflare"),
database: defineDatabase("cloudflare-d1"),
auth: defineAuth("better-auth"),
},
});Discoverable options block
After your first quickback compile, the file gets a managed block bracketed by sentinel comments:
export default defineConfig({
name: "my-app",
template: "hono",
providers: cloudflare({}),
// [quickback:options-start]
// ── Project ──
//
// Primary custom domain (auto-inferred from cms/account/api domains if unset).
// domain: "example.com",
//
// ── Embedded UIs (CMS / Account) ──
//
// Login/signup/profile/orgs SPA. Set `true` for default, or object to customize.
// account: {
// fileUploads: false, // show the avatar upload UI on the profile page
// auth: { passkey: false, emailOTP: false, ... },
// ...
// },
//
// ... every other option Quickback supports
// [quickback:options-end]
});This block is regenerated on every compile — the compiler is the source of truth, so newly added options surface automatically when you upgrade Quickback (no CLI bump needed).
To enable an option, copy the line you care about above the [quickback:options-start] sentinel and uncomment it:
export default defineConfig({
name: "my-app",
template: "hono",
providers: cloudflare({}),
account: {
fileUploads: true, // ← lifted out of the managed block
},
// [quickback:options-start]
// ... account is now omitted from this block on next compile
// [quickback:options-end]
});Quickback notices that account is set above the marker and drops it from the managed block on the next compile to avoid duplicate definitions. Anything outside the marker block is yours and never touched.
If you ever want to opt out of the auto-managed block, run:
quickback compile --no-sync-configRequired Fields
| Field | Type | Description |
|---|---|---|
name | string | Project name |
template | "hono" | Application template ("nextjs" is experimental) |
providers | object | Runtime, database, and auth provider configuration |
Optional Fields
Every optional top-level key the config accepts is listed below. You never have to memorize them — after your first compile, the discoverable options block lists each one (minus the ones you've set) right inside your quickback.config.ts. This table is the human reference; follow the links for detailed pages.
| Field | Type | Description |
|---|---|---|
domain | string | Primary custom domain (auto-inferred from cms/account/api domains if unset) — see Domains |
api | object | Dedicated hostname for the feature API surface (/api/v1/*); additive alias — see Domains |
auth | object | App-level auth settings (roleHierarchy, jwt, loginUrl/profileUrl, domain) — see Access |
authz | object | Relationship-based authorization (relationships, roles, permissions, arrows) — see Permissions |
trustedOrigins | string[] | CORS trusted origins for cross-domain auth |
features | object | Feature flags such as organizations / pinnedOrganizationId — see Pinned Organization Mode |
cms | boolean | object | Embed CMS in compiled Worker (domain, access, sysadmin) — see CMS |
account | boolean | object | Embed Account UI in compiled Worker (domain, name, auth.*, branding) — see Account UI |
apps | object | Mount hand-authored SPAs on their own hostname or path prefix — see Domains |
agents | boolean | object | AI/agent discovery surface (robots.txt, llms.txt, MCP, OAuth discovery) — see Agents |
security | object | false | HTTP response security headers (CSP, HSTS, COOP/COEP, Referrer/Permissions-Policy) — see Security Headers |
openapi | object | OpenAPI spec generation (generate, publish, docs: 'scalar' | 'swagger', types) — see OpenAPI |
schemaRegistry | object | Schema registry generation for the CMS ({ generate: false } to disable) — see Schema Registry |
etag | object | ETag caching for GET responses ({ enabled: false } to disable) — see Caching & ETags |
email | object | Email delivery config (provider, from, fromName, region) — see Email Configuration |
bindings | object | Extra Cloudflare bindings (durable objects, queues, R2, services, vars, AI) — see Bindings |
compiler | object | Compiler internals — features.auditFields: false, migrations.renames (headless CI), securityContracts, injectId, routes |
build | object | Build options (outputDir, packageManager, dependencies) — see Custom Dependencies |
dev | object | Local development settings (port, api, cms, account, hostnames) — see Variables |
preset | string | Inherit defaults from a named preset |
Custom Dependencies
Add third-party npm packages to the generated package.json using build.dependencies. This is useful when your action handlers import external libraries.
export default defineConfig({
name: "my-app",
template: "hono",
build: {
dependencies: {
"fast-xml-parser": "^4.5.0",
"lodash-es": "^4.17.21",
},
},
providers: {
runtime: defineRuntime("cloudflare"),
database: defineDatabase("cloudflare-d1"),
auth: defineAuth("better-auth"),
},
});These are merged into the generated package.json alongside Quickback's own dependencies. Run npm install after compiling to install them.
Headless Migration Rename Hints
When drizzle-kit generate detects a potential rename, it normally prompts for confirmation. Cloud/CI compiles are non-interactive, so you must provide explicit rename hints.
Add hints in compiler.migrations.renames:
export default defineConfig({
// ...
compiler: {
migrations: {
renames: {
// Keys are NEW names, values are OLD names
tables: {
events_v2: "events",
},
columns: {
events: {
summary_text: "summary",
},
},
},
},
},
});Rules:
tables:new_table_name -> old_table_namecolumns.<table>:new_column_name -> old_column_name- Keys must match the names in your current schema (the new names)
- Validation fails fast for malformed rename config, unsupported keys, or conflicting legacy/new rename paths.
Disable Audit Field Injection
By default, Quickback auto-injects four audit fields (createdAt, createdBy, modifiedAt, modifiedBy) into every feature table, plus two soft-delete fields (deletedAt, deletedBy) into tables that use soft delete (the default delete mode). The audit fields drive record-provenance queries and the CMS audit panel; the soft-delete pair drives soft-delete filtering.
To opt out project-wide:
export default defineConfig({
name: "my-app",
compiler: {
features: {
auditFields: false, // skip audit-field injection globally
},
},
providers: { /* ... */ },
});When disabled, you're responsible for supplying these columns yourself if you want soft delete, audit trails, or modifiedAt-based optimistic concurrency. See the Schema → Audit Fields section for the full list and what each field drives.
Security Contract Reports and Signing
After generation, Quickback verifies machine-checkable security contracts for Hono routes and RLS SQL.
It also emits a report artifact and signature artifact by default:
reports/security-contracts.report.jsonreports/security-contracts.report.sig.json
You can configure output paths and signing behavior:
export default defineConfig({
// ...
compiler: {
securityContracts: {
failMode: "error", // or "warn"
report: {
path: "reports/security-contracts.report.json",
signature: {
enabled: true,
required: true,
keyEnv: "QUICKBACK_SECURITY_REPORT_SIGNING_KEY",
keyId: "prod-k1",
path: "reports/security-contracts.report.sig.json",
},
},
},
},
});If signature.required: true and no key is available, compilation fails with a clear error message (or warning in failMode: "warn").
Hostname architecture (v0.33+)
A modern Quickback project runs on one Cloudflare Worker that serves multiple URL surfaces. By default, everything lives under a single unified backend hostname (auto-inferred as quickback.{baseDomain} from your other configured subdomains, or overridable via config.domain). For larger deployments, you can opt-in to dedicated hostnames for specific surfaces — they're additive aliases, never replacements.
The unified backend contract
When any subdomain is configured (account.domain, cms.domain, auth.domain, api.domain, apps.<n>.domain), the compiler always emits a unified backend host where every compiler-emitted backend route is reachable:
/api/v1/*— feature routes/auth/v1/*— Better Auth/storage/*— R2 file storage/health,/openapi.json— monitoring/discovery/account/*,/cms/*— bundled SPAs (when enabled)/broadcast/v1/*(realtime),/mcp(MCP), etc.
The unified hostname is quickback.{baseDomain} by default (where baseDomain is extracted from your first configured subdomain). Override with config.domain if you prefer a different name.
This contract is forward-pointing: it gives a stable URL pattern for future management tooling — https://quickback.{baseDomain}/... will always reach a Quickback backend regardless of how the operator splits their surfaces.
Dedicated hostnames (optional, additive)
For branding or operational separation, you can opt specific surfaces onto their own hostnames:
{
// Bundled SPAs on their own hosts (already-shipped pattern):
cms: { domain: 'cms.example.com' },
account: { domain: 'account.example.com' },
// v0.33+ — backend surfaces on their own hosts:
auth: { domain: 'auth.example.com' },
api: { domain: 'api.example.com' },
// User SPAs (existing pattern):
apps: {
www: { domain: 'app.example.com' },
},
}On a dedicated hostname, the hostname IS the namespace — the path drops the prefix that matches the hostname:
| URL on dedicated host | Internally rewritten to | Notes |
|---|---|---|
https://api.example.com/v1/users | /api/v1/users | Worker-entry rewriter prepends /api |
https://auth.example.com/v1/sign-in/email | /auth/v1/sign-in/email | Same shape with /auth |
https://api.example.com/api/v1/users | (unchanged) | Long form also accepted |
https://api.example.com/auth/v1/sign-in | 404 | Auth doesn't live on the API hostname |
https://api.example.com/health | (unchanged) | Explicit exception — monitoring agents expect /health everywhere |
Dual-reach is preserved on the unified host:
| URL on unified host | Handler | Notes |
|---|---|---|
https://quickback.example.com/api/v1/users | feature route | Works whether api.domain is set or not |
https://quickback.example.com/auth/v1/sign-in/email | Better Auth | Works whether auth.domain is set or not |
https://quickback.example.com/v1/users | 404 | Shortcut only applies on dedicated hostnames |
Each dedicated hostname also:
- Gets a
{ pattern, custom_domain: true }route inwrangler.tomlautomatically - Joins
trustedOriginsfor CORS - Joins the cross-subdomain cookie inference (auto-enables
crossSubDomainCookieswhen ≥2 same-parent subdomains are configured, soauth.example.comandapp.example.comcan share session cookies) - Joins CSP
connect-srcso SPAs can fetch from it
Why two URL shapes (Shape 1 vs Shape 2)
If you only want Better Auth to advertise itself on a different URL (e.g. so verification emails link to auth.example.com instead of the unified host), set the BETTER_AUTH_URL env var on the Worker — no code change required. BA generates outgoing URLs against that value. This is sufficient when the goal is branding email/OAuth callback URLs without changing where requests actually land.
auth.domain (Shape 1, above) is the heavier option: it binds an actual hostname to the Worker, configures CORS/cookies/CSP for it, and adds the path-shortcut rewriter. Use it when you want the dedicated hostname as a real ingress, not just a label.
CMS Configuration
Embed the Quickback CMS in your compiled Worker. The CMS reads your schema registry and renders a complete admin interface — zero UI code per table.
// Simple — embed CMS at root
cms: true,
// With custom domain
cms: { domain: "cms.example.com" },
// With access control (only admins can access CMS)
cms: { domain: "cms.example.com", access: "admin" },
// Cross-tenant DB-admin tier — sysadmins see every tenant's data
cms: { domain: "cms.example.com", sysadmin: true },| Option | Type | Default | Description |
|---|---|---|---|
domain | string | — | Custom domain for CMS. Adds a custom_domain route to wrangler.toml. |
access | "user" | "admin" | "admin" | Controls who can access the CMS. When "admin", platform control-plane users (user.role === "appmanager") can enter; when sysadmin mode is also on, user.role === "sysadmin" is admitted too. The gate is enforced at the Worker level (SPA shell, /api/v1/schema, and custom_view CRUD all return 403 to callers outside those roles) and in the SPA. Also hides the CMS link in Account UI. See CMS Connecting for the full enforcement model. |
sysadmin | boolean | false | Cross-tenant DB-admin tier. When true, opts into the SYSADMIN pseudo-role (user.role === "sysadmin"), the firewall escape hatch (sysadmins bypass tenant scopes, soft-delete still enforced), the cross-tenant /auth/v1/admin/list-organizations endpoint (sibling of Better Auth's /auth/v1/admin/list-users, gated to sysadmin only), and the SPA's "All organizations" sentinel. The CMS HTML gate admits userRole === "sysadmin" alongside "appmanager". Off by default. See SYSADMIN pseudo-role. |
Sysadmin tier (cms.sysadmin: true)
Quickback's default model assumes every CMS user belongs to at least one organization (member row in the auth DB). For platforms that need a Supabase-style cross-tenant DB-admin tool — debugging a tenant's data, running cross-tenant queries, supporting a customer — that's a poor fit: a sysadmin without member rows would dead-end, and auto-filling their active org from "first membership" silently scopes them to a tenant they have no relationship with.
cms: { sysadmin: true } introduces a separate role tier:
- Role:
user.role === "sysadmin"(set on the Better Auth user table). Distinct from"appmanager"— appmanagers keep platform control-plane access (CMS shell, Better Auth admin surfaces, support tooling), while sysadmins get true cross-tenant data access.ADMINis now a rejected legacy pseudo-role; useuserRole: ["appmanager"]for control-plane gates androles: ["SYSADMIN"]for cross-tenant access. - Firewall: every
buildFirewallConditions()emits anif (ctx.userRole === "sysadmin") return undefined(or just the soft-delete predicate when present) at the top, so sysadmins see every tenant's rows. See Firewall escape hatch. - Org listing: a
GET /auth/v1/admin/list-organizationsendpoint selects directly from the auth-schemaorganizationtable — every row, every tenant. Sits under Better Auth's admin URL namespace (sibling of/auth/v1/admin/list-users) so the URL shape matches BA convention; the handler is Quickback-emitted and gated touser.role === "sysadmin"only. For the membership-bound caller's-orgs listing, use BA's own/auth/v1/organization/list. - CMS SPA: when the user is a sysadmin, the header switcher loads from the cross-tenant endpoint, defaults to "All organizations" (which omits
?organizationId=and reaches the firewall escape), and pins selected tenants via?organizationId=on every API call. Org admins keep the existing membership-based switcher. - CMS HTML gate: admits
userRole === "sysadmin"alongside"appmanager". Org admins are not locked out when you setcms.access: "user";cms.access: "admin"is specifically the platform control-plane gate.
Off by default. Projects that don't enable it get zero behavior change; setting cms.sysadmin: true and granting a user user.role = "sysadmin" is the only way to unlock the cross-tenant view.
Account UI Configuration
Embed the Account UI in your compiled Worker. Provides login, signup, profile management, organization management, and admin — all built from your config at compile time.
For post-signup logic — auto-provisioning a workspace, gating signup, setting an active organization — drop a file in quickback/hooks/. See Auth Hooks.
// Simple — embed Account UI with defaults
account: true,
// Full configuration
account: {
domain: "auth.example.com",
adminDomain: "admin.example.com",
name: "My App",
companyName: "My Company",
auth: {
password: true,
emailOTP: false,
passkey: true,
organizations: true,
admin: true,
},
},| Option | Type | Description |
|---|---|---|
domain | string | Custom domain for Account UI (e.g., auth.example.com) |
adminDomain | string | Custom domain for admin-only access (e.g., admin.example.com). Serves the same Account SPA — the client-side router handles admin routing. |
appUrl | string | URL of the tenant's main application — where users are redirected after login. Baked into the Account SPA as VITE_QUICKBACK_APP_URL. Independent from domain. |
name | string | App display name shown on login, profile, and email templates |
companyName | string | Company name for branding |
tagline | string | App tagline shown on login page |
description | string | App description |
Auth Feature Flags
Control which authentication features are exposed. Disabled features are excluded from the Account UI bundle at compile time (routes removed before the Vite build, keeping bundle sizes minimal) and enforced on the server side via the Better Auth config — so "signup off" actually closes POST /auth/v1/sign-up/email, not just the SPA's signup screen.
account.auth.* is a shorthand for providers.auth.config — the compiler maps each flag onto the Better Auth server config before any code is generated. You can write either side directly; setting both incompatibly (e.g. account.auth.signup: false and emailAndPassword.disableSignUp: false) fails the compile with a precise error rather than picking a silent winner.
| Flag | Type | Default | Maps to (server) | Description |
|---|---|---|---|---|
auth.password | boolean | true | emailAndPassword.enabled | Email/password authentication — both server endpoint and UI |
auth.emailOTP | boolean | false | adds emailOtp plugin | Email one-time password login — adds plugin server-side, surfaces UI |
auth.passkey | boolean | false | adds passkey plugin | WebAuthn/FIDO2 passkey authentication — adds plugin server-side, surfaces UI |
auth.signup | boolean | true | emailAndPassword.disableSignUp (inverted) | Self-service account registration — false hides the UI and rejects the server endpoint with 403 |
auth.organizations | boolean | true | UI surface only | Organization management UI (dashboard, invitations, roles). Server-side organization support stays on regardless; set features.pinnedOrganizationId when you want one fixed org with no switching UI |
auth.admin | boolean | false | UI surface only | Admin panel for user management in the Account SPA. The Better Auth admin plugin is compiler-managed separately from this UI toggle |
auth.emailVerification | boolean | false | emailAndPassword.requireEmailVerification | Require email verification before sessions are issued. False = sessions usable immediately after signup |
Trusted Origins
List of origins allowed to make cross-origin requests to your API. Required when your CMS, Account UI, or frontend app run on different domains.
trustedOrigins: [
"https://auth.example.com",
"https://admin.example.com",
"https://cms.example.com",
"https://app.example.com",
],Local Development
Configure local development URLs. These are automatically added to the trusted origins list for CORS and Better Auth, so authentication works out of the box during local development.
dev: {
port: 8787, // wrangler dev port (default: 8787)
api: "http://localhost:8787", // API backend URL
cms: "http://localhost:8787/cms", // CMS URL (path on same worker)
account: "http://localhost:8787/account", // Account URL (path on same worker)
},If you run a SPA standalone on its own Vite dev server, set its URL here so CORS allows it:
dev: {
port: 8787,
api: "http://localhost:8787",
account: "http://localhost:5173", // standalone Account dev server
},| Field | Type | Default | Description |
|---|---|---|---|
port | number | 8787 | Local dev server port (sets [dev].port in wrangler.toml) |
api | string | http://localhost:{port} | API backend URL |
cms | string | — | CMS dev URL (added to trusted origins if set) |
account | string | — | Account dev URL (added to trusted origins if set) |
Email Configuration
Configure email delivery for authentication emails (verification, password reset, invitations).
Cloudflare Email Sending is now in public beta and is Quickback's default transport for Cloudflare deploys. The Workers
send_emailbinding is zero-config — no API keys, no third-party accounts. AWS SES remains supported as an explicit opt-in (email.provider: 'aws-ses') for non-Cloudflare runtimes or existing SES setups.
// Cloudflare Email Service (default) — uses send_email binding, no API keys needed
email: {
from: "noreply@example.com",
fromName: "My App",
},
// AWS SES — requires AWS credentials set as Wrangler secrets
email: {
provider: "aws-ses",
region: "us-east-2",
from: "noreply@example.com",
fromName: "My App",
},| Field | Type | Default | Description |
|---|---|---|---|
provider | "cloudflare" | "aws-ses" | "cloudflare" | Email transport |
from | string | "noreply@example.com" | Sender email address |
fromName | string | "{name} | Account Services" | Sender display name |
region | string | "us-east-2" | AWS SES region (aws-ses only) |
Auto-wiring
For the Cloudflare provider (the default), the compiler automatically:
- Emits
[[send_email]] EMAILintowrangler.toml - Generates
src/lib/cloudflare-email.ts - Wires
sendResetPassword,sendVerificationEmail,sendInvitationEmail, etc. into Better Auth
…whenever any auth surface needs to send mail — emailAndPassword.enabled, the emailOtp / magicLink / organization plugins — or you set the top-level email: {…} config. You don't need to add bindings.sendEmail by hand for the standard cases.
Cloudflare Email Routing setup is required. The binding alone does not deliver mail. Enable Email Routing on the sending zone and add at least one verified destination address (Cloudflare docs). Otherwise sends fail at runtime even with the binding present and the
/auth/v1/ses/statuscheck passing locally.
Multi-Domain Example
A complete config with CMS, Account UI, and Admin on separate subdomains:
export default {
name: "my-app",
template: "hono",
cms: { domain: "cms.example.com", access: "admin" },
account: {
domain: "auth.example.com",
adminDomain: "admin.example.com",
name: "My App",
companyName: "Example Inc.",
auth: {
password: true,
emailOTP: false,
passkey: true,
organizations: false,
admin: true,
},
},
email: {
from: "noreply@example.com",
fromName: "My App",
},
trustedOrigins: [
"https://auth.example.com",
"https://admin.example.com",
"https://cms.example.com",
"https://example.com",
],
providers: {
runtime: { name: "cloudflare", config: {
routes: [{ pattern: "api.example.com", custom_domain: true }],
}},
database: { name: "cloudflare-d1", config: { binding: "DB" } },
auth: { name: "better-auth", config: { plugins: ["admin"] } },
},
};This produces a single Cloudflare Worker serving five domains. See Multi-Domain Architecture for details on hostname routing, cross-subdomain cookies, and the unified fallback domain.
See the sub-pages for detailed reference on each section:
- Providers — Runtime, database, and auth options
- Variables — Environment variables and flags
- Output — Generated file structure
- Pinned Organization Mode — Fixed-org deployments without org switching UI
- Multi-Domain Architecture — Custom domains, hostname routing, and cross-subdomain auth