1---2name: multi-tenant-architecture3description: Provides architecture guidance for multi-tenant SaaS platforms on Cloudflare or Vercel. Covers platform choice, domain strategy, tenant identification and isolation, subdomain routing, custom domains and SSL, white-label setup, tenant context propagation, PSL submission, and mapping platform limits to pricing plans. Use when building a multi-tenant application or asking "how do I support multiple tenants", "build a white-label platform", "add custom domains", "route tenants by subdomain", or "map limits to plans". For general app folder structure use define-architecture; for scaffolding a new Next.js repo use scaffold-nextjs.4---56# Multi-Tenant Platform Architecture (Cloudflare or Vercel)78- **IS:** domain strategy, tenant identification and isolation, subdomain routing, custom domains, white-label setup, and plan/limit mapping on Cloudflare or Vercel.9- **IS NOT:** general app folder structure or module boundaries (use `define-architecture`), or scaffolding a new repo (use `scaffold-nextjs`).1011## Contents1213- Platform dispatch (decide first)14- Workflow (order matters)15- Gotchas16- Output schema17- Pre-commit checklist18- Related skills1920## Platform dispatch (decide first)2122| Signals | Platform | Load |23|---------|----------|------|24| Tenants run untrusted or per-tenant code; need code-level isolation; edge-first compute on D1/KV/Durable Objects | Cloudflare (Workers for Platforms, dispatch namespaces) | [cloudflare-platform.md](references/cloudflare-platform.md) |25| All tenants share one Next.js codebase; need ISR, React Server Components, managed deploys | Vercel (App Router + Middleware) | [vercel-platform.md](references/vercel-platform.md), then [vercel-domains.md](references/vercel-domains.md) for domains |2627- Pick one platform and commit; never mix hosting (hybrid routing complexity compounds).28- Load only the chosen platform's references unless explicitly comparing.29- Load [psl.md](references/psl.md) when deciding domain strategy (step 1).30- Load [limits-and-quotas.md](references/limits-and-quotas.md) before mapping limits to pricing (step 8).31- `agents/openai.yaml` is launcher metadata for external runners only; do not load it in normal use.3233## Workflow (order matters)34351. Choose domain strategy36- Dedicated tenant domain, separate from the brand domain, for all subdomains and custom hostnames. Reputation does not isolate: a phishing site on `random.acme.com` damages the whole domain.37- Register a separate TLD for tenant workloads (e.g. `acme.app` for tenants, `acme.com` for brand).38- Untrusted content on sibling subdomains: choose PSL submission, record owner plus timeline. Otherwise record `No PSL` with the cookie-isolation reason. See [psl.md](references/psl.md).39- Start PSL early; review takes weeks.40412. Choose tenant identification strategy (pick one primary; offer custom domain as upgrade path)42- **Subdomain-based**: `tenant.yourdomain.com`. Requires wildcard DNS. Simplest at scale.43- **Custom domain**: tenant CNAMEs their own domain to your platform. Best for serious/paying tenants.44- **Path-based**: `yourdomain.com/tenant-slug`. No per-tenant DNS/SSL, but limits branding and complicates cookie isolation.45463. Define isolation model47- **Cloudflare**: per-tenant Workers via dispatch namespaces for untrusted code. Avoid shared-tenant branching unless you fully control code and data.48- **Vercel**: shared Next.js app with `tenant_id` scoping. Middleware resolves tenant from hostname; every query includes tenant context. Postgres RLS for defence-in-depth.49504. Route traffic deterministically (tenants never control routing or see each other)51- **Cloudflare**: platform Worker owns routing: hostname -> tenant id -> dispatch namespace -> tenant Worker. 404 when no mapping.52- **Vercel**: Middleware extracts hostname, rewrites to a `/domains/[domain]` segment; Edge Config for sub-millisecond lookups. 404 when no mapping.53545. Pass tenant context through the stack (single authority: Middleware or platform Worker; never trust client-supplied identity)55- **Cloudflare**: platform Worker resolves the tenant, injects headers/bindings before dispatching to the tenant Worker.56- **Vercel**: Middleware sets `x-tenant-id`, `x-tenant-slug`, `x-tenant-plan` on forwarded request headers (not the response). Server Components read via `headers()`; API routes read from request headers:57 ```ts58 // middleware.ts59 import { NextRequest, NextResponse } from "next/server";60 export function middleware(request: NextRequest) {61 const hostname = request.headers.get("host") ?? "";62 const tenant = hostname.split(".")[0]; // resolve from Edge Config/DB in production63 const requestHeaders = new Headers(request.headers);64 requestHeaders.set("x-tenant-id", tenant);65 return NextResponse.next({ request: { headers: requestHeaders } });66 }67 ```68696. Bind only what is needed70- **Cloudflare**: least-privilege bindings per tenant (DB/storage/limited platform API), no shared global state. New bindings are explicit changes; redeploy to grant access.71- **Vercel**: Edge Config for tenant config (domain mappings, feature flags, plan info). `@vercel/sdk` for domain management. DB connections scoped by `tenant_id`, or database-per-tenant (Neon).72737. Support custom domains and per-tenant static files74- Provide a DNS target, verify ownership, store the mapping, route by hostname.75- **Cloudflare**: Cloudflare for SaaS custom hostnames with managed certs. See [cloudflare-platform.md](references/cloudflare-platform.md).76- **Vercel**: `@vercel/sdk` for domain CRUD plus automatic Let's Encrypt SSL; wildcard subdomains require Vercel nameservers. See [vercel-domains.md](references/vercel-domains.md).77- Custom domains shift reputation to the tenant and create natural user segments (casual on platform domain, serious on their own).78- `robots.txt`, `sitemap.xml`, `llms.txt` must vary by tenant; never serve from `/public`. Cloudflare: generate in the tenant Worker. Vercel: route handlers under the domain segment (see [vercel-platform.md](references/vercel-platform.md)).79808. Surface limits as plans81- Map platform limits to pricing tiers; expose in API and UI.82- No long jobs in requests; use queues or workflows.83- See [limits-and-quotas.md](references/limits-and-quotas.md); re-check official docs before final architecture or pricing decisions.84859. Make the API the product86- Everything works over HTTP; the UI is for ops, incidents, billing.87- Platform logic stays in the routing layer (dispatch Worker or Middleware); tenant content serves requests.88- If it only works in the UI, the platform is leaking.899010. Extend without breaking boundaries91- Add queues, workflows, or containers as optional modes. Keep routing explicit and isolation intact.9293## Gotchas9495- Tenant headers go on the Middleware request, not the response: `headers()` in Server Components reads forwarded request headers, so use `NextResponse.next({ request: { headers } })` or the tenant id never arrives.96- Don't start path-based if custom domains are on the roadmap: migrating later means URL rewrites, cookie changes, and DNS migration.97- Never share DB connections across tenants without RLS or `tenant_id` scoping: one missing WHERE clause leaks another tenant's data.98- Never block `/.well-known/acme-challenge/*` with Middleware or redirects: Let's Encrypt HTTP-01 validation fails and custom-domain SSL never issues.99- Edge Config writes are not instant: propagation takes up to 10 seconds, so a "domain connected" UI reading Edge Config immediately shows stale state.100101## Output schema102103```markdown104# Multi-tenant architecture105106## Platform decision107- Platform: Cloudflare | Vercel108- Why this platform:109- Rejected platform and reason:110111## Domain map112- Brand domain:113- Tenant domain:114- Tenant subdomains:115- Custom domains:116- PSL decision: Submit | No PSL117- PSL owner/timeline or No PSL reason:118119## Routing matrix120| Host pattern | Resolver | Destination | Unknown tenant behavior |121|---|---|---|---|122123## Tenant context flow124- Authority: Middleware | platform Worker125- Propagation:126- Server read path:127- Database/API scoping:128129## Isolation model130- Compute isolation:131- Data isolation:132- Config/binding isolation:133134## Custom-domain lifecycle1351. DNS target:1362. Ownership verification:1373. Certificate provisioning:1384. Routing activation:1395. Removal/failure path:140141## Limits-to-plan table142| Limit | Source URL/date | Free | Pro | Enterprise | Enforcement point |143|---|---|---:|---:|---:|---|144145## Validation evidence146| Check | Command/source | Expected result | Result |147|---|---|---|---|148```149150## Pre-commit checklist151152- [ ] Platform chosen with documented rationale153- [ ] Tenant workloads off the brand domain; PSL decision and timeline set154- [ ] Tenant identification strategy chosen; custom-domain upgrade path defined155- [ ] Isolation model defined: per-tenant Workers (Cloudflare) or shared-app plus RLS (Vercel)156- [ ] Routing authoritative and tenant-blind; dispatch or Middleware handles all traffic157- [ ] Tenant context flows through Middleware/platform Worker only; no client-supplied identity trusted158- [ ] Custom-domain onboarding defined: DNS target, verification, cert provisioning159- [ ] Per-tenant static files (robots.txt, sitemap.xml, llms.txt) served dynamically160- [ ] Limits tied to billing; API parity with UI161- [ ] Limits snapshot refreshed from official docs and dated in planning notes162163Evidence commands (run or mark N/A):164165| Check | Evidence |166|---|---|167| Tenant context exists at the boundary | `rg "x-tenant-id|tenant_id|tenantId|CREATE POLICY|USING \\(" .` |168| Tenant routing works | `curl -sI -H "Host: tenant.example.com" <local-or-preview-url>` |169| Per-tenant static files are dynamic | `curl -s -H "Host: tenant.example.com" <url>/robots.txt` and `curl -s -H "Host: tenant.example.com" <url>/sitemap.xml` |170| Custom-domain verification path exists | API route, SDK call, or platform config path in the plan |171| Platform limits are up to date | official Cloudflare/Vercel URLs with access date in the Limits-to-plan table |172173## Related skills174175- `define-architecture`: folder structure, module contracts, and middleware pipelines for the application itself.176- `scaffold-nextjs`: bootstrap the Next.js turborepo before applying these tenancy patterns.177- `optimise-seo`: per-tenant sitemaps, canonical URLs, and structured data once routing works.