Articles

What Is Row-Level Security (RLS)? Database Access Control

Row-level security lets a database restrict which rows a query can see or modify, per user, enforced at the engine — not the application layer.

Chisato Chisato · · 5 min read
Light trails resembling data streams flowing past a lock

Row-level security (RLS) is a database feature that restricts which rows a query can read or modify based on who’s running it, enforced inside the database engine rather than in application code. Instead of every query filtering by WHERE tenant_id = ? and trusting the application to get it right every time, the database itself refuses to return or touch rows a policy doesn’t allow — even if the query never mentions the restriction at all.

The problem it solves

Most multi-tenant or multi-user applications need to make sure one user’s queries can’t see another user’s data. The conventional approach bakes that check into every query: application code adds a WHERE clause, an ORM scopes queries automatically, or a middle tier filters results before returning them. This works until it doesn’t — a forgotten filter in one endpoint, a raw query written for a one-off script, or a background job that skips the application layer entirely, and rows leak across tenants.

RLS moves the check to the last line of defense: the database. A policy is attached to a table, and the engine applies it to every query against that table, regardless of which application, script, or admin tool issued it. If the policy says a row belongs to tenant_id = current_tenant(), no query — written correctly or not — can return a row outside that scope.

How it works

RLS is defined as policies attached to a table, each with a condition that must evaluate true for a row to be visible (or writable). In PostgreSQL, the pattern looks roughly like this:

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

Once enabled, every SELECT, UPDATE, and DELETE against orders is silently rewritten to include that condition. A query with no WHERE clause at all — SELECT * FROM orders — returns only the rows the policy permits. Separate USING clauses control what’s visible for reads, and WITH CHECK clauses control what new or modified rows are allowed to satisfy, so you can allow a user to see rows they can’t create, or vice versa.

Policies can be layered: a table can have multiple policies for different roles or operations, combined with AND/OR depending on how they’re declared. Superusers and roles with BYPASSRLS skip the checks entirely, which matters for migrations, admin tooling, and backup jobs that legitimately need unrestricted access.

Where the tenant context comes from

The tricky part of RLS isn’t the policy syntax — it’s reliably telling the database who’s asking. A common pattern sets a session variable at the start of each connection or transaction, populated from the authenticated user’s JWT or session:

SET app.tenant_id = 'a1b2c3d4-...';

The application layer’s job shrinks from “remember to filter every query” to “set the session context correctly once per connection” — a much smaller surface to get wrong, and one that fails safe: if the variable is never set, most policies simply return zero rows rather than everything.

RLS vs application-layer filtering

Application-layer filteringRow-level security
Enforcement pointEvery query, in codeDatabase engine
Fails safe?No — a missed filter leaks dataUsually — unset context returns nothing
Covers raw SQL / admin toolsNoYes
PerformanceNo extra overheadAdds a predicate to every query
ComplexitySpread across the codebaseCentralized in schema
Best forSimple, single-tenant appsMulti-tenant SaaS, regulated data

RLS isn’t a replacement for RBAC or ABAC at the application layer — those still decide what a user is allowed to do in the product. RLS is a backstop that decides what rows a connection is allowed to touch, independent of the application logic on top. The two work well together: RBAC/ABAC governs feature access and business rules, RLS guarantees tenant isolation even when something upstream gets it wrong.

When it’s worth adopting

RLS earns its keep in multi-tenant systems where a data leak between tenants is a serious incident — SaaS platforms, healthcare and financial applications, anything with regulatory isolation requirements. It’s also useful when multiple applications or internal tools query the same PostgreSQL database directly, since a shared policy protects all of them without duplicating filter logic in each.

It’s less necessary for single-tenant applications, internal tools with a small trusted user base, or systems where SQL vs NoSQL choices already push you toward document stores that isolate tenants by database or collection rather than by row.

Costs and caveats

RLS isn’t free. Every enabled table adds a predicate to every query plan, which can defeat index usage if the policy condition isn’t itself indexed — pair tenant-scoping columns with an index, the same way you would for any Postgres index type you rely on for filtering. Policies also add a layer of logic that’s easy to forget exists: a developer debugging “missing” rows may not think to check for an active RLS policy, and complex policies with subqueries can be hard to reason about and slow to plan.

RLS also doesn’t protect against every threat. A connection with BYPASSRLS or superuser privileges skips it entirely, so it’s not a substitute for zero trust network controls or a WAF in front of the application. And because policies run inside transactions, they interact with MVCC snapshots — a row visible at the start of a transaction stays visible for its duration even if the policy condition would now exclude it, which matters for long-running transactions.

The takeaway

Row-level security pushes tenant and access isolation down into the database engine, so a policy — not a hopefully-correct WHERE clause in every code path — decides what a query can see. It fails safer than application-layer filtering, protects against raw SQL and admin-tool leaks, and is worth the modest performance and complexity cost anywhere a cross-tenant data leak would be a real incident. It complements, rather than replaces, access control decisions made higher up the stack.

Chisato Chisato · · 4 min read

OCSP vs CRL: How Certificate Revocation Works

OCSP and CRL are the two mechanisms browsers use to check if a TLS certificate has been revoked before its expiry date. Here's how each works.

#Security #Cryptography #Networking
Chisato Chisato · · 4 min read

What Is a Watering Hole Attack?

A watering hole attack compromises a site its targets already trust, then waits for victims to visit — rather than phishing them directly.

#Security #Cybersecurity #Networking