Multi-Tenant Model
Governed distribution, file-based serving namespaces, per-tenant {ns}-brand-system instances of one contract, and cross-tenant isolation.
Multi-Tenant Model
The Registry hosts multiple brands — internal Vandoko brands and Vandoko Agency customers — each with its own profile, namespace, components, assets, and isolation boundary. This is governed distribution, not multi-tenant SaaS: Vandoko Agency curates each tenant's brand system; the Registry distributes it under that tenant's namespace, gated to that tenant (ADR 0002 §1–§5).
Content model: file-based per-namespace
Each serving namespace owns a directory under registries/:
registries/
vandoko/ # instance #1
registry.json # this tenant's shadcn manifest ("name": "vandoko")
brand-system.css # the {ns}-brand-system token instance
blocks/ ui/ ... # component sources
r/ # build output — served by the dynamic handler, NOT in public/
versions/{date}/ # immutable brand-package snapshots
{customer-slug}/
registry.json # "name": "{customer-slug}"
brand-system.css
blocks/ ...
r/Everything is git-governed and reviewable. Supabase is used only for discovery and gating indexes (the namespaces table with a kind='serving' discriminator, and the api_keys table) — never as the component source of truth (ADR 0002 §2, §5; registries/README.md).
Per-brand token delivery
Each tenant ships a registry:base item named {ns}-brand-system whose single file is registries/{ns}/brand-system.css, a complete instance of the token contract. The contract is the variable names + structure; the instance is one brand's values. A build fails if an instance omits a contract token or adds an off-contract one (pnpm brand:lint).
Vandoko's instance is vandoko-brand-system — the canonical
contract-bearing registry:base (instance #1). It supersedes
vandoko-theme via alias; both remain
resolvable and installable (they ship the same CSS). The canonical
contract lives in lib/brand-contract-tokens.json.
Namespace-aware serving
| Path | Serves | Notes |
|---|---|---|
/r/{ns}/{name}.json | registries/{ns}/r/{name}.json | Dynamic handler, tenant-gated per request |
/r/{name}.json (legacy) | registries/vandoko/r/{name}.json | Implicit ns=vandoko alias; byte-identical to the namespaced path |
Per-namespace JSON lives outside public/ and is read by a dynamic route handler that enforces the tenant check before reading the file. Static files cannot be tenant-filtered per request, so hard isolation requires the dynamic gate. The build runs once per namespace via pnpm registry:build:all:
pnpm registry:build:all # shadcn build registries/{ns}/registry.json --output registries/{ns}/rIsolation & auth
Isolation is single-Clerk-org with a custom api_keys Supabase table carrying two scopes (ADR 0002 §2):
scope='namespace'— a dev/CI key for one tenant's items (least privilege). A cross-namespace request gets a JSON403.scope='platform'— the Portal server's key (and other trusted platform systems); reads anykind='serving'namespace.
The cross-tenant guarantee: a key for tenant A can never fetch tenant B's items — enforced in middleware (load-bearing) and mirrored in the route handler (defense-in-depth). Legacy Clerk-issued keys are bridged as implicitly ns=vandoko during the transition. For the full behavior matrix, status codes, and the ≤10s revocation SLA, see the Registry Auth & Cache Contract (docs/registry-auth-contract.md); a consumer-facing summary is in Consuming the Brand System.
Customers never hold registry keys. Keys belong to Vandoko-controlled systems. "Isolation" here means bounding the blast radius of our own credentials, not defending against hostile customer-vs-customer access.
Per-tenant assets
Brand assets are scoped by brand_guidelines.namespace_id and physically partitioned in Supabase Storage ({ns}/{folder}/{file}). Customer assets are private, served via short-lived signed URLs; the per-version asset-manifest.json carries the signed-URL policy (IDs, versions, alt text, usage rules). A leaked URL cannot expose another tenant's assets (ADR 0002 §4).
Onboarding a new tenant
The end-to-end runbook (create serving namespace → scaffold the tree → author the brand profile → add scoped components → build → upload assets → issue a scoped key → hand off consumer config → verify isolation → publish the first version snapshot → record lineage) is the detailed top-level "Customer-onboarding runbook" section near the end of ADR 0002 (the 11-step ordered checklist — not the brief §6 subsection of the same name).