Row-level security is the cheapest multi-tenancy you will ever buy
Every multi-tenant conversation starts in the same place: separate databases, separate schemas,
or one shared set of tables with a tenant_id column. The first two feel safer. For a solo
founder they are usually a trap, because they convert an engineering problem into an operations
problem, and operations is the thing you do not have people for.
The failure mode of the shared-table approach
The reason people distrust a shared table is not the schema. It is that isolation depends on
every query, forever, including the one written at midnight by a version of you who is tired.
One missing WHERE tenant_id = $1 and you have shipped a data breach.
Row-level security removes that class of bug by moving the check below the query. You set the tenant on the connection, Postgres appends the predicate itself, and a forgotten clause returns nothing instead of returning everything.
What it actually costs
Three things, in my experience:
- Connection discipline. The tenant has to be set on every checked-out connection, which means your pooler needs to be in transaction mode and your framework needs a hook it cannot skip. This is the part people get wrong.
- Index shape. Your indexes now want the tenant column first. Retrofitting that on a large table is a maintenance window.
- Debuggability. A superuser bypass role for support work, used deliberately and logged, or you will spend an afternoon confused about why the row you can see in one session does not exist in another.
None of those are free. All of them are cheaper than running fifty databases and their fifty migrations.
Where it stops being right
When a tenant is large enough to need its own performance envelope, or contractually needs its own encryption boundary, RLS stops being the answer and you graduate to a dedicated instance. That is a good problem. Build for the ninety-five tenants who will never get there.