What Is Row Level Security, and When Do You Need It?

Application code decides who sees what until someone writes a query that forgets to ask. Row level security moves the decision somewhere it cannot be skipped.

What Is Row Level Security, and When Do You Need It? — Troiana insight cover

In short

Row level security enforces access rules in the database, so a query returns only rows the current user may see regardless of how the query was written. It matters because application-level filtering depends on every query remembering to filter — and the one that forgets is a data leak. It is essential when clients query the database directly, and valuable defence in depth otherwise.

The problem it solves

In a multi-tenant application, most tables hold rows belonging to different customers. Every query must return only the current customer's rows.

The usual approach is a WHERE clause on every query. It works, and it depends on every query — including the one written at speed on a Friday, the one in a background job, the one in an admin script, and the one added by someone unfamiliar with the convention — remembering to include it.

One forgotten clause is a data leak across customers, and it is a category of bug that reviews miss because the query looks correct in isolation.

Row level security moves the rule into the database. Policies attached to a table are applied to every query automatically, so a query that forgets to filter still returns only permitted rows.

How it works

You enable RLS on a table, then define policies describing which rows are visible or modifiable for the current context.

The context typically comes from a session variable your application sets after authenticating — the current user or tenant identifier. The policy references that variable, so the database can evaluate access per row.

Once enabled, the default is deny. Without a matching policy, no rows are returned, which is the correct default and occasionally a surprise during setup.

Policies can differ by operation, so read access and write access are separate decisions.

When you genuinely need it

When clients query the database directly. Platforms exposing the database to browsers or mobile apps make RLS the only protection — application-side filtering does not exist in that architecture. This is not optional; it is the security model.

When several applications share a database. An admin tool, a background worker and a public API each implementing their own filtering is three chances to get it wrong.

When the consequence of a leak is severe. Health, financial, or client-confidential data justifies enforcement that does not depend on discipline.

When people run ad-hoc queries against production. Support and analytics access is a common route to accidental exposure.

When application filtering is reasonable

Be honest that RLS is not always the right answer.

A single application, a small team, consistent query patterns through one data layer, and no direct database exposure — application-level filtering is workable, and RLS adds complexity for defence in depth rather than for a gap.

The strongest middle position: enforce tenancy in one place in your data layer so individual queries cannot forget, and add RLS when the architecture or the data justifies it.

The costs

Performance. Policies are evaluated per row, so a poorly written one is a query slowdown across the entire application. Keep policies simple and ensure the columns they reference are indexed — a policy filtering on an unindexed tenant column is the usual cause of a sudden slowdown after enabling it.

Debugging becomes less obvious. A query returning nothing may be correct, or may indicate the session context was not set. That confusion is common early, and the answer is almost always a missing context variable.

Migrations need care. A migration running without the right context may see no rows, or fail. Data-modifying migrations often need to run with a role that bypasses policies, deliberately and visibly.

Superuser roles bypass it. Anything connecting as an owner or superuser ignores policies entirely, so a background job using the wrong credentials silently loses the protection.

Testing that policies actually hold

This is where teams stop short, and an untested policy is an assumption.

Write tests that attempt cross-tenant access — set the context to one tenant, query for another tenant's row, and assert nothing comes back. One test per table, mechanical to write, and it catches the case that matters.

Test writes as well as reads. A policy permitting reads correctly may allow an update it should not, since those are separate policies.

Test with no context set, which should return nothing rather than everything.

Include new tables in the check. The realistic failure mode is not a broken policy; it is a table added later with RLS never enabled. A test enumerating tables and asserting RLS is on catches that permanently, and it is worth more than any individual policy test.

Getting it right

Enable it on every table holding tenant data. Keep policies simple and index what they reference. Set the session context in one place your application cannot forget. Use a separate role for migrations and jobs that need to bypass. Test cross-tenant access per table, and assert that every table has it enabled.

That last assertion is the one that keeps working as the schema grows, which is when the risk actually appears — the same reasoning that applies to any schema decision that gets harder to change later.

If you are building multi-tenant and want the isolation reviewed before it holds real client data, book a call.

Common questions

What is row level security?

A database feature that enforces access rules per row, so a query returns only rows the current user is permitted to see regardless of how it was written. Policies attached to a table are applied automatically, which means a query that forgets to filter by tenant still returns only permitted rows.

Why isn't filtering in application code enough?

Because it depends on every query remembering to filter — including background jobs, admin scripts, ad-hoc analytics queries, and code written by someone unfamiliar with the convention. One forgotten clause is a cross-tenant data leak, and it is a bug reviews miss because the query looks correct in isolation.

When do you definitely need row level security?

When clients query the database directly, since application-side filtering does not exist in that architecture and RLS is the security model. Also when several applications share a database, when the consequence of a leak is severe, and when people run ad-hoc queries against production.

Does row level security slow queries down?

It can, since policies are evaluated per row. Keep policies simple and make sure the columns they reference are indexed — a policy filtering on an unindexed tenant column is the usual cause of a sudden slowdown after enabling it.

How do you test that RLS policies work?

Write tests that set the context to one tenant, query for another tenant's rows, and assert nothing returns — for reads and writes separately, since those are different policies. Most importantly, add a test that enumerates tables and asserts RLS is enabled, because the realistic failure is a new table where nobody turned it on.

Have something worth building right?