The Database Is the Backend: Multi-Tenant Isolation Without a Server
Four products, forty client organisations, one shared set of tables and no application server. What makes that safe, and the three ways we have seen it go wrong.
The conventional shape of multi-tenant software is a server in the middle: the client asks, the server checks who is asking, the server queries the database. That middle layer exists almost entirely to answer one question — is this person allowed to see this row?
Modern Postgres answers that question itself. Row-level security attaches a policy to a table, and every query is filtered by that policy regardless of where it came from. If the policy is right, a client with a stolen token still sees only their own rows. The middle layer becomes optional.
We have run production systems on that premise for several years across four products. It works, and it is not free.
The shape
Every tenant-owned table carries a tenant identifier. A policy restricts each operation to rows whose tenant matches the authenticated user's membership. The client application connects directly with the anonymous key, which is public by design and grants nothing on its own — the policy, not the key, decides what is visible.
The mental adjustment this requires is the whole thing: the client is untrusted and always was. Hiding a button has never been security; it merely made insecure systems look secure. Removing the server layer does not make you less safe, it makes the existing dependency on the database explicit.
What we got
No server to patch, scale, monitor or pay for. No API layer whose only content is authorization checks duplicated from rules already present in the schema. And a single place to audit: when someone asks "who can see this data?", the answer is a policy you can read, not a code path you have to trace through three files.
That last point is the underrated one. Authorization spread across an application layer drifts. Authorization expressed as policies next to the data does not, because there is nowhere else for it to live.
Three ways it goes wrong
- A table ships without a policy. This is the failure that actually happens, and it is silent — the table simply behaves like every other table until someone notices. The mitigation is procedural rather than clever: enabling RLS is part of creating a table, never a follow-up task, and a periodic check for tables with security disabled belongs in your routine.
- The bootstrap gap. Policies that depend on a membership row cannot create the first membership row: the user has no membership yet, so the policy denies the insert that would create it. Every new tenant hits this on their first action. It needs an explicit path — a trigger or a privileged function — and it is worth solving before the first customer rather than during their onboarding.
- Policy complexity outrunning comprehension. Policies that call functions that query other tables with their own policies become genuinely difficult to reason about, and performance degrades in ways that are hard to attribute. When a policy stops being readable in one screen, that is the signal you have exceeded what this approach should carry.
What it cannot do
Anything that must not be visible to the client at all. Third-party API keys, payment provider secrets, business logic that constitutes intellectual property — none of that can live in a file the user downloads. Those need a server-side function, and most platforms provide one. The architecture is not "no server ever"; it is "no server for authorization", which is the part that was consuming the effort.
It also does not remove the need to think. A policy is code with security consequences, written in SQL, and a wrong one fails as badly as a wrong middleware check. The gain is concentration — one place to get right instead of many — not the elimination of the problem.
When to reach for a server anyway
If authorization depends on state the database does not hold, if you need rate limiting and abuse control that policies cannot express, or if compliance requires an audit trail the client cannot influence — put a server in. Those are real requirements and this architecture does not meet them.
What is not a reason: the belief that a server is what makes software professional. For the products we run, the database enforcing its own rules has been more reliable than an application layer restating them, precisely because there is only one copy of the truth.
References & notes
- PostgreSQL and Supabase documentation on row-level security.
- The three failure modes described here are drawn from our own production incidents.
Corrections are published inline and dated. Write to us if something here is wrong.
// weekly dispatch
One email. Every Tuesday.
The week's analysis, one tool we actually tested, and one behavioural pattern worth practising. Unsubscribe in one click.