Multi-Domain Architecture
Serve CMS, Account UI, Admin, and user-authored SPAs on separate custom domains from a single Cloudflare Worker.
A single Quickback-compiled Cloudflare Worker can serve your API, CMS, Account UI, Admin panel, and your own product SPAs on separate custom domains. The compiler generates hostname-based routing so each domain serves the right content.
How It Works
When you configure custom domains for CMS, Account, Admin, or apps, the compiler:
- Adds
custom_domainroutes towrangler.toml - Generates hostname-based middleware in the Worker
- Auto-configures cross-subdomain cookies for shared authentication
- Auto-infers a unified
quickback.{baseDomain}fallback domain
All configured domains point to the same Worker — there's only one deployment.
Domain Types
| Domain | Config | Serves | API Routes |
|---|---|---|---|
api.example.com | providers.runtime.config.routes | API only | All |
cms.example.com | cms: { domain } | CMS SPA | Blocked (404) |
auth.example.com | account: { domain } | Account SPA | Pass-through |
admin.example.com | account: { adminDomain } | Account SPA | Pass-through |
www.example.com | apps: { <name>: { domain } } | User-authored SPA | Pass-through |
quickback.example.com | Auto-inferred | Everything | All |
CMS Domain
Serves the CMS SPA at root (/). API routes (/api/, /auth/, /admin/, /storage/, /health) return 404 — preventing direct API access on the CMS domain.
Account Domain
Serves the Account SPA at root (/). API and auth routes pass through to the Worker so authentication flows work directly on this domain.
Admin Domain
Identical behavior to the Account domain — serves the same Account SPA at root. The Account SPA's client-side router handles the /admin route, restricting access to users with the admin role.
App Domains
Mount your own product SPAs on dedicated hostnames via the apps field. Each entry binds one or more hostnames to a directory under src/apps/<name>/ and inherits the same API/auth pass-through behavior as the Account domain — so the SPA can hit /api/v1/... and /auth/... same-origin without CORS.
apps: {
www: {
domain: "www.example.com",
aliasDomains: ["example.com"], // apex serves the same SPA
},
marketing: {
domain: "marketing.example.com",
},
}| Option | Required | Description |
|---|---|---|
domain | Yes | Primary custom hostname served at root (/) |
aliasDomains | No | Additional hostnames that serve the same SPA. Each gets its own custom_domain = true route. Common case: apex (example.com) alongside www.example.com. |
assetsDir | No | Subdirectory under src/apps/ to serve from. Defaults to the record key. Override only when the on-disk directory needs to differ from the URL-facing name. |
Drop the prebuilt SPA output at quickback/apps/<name>/ — the source-apps passthrough copies it byte-for-byte into src/apps/<name>/ on every compile. The compiler doesn't build the SPA; it just emits routing for whatever's already there. See Output Structure for the passthrough rules.
The names cms, account, and public are reserved (they collide with compiler-emitted directories under src/apps/). Two app entries can't claim the same hostname, and an app's hostname can't collide with cms.domain / account.domain / account.adminDomain.
Unified Domain
The compiler auto-infers a quickback.{baseDomain} domain from your configured custom domains. For example, if you have cms.example.com, it creates quickback.example.com.
On this domain, everything is available on a single origin:
| Path | Content |
|---|---|
/ | Redirects to /cms/ |
/cms/ | CMS SPA |
/account/ | Account SPA |
/api/v1/... | API routes |
/auth/... | Auth routes |
/admin/... | Admin API routes |
/storage/... | File storage |
/health | Health check |
/openapi.json | OpenAPI spec |
The unified domain is useful for development and debugging. You can override it with an explicit domain field in your config.
Configuration
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",
auth: { password: true, admin: true },
},
apps: {
www: {
domain: "www.example.com",
aliasDomains: ["example.com"],
},
},
providers: {
runtime: { name: "cloudflare", config: {
routes: [{ pattern: "api.example.com", custom_domain: true }],
}},
// ...database, auth
},
};trustedOrigins is optional here — every cms.domain, account.domain, account.adminDomain, and apps[*].domain (plus aliases) is automatically added. Declare trustedOrigins only when you need to allow an origin the compiler can't infer (e.g. a third-party domain).
This generates the following wrangler.toml routes:
routes = [
{ pattern = "api.example.com", custom_domain = true },
{ pattern = "cms.example.com", custom_domain = true },
{ pattern = "auth.example.com", custom_domain = true },
{ pattern = "admin.example.com", custom_domain = true },
{ pattern = "www.example.com", custom_domain = true },
{ pattern = "example.com", custom_domain = true },
{ pattern = "quickback.example.com", custom_domain = true }
]Cross-Subdomain Authentication
When two or more custom domains share a parent domain (e.g., cms.example.com + auth.example.com + www.example.com), the compiler automatically configures Better Auth for cross-subdomain cookie sharing. App hostnames (primary + aliases) are folded into the shared-parent detection alongside the CMS, Account, Admin, and top-level domain slots:
- Sets
crossSubDomainCookies: { enabled: true, domain: '.example.com' } - Sets
sameSite: 'none'andsecure: trueon auth cookies
This means a user who logs in on auth.example.com is automatically authenticated on cms.example.com, admin.example.com, and www.example.com — no additional setup needed. Apex aliases (e.g. example.com listed under aliasDomains) are recognized as already being the parent — they don't break shared-parent equality.
Automatic Detection
Cross-subdomain cookies are configured automatically. You only need to set them manually if your domains don't share a common parent (e.g., auth.myapp.com + cms.different.com).
Compile-Time Feature Gating
When the compiler builds the Account SPA, it excludes route files for disabled features before the Vite build. This means disabled features never appear in the JavaScript bundle.
| Feature Flag | Routes Excluded When false |
|---|---|
auth.organizations | Dashboard, organization CRUD, org switching ($slug) |
auth.passkey | Passkey management and setup |
auth.admin | Admin panel routes |
This keeps bundle sizes minimal and prevents dead code in production. Users can't access disabled features even if they navigate to the URL directly — the routes don't exist in the build.
Hostname Routing Details
The compiler generates middleware that checks new URL(c.req.url).hostname on every request:
CMS domain — Blocks API routes with 404. All other paths are mapped to the /cms/ asset prefix and served with SPA fallback to /cms/index.html.
Account/Admin domain — API and auth routes (/api/, /auth/, /admin/, /storage/, /health) pass through to Hono handlers. All other paths are mapped to the /account/ asset prefix and served with SPA fallback to /account/index.html.
App domain — Identical to Account: API/auth/admin/storage/health pass through, other paths map to /<assetsDir>/ with SPA fallback. The multi-domain catchall (emitted when both CMS and Account are configured) explicitly skips app hostnames, so an API miss on an app host lands in app.notFound (404 JSON) instead of stray-serving a sibling SPA's HTML.
Unified domain — No hostname filtering. CMS is served at /cms/, Account at /account/. Root (/) redirects to /cms/. API routes work at their standard paths.
Without Custom Domains
If you enable cms and account without custom domains, everything is served on a single domain:
- CMS at
/cms/(root/redirects to/cms/) - Account UI at
/account/ - API at
/api/v1/ - Auth at
/auth/
This is the simplest setup — no DNS configuration needed, no cross-subdomain cookies. Auth cookies work naturally because everything is same-origin.
Preview Deploys
The same compiled bundle can be deployed to additional hostnames — preview environments, staging, per-PR URLs — without recompiling. Set the EXTRA_APP_HOSTS env var on the deploy target (a comma-separated list of hostnames) and the worker treats each one like an entry in apps[*].domain:
- Apps hostname middleware serves the SPA at root.
- CORS / Better Auth
trustedOriginstrusts requests whoseOriginhostname matches.
Empty or unset means no extension and zero behavior change for production.
[vars]
EXTRA_APP_HOSTS = "app-preview.example.com,pr-42.example.com"# Or set as a secret per deploy target
wrangler secret put EXTRA_APP_HOSTS --env previewMirrors the existing BETTER_AUTH_URL auto-trust behavior, where the deployed worker auto-trusts its own configured base origin without needing an explicit trustedOrigins entry. With EXTRA_APP_HOSTS, one env var extends both the apps allowlist and the CORS allowlist.
Multi-app deploys
When more than one app is hostname-mounted, extras attach to the first registered apps middleware (Hono runs middleware in registration order and terminates on first match). If you need extras to route to a specific app in a multi-app deploy, set apps[*].domain directly and recompile rather than using this env var.
See Also
- Configuration — CMS and Account config options
- Output Structure — Generated file and directory layout
- CMS — Schema-driven admin interface
- Account UI — Authentication and account management