Multi-Tenant Architecture
One database, one Worker deployment, unlimited tenants. Costs grow with usage, not with tenant count.
Why This Structure
Traditional multi-tenant approaches -- one database per tenant, or one deployment per tenant -- create linear cost growth. Ten tenants means ten databases. A hundred tenants means a hundred databases, each with idle connections and baseline costs.
We structured it differently because the cost math did not work otherwise:
| Resource | Scales with | Cost impact |
|---|---|---|
| Neon PostgreSQL | Storage + compute time | Pay for actual queries, not idle connections |
| Hyperdrive | Connection count | Pools connections at the edge, Neon sees fewer |
| Cloudflare Workers | Request count | Single deployment serves all tenants |
| Domain mapping | Tenant count | DNS records, near-zero marginal cost |
Adding a new tenant means inserting a row in tenants and a DNS record. No new database, no new deployment, no new infrastructure. This keeps infrastructure costs flat as tenant count grows.
Three Tiers
Tier 0 (Platform) -> Control plane, global defaults, design tokens
Tier 1 (Partner) -> Branded instance, manages sub-tenants, owns domains
Tier 2 (Customer) -> Consumes platform, inherits branding, scoped accessSimple projects start with dormant Tier 1 and Tier 2. You hardcode a single tenant_id and the multi-tenant machinery activates later. No retrofitting required -- the schema already knows about tiers.
Tier Governance
Each tier has a visibility scope. The rules are strict because they are enforced at the database level, not in application code.
| Tier | Can see | Can modify | Token governance |
|---|---|---|---|
| T0 (Platform) | Everything across all tenants | Global defaults, protected tokens, platform config | Owns PROTECTED_TOKEN_PATHS -- no override allowed |
| T1 (Partner) | Own data + all T2 children | Own branding, own tenants, theme selection | Can override non-protected tokens for own scope |
| T2 (Customer) | Only own scoped data | Own content within partner's boundaries | Inherits T1 branding, can fine-tune non-locked tokens |
This hierarchy means a T1 partner admin cannot see another partner's data, even if they share the same database. A T2 customer cannot see other customers under the same partner. The database enforces this, not application-level filters.
Token Governance Example
Design tokens follow the same governance model:
T0 defines: --status-error: #DC2626 (PROTECTED -- cannot override)
T1 overrides: --interactive-primary: ... (brand color -- allowed)
T2 fine-tunes: --background-canvas: ... (within T1's allowed range)Protected tokens (error states, focus rings, WCAG-required contrast pairs) are locked at T0. Brand-level tokens flow down from T1 and can be selectively overridden at T2.
Data Isolation
Shared Schema with RLS
One Neon project, shared schema, Row-Level Security (RLS) for isolation.
-- Every tenant-scoped table includes tenant_id
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
title TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- RLS enforces isolation at the database level
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON documents
USING (tenant_id = current_setting('app.tenant_id')::uuid);The database enforces isolation. Application code does not need to remember WHERE tenant_id = ? -- RLS handles it. If a query somehow omits the tenant filter, it returns zero rows instead of leaking data.
Branch Isolation (Development)
Branch isolation is separate from tenant isolation. Neon branches give each developer or CI run their own copy of the database without copying data. This is for development workflow, not tenant isolation.
Production DB
├── branch: dev/feature-auth (developer A)
├── branch: dev/fix-rls (developer B)
└── branch: ci/pr-142 (CI run)Each branch is a copy-on-write fork. It starts with production data but diverges independently. Branches are cheap to create and dispose of.
Hyperdrive Connection Pooling
Cloudflare Workers are stateless. Every request to Neon would normally open a new TCP connection, which is expensive. Hyperdrive sits between Workers and Neon, maintaining a connection pool at the edge.
Request -> Worker -> Hyperdrive (pool) -> NeonConfiguration lives in wrangler.jsonc:
{
"hyperdrive": [{
"binding": "DB",
"id": "your-hyperdrive-id"
}]
}In application code:
const db = drizzle(env.DB.connectionString);Hyperdrive handles connection reuse. Your code does not manage pools.
Tenant Resolution
A single Cloudflare Worker deployment serves all tenants. The Worker resolves which tenant a request belongs to by inspecting the request domain.
// Simplified tenant resolution
const host = new URL(request.url).hostname;
const tenant = await db.query.domainMappings.findFirst({
where: eq(domainMappings.domain, host),
});
// Set the tenant context for RLS
await db.execute(
sql`SELECT set_config('app.tenant_id', ${tenant.id}, true)`
);Domain types:
| Type | Example | Resolution |
|---|---|---|
| Platform | app.syncupsuite.com | Tier 0 -- platform admin |
| Partner custom | brand.example.com | Tier 1 -- lookup in domain_mappings |
| Partner subdomain | acme.brandsyncup.com | Tier 1 -- parse subdomain |
| Customer | Inherits from Partner | Tier 2 -- scoped by partner's tenant |
Auth Graduation
Authentication follows the same tiered model. Users do not start with a full account -- they graduate into one:
Anonymous -> Preview/Inquiry -> OAuth (Google/GitHub) -> Full AccountEach step adds capabilities without requiring migration. A user who starts as anonymous and later signs in with Google retains their session history. The auth system (Better Auth + Firebase Identity) manages this graduation transparently.
Tier governance applies to auth as well: a T1 partner can configure which OAuth providers are available to their T2 customers.
What This Means in Practice
- Adding tenant #100 costs the same as tenant #10. No new infrastructure, no new deployments.
- RLS is the security boundary, not application code. Even a bug in the application layer cannot leak cross-tenant data.
- Tokens respect hierarchy. Platform tokens are protected. Partner tokens cascade down. Customer tokens fine-tune within bounds.
- One Worker serves all. Domain mapping, not deployment configuration, determines tenant routing.