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.

← All writing