Multi-tenancy
Multi-tenancy across stacks: how each framework isolates a tenant
Every B2B framework will happily build you a multi-tenant application. They differ almost entirely in where the isolation is enforced, and that single fact determines what happens the day somebody forgets a filter.
This is a routing page, not a tutorial. It exists so you can find the question you actually have — which tenancy model to pick,how to model orgs and members,how RLS actually works, orwhere a role should live.
Where each stack enforces isolation
| Stack | Isolation is enforced by | And it fails when |
|---|---|---|
| Django | App-level filtering — a queryset filter, or a tenant package that adds one | A queryset is built outside the shared helper and the filter is forgotten |
| Rails | App-level — a current-tenant scope set per request, usually via a gem | A background job, console task or API path runs without a request to set the scope |
| Laravel | App-level — a global scope plus middleware that binds the tenant | A queued job resolves the model without the middleware that would have scoped it |
| Next.js | App-level — middleware resolves the tenant, the data layer scopes every query | A server action or route handler queries without the tenant the middleware set |
| NestJS | App-level — request-scoped provider plus a guard that every route must pass | A route is added without the guard; nothing in the type system objects |
| Go | App-level — tenant identity carried on context.Context | A goroutine or background worker is started without the parent context, silently defaulting |
| Phoenix | App-level — an Ecto scope or prefix-based tenancy bound in a plug | A PubSub or Task starts outside the request that established the prefix |
| Postgres RLS | Database-enforced — a policy keyed on a session variable the app sets | It does not fail. That is the entire point — see below |
Read the last two columns together. Seven of those eight rows are enforced byapplication code, which means the guarantee is only as strong as the most recently added code path. A new route, a background job, or an admin script is perfectly correct-looking and perfectly unprotected.
The one mechanism that is not a discipline
Postgres row-level security moves the guarantee out of your application and into the database, where a forgotten WHERE org_id = ? does not return another tenant's rows — it returns an error, or nothing. The application cannot forget its way past a policy.
This is worth separating from the framework debate, because RLS is not a framework feature at all. It works identically behind Django, Rails, Next.js, NestJS, Go or SvelteKit, because the policy lives in Postgres and the app only sets a session variable. Whatever you build on, you can adopt it.
The catch is that pooled connections break it quietly unless you are careful — which is the subject ofour page on RLS with a connection pool.
What we actually ship, and what we do not
We build SvelteKit starters. Three of them, over SQLite, Supabase and Postgres, with the same orgs → members → invites → RBAC → billing → audit service layer in each. Everything above about other frameworks is publicly documented behaviour, and we have not run any of them in our test suite — so if you are in one of those stacks, this page tells you where your isolation lives, and our kit is not the answer to your question.
If you are on any stack that talks to Postgres and want the database to enforce the guarantee, the Postgres starter is vendor-neutral and the RLS layer is the part worth reading. If you are choosing a database first, start withSQLite vs Postgres for a multi-tenant SaaS.
Get in touch
Questions about the product, team licenses, or anything else? We'll aim to respond within 48 hours.