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

StackIsolation is enforced byAnd it fails when
DjangoApp-level filtering — a queryset filter, or a tenant package that adds oneA queryset is built outside the shared helper and the filter is forgotten
RailsApp-level — a current-tenant scope set per request, usually via a gemA background job, console task or API path runs without a request to set the scope
LaravelApp-level — a global scope plus middleware that binds the tenantA queued job resolves the model without the middleware that would have scoped it
Next.jsApp-level — middleware resolves the tenant, the data layer scopes every queryA server action or route handler queries without the tenant the middleware set
NestJSApp-level — request-scoped provider plus a guard that every route must passA route is added without the guard; nothing in the type system objects
GoApp-level — tenant identity carried on context.ContextA goroutine or background worker is started without the parent context, silently defaulting
PhoenixApp-level — an Ecto scope or prefix-based tenancy bound in a plugA PubSub or Task starts outside the request that established the prefix
Postgres RLSDatabase-enforced — a policy keyed on a session variable the app setsIt 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.

Max 2000 characters

Stored in our own database — no third party. Deleted on request.