Recipe: Multi-Tenant SaaS Skeleton
This recipe lays out the bones of a multi-tenant SaaS: a parent circle that owns the product, a circle-ref per customer pointing at their isolated sub-circle, an auth-policy governing who gets in, and an api-token per tenant for programmatic access. Each customer gets a sovereign namespace; you get one product to run.
The problem it solves
Multi-tenancy is where SaaS projects get dangerous: one customer’s data leaking into another’s is an existential bug, and bolting isolation onto a shared database after the fact is painful and fragile. This recipe starts from isolation — every tenant is a separate circle with its own schema — so data separation is structural, not a WHERE tenant_id = ? clause you have to remember everywhere.
Elements
| Element | Role |
|---|---|
circle | The parent product circle, and (per tenant) an isolated sub-circle. |
circle-ref | A reference tile pointing at a tenant’s nested sub-circle. |
auth-policy | Declares authentication requirements and access rules. |
api-token | A scoped credential per tenant for programmatic access. |
Flow
- Start with your product
circle— the tenant that owns the shared product surface. - Onboard each customer as a sub-circle. The parent circle’s
add_subcircleoperation creates a new circle nested under it; because every circle carries its owncircle_{uuid}Postgres schema, that tenant’s data is physically isolated from every other tenant by construction. Acircle-reftile points at it, and itsresolveoperation returns the child’s public metadata (name, type, created_at) without entering the child’s schema. - Govern access with an
auth-policy. Itsevaluateoperation tests credentials (role, action, token) against the policy’s rules — attach it to the surfaces that need gating so the same access logic applies everywhere. - Issue per-tenant programmatic access with an
api-token:generatea scoped token, define what it can touch withscopes, androtateorrevokeit over the customer’s lifecycle. - Build the product features flat inside each tenant’s sub-circle; the parent circle holds only what is shared.
What this shows
In Triform, a tenant is a circle — its own schema, git repo, and wallet — so multi-tenant isolation is the platform’s default shape, not a discipline you impose. add_subcircle plus a circle-ref gives you a clean parent→tenant tree where the parent can see public metadata (resolve) without reaching into a child’s private data. Access and credentials are themselves elements — an auth-policy you attach and an api-token you scope per tenant — so the security model is declared and inspectable, not scattered through application code.