What is Row-Level Security (RLS)?

Last updated: July 2026

Row-level security is a database mechanism that filters which rows each user can see or change, enforced by policies on every query. The mental model is an enforced, invisible WHERE clause: the condition you’d otherwise have to remember in every query becomes a property of the table itself — applied by the engine, on every access path, including the ones your application code never sees.

Key takeaways

QuestionAnswer
What it isPer-row access control, enforced by the database engine itself
The mental modelA WHERE clause that cannot be forgotten
Killer use caseMulti-tenant isolation and per-user data, at the layer breaches can’t skip
The fine printBypass roles, default-deny, pooling context — the gotchas are operational
The failure it preventsOne missed filter in application code = cross-tenant breach

RLS in ten lines of SQL

CREATE TABLE invoices (
  tenant_id uuid NOT NULL,
  total     numeric(10,2)
);

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;   -- from here: default-deny

CREATE POLICY tenant_isolation ON invoices
  USING      (tenant_id = current_setting('app.tenant')::uuid)   -- what you can read
  WITH CHECK (tenant_id = current_setting('app.tenant')::uuid);  -- what you can write

-- Every query now behaves as if it ended with WHERE tenant_id = <yours>.
-- A query that forgets its filter returns nothing — not everything.

The same guarantee exists in document databases as access control on the row itself — each object carries its ACL, and the platform enforces it on every request:

// JavaScript / Node.js — Back4app JS SDK
// Row-level security as data: this row is readable by its owner, period
const note = new Parse.Object('Note');
note.set('text', 'Q3 salary planning');
note.setACL(new Parse.ACL(Parse.User.current())); // owner-only, on the row itself
await note.save();

// Later, any query by any user — the row exists only for its owner
const notes = await new Parse.Query('Note').find(); // others never see it

What the engine does with a query

How row-level security filters a queryTwo different users run the same query against one table; the engine applies the security policy predicate per row before user conditions, so each receives only the rows their policy allows.

SELECT * FROM invoices
(same query, any user)

Policy predicate
applied per row, first

Tenant A sees
only tenant A rows

Tenant B sees
only tenant B rows

Two different users run the same query against one table; the engine applies the security policy predicate per row before user conditions, so each receives only the rows their policy allows.

USING vs. WITH CHECK, permissive vs. restrictive

ConceptGovernsRule of thumb
USINGWhat exists for you: reads, and rows eligible for update/delete”Can I see it?”
WITH CHECKWhat you may write: inserts, and post-update values”Can I create it this way?”
Permissive policies (default)OR-ed together — any match grantsGrant access
Restrictive policiesAND-ed on top — all must passImpose mandatory limits

Two composition rules worth memorizing from the PostgreSQL reference: omitting WITH CHECK reuses USING for writes, and at least one permissive policy must pass before restrictive ones are even consulted — restrictive-only means nobody gets in.

Where RLS is supported

Engine familyMechanism
PostgreSQL (9.5+) and derivativesCREATE POLICY — the reference implementation
SQL Server (2016+)Security policies over inline predicate functions (filter + block)
Distributed SQL (CockroachDB, YugabyteDB)PostgreSQL-compatible policies
Cloud data warehousesRow access policies, vendor-flavored
Document databasesNot policy-based — per-object ACLs enforced by the platform layer

The last row is the one this glossary cares about: on document stores the row-level boundary is typically data on the row (ACLs) rather than a predicate in the engine — same guarantee, different mechanism, detailed in the Back4app engineering write-up.

The gotchas, finally in one place

  • Default-deny surprises. Enabling RLS with no policy blocks everyone except the owner — half the “RLS broke production” stories are this.
  • The bypass matrix. Superusers, BYPASSRLS roles, and table owners skip policies — set FORCE ROW LEVEL SECURITY if the owner is also the application’s user, and audit who holds what.
  • Views evaluate as their owner by default — a view over an RLS table can silently bypass it unless created with invoker rights (security_invoker in PostgreSQL).
  • Constraints leak existence. A unique or foreign-key violation can reveal that an invisible row exists — the documented covert channel; treat uniqueness on protected values accordingly.
  • Errors can leak values. Crafted expressions that fail on specific data (a division by zero when a hidden value matches) exfiltrate through error messages — the side-channel class the SQL Server docs describe.
  • Dumps and pools have modes. Backup tooling may need row security off to export everything; transaction-mode poolers need transaction-scoped context (SET LOCAL) or tenants bleed between requests.

Performance: policies are code on the hot path

Three rules cover most of the field experience: index every column a policy references (the predicate runs before your query’s own filters — unindexed, it’s a scan); keep predicates join-free, pushing lookups into functions or role checks; and make per-request functions evaluate once per query, not once per row, by wrapping them in a scalar subquery. Measured well, RLS costs single-digit percent; measured badly, it’s the mystery slowdown on every table you secured.

Common use cases

  • Multi-tenant SaaS. The canonical case — tenant isolation enforced beneath the application, where a forgotten filter can’t become a breach.
  • Per-user data. Messages, documents, health records: users see their rows, full stop.
  • Departmental and regional boundaries. Sales sees its region; auditors see everything, read-only — policy layering at work.
  • Compliance regimes. Access control demonstrable at the data layer, which is where auditors like it.
  • Shared analytical access. Analysts query production replicas directly, seeing only what their role permits — safe because the database enforces it, not the dashboard.

Should you enforce at the row level? A decision matrix

Enforce with RLS/ACLs when…Application filtering may suffice when…
Multiple tenants or users share tablesThe database has exactly one trusted caller
Anything besides the app touches the databaseNo BI tools, no admin SQL, no second service
A missed filter means breach, not bugScoping is convenience, not security
Compliance wants data-layer proofThe data is not sensitive
You want the boundary tested once, centrallyYou enjoy auditing every query forever

The honest synthesis of the two columns: keep scoping in the application for readability — and enforce it in the database anyway. Defense in depth is the entire point.

Limitations and trade-offs

  • Policies are invisible by design — which makes debugging “where did my rows go?” a genuine skill; log the session context first.
  • Complex authorization outgrows predicates. Rules that need workflow state or cross-entity logic belong in application authorization layers, with RLS as the backstop.
  • Operational surface is real. The bypass matrix, dump modes, and pooling context are ongoing operational knowledge, not one-time setup.
  • Per-row evaluation is a tax — small when engineered, unbounded when not.
  • It secures rows, not columns. Hiding fields is column-level security; combine both for cell-level effect.

Row-level security on Back4app

Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. Its row-level model is the ACL-as-data mechanism from the tabs above: every object carries its access list, roles express tenants and teams, and the platform enforces both on every request — SDKs, REST, GraphQL, and the dashboard included, with class-level permissions as the restrictive layer on top. It’s the enforced-WHERE-clause guarantee, delivered on a document database, with the gotcha list above absorbed by the platform rather than assigned to you.

Frequently asked questions

What is row-level security in simple terms?

An enforced, invisible WHERE clause. You attach policies to a table, and the database applies their conditions to every query automatically — each user sees and modifies only the rows the policy allows, no matter what query they write or which tool they use. The filter lives in the database, so no application code path can forget it.

How does row-level security work?

You enable RLS on a table and create policies containing boolean predicates — typically comparing a row's owner or tenant column against the current user or a session variable. At query time the engine evaluates the predicate per row before the user's own conditions run, filtering reads and blocking disallowed writes. Once RLS is enabled with no policy, the default is deny-all.

What is the difference between USING and WITH CHECK?

Direction. USING filters what exists for you: rows visible to SELECT and eligible for UPDATE or DELETE. WITH CHECK validates what you write: rows being inserted, and the new values after an update. Omit WITH CHECK and the USING predicate applies to both — but tables where users may read broadly and write narrowly need both, set differently.

Who can bypass row-level security?

In PostgreSQL: superusers, roles granted BYPASSRLS, and — the one everyone forgets — the table owner, unless you set FORCE ROW LEVEL SECURITY. Auditing that bypass matrix is part of deploying RLS; a perfect policy protects nothing if the application connects as the table owner.

Does row-level security hurt performance?

It can — the policy predicate is evaluated against candidate rows on every query. The mitigations are consistent across production experience: index the columns your policies reference, keep predicates free of joins (use functions or lookup roles instead), and wrap per-request functions so they are evaluated once per query rather than once per row. A policy on an unindexed column is a table scan with a security badge.

What is the difference between RLS and application-level filtering?

Where the boundary lives. Application filtering scopes queries in code — flexible, but duplicated across every code path and bypassed by anything that talks to the database directly. RLS enforces in the engine, covering every access path including admin tools and other services. The consensus is defense in depth: scope in the application for clarity, enforce in the database for safety.

How does RLS work with connection pooling?

Carefully. Pooled applications connect as one database user, so policies key off a per-request session variable instead of the connection's identity — set after checkout, read by the policy, reset on return. With transaction-mode poolers the setting must be transaction-scoped, or one tenant's context can leak into the next request on the same connection. This interaction is the most common production RLS bug.

How do you test row-level security policies?

Impersonate each role and run all four operations — select, insert, update, delete — verifying both what appears and what is refused. Add the two forgotten cases: the unset-context case (no tenant variable set should mean no rows, not all rows) and the bypass matrix (owner, superuser, replication paths). RLS earns its trust through adversarial tests, not through the policy reading well.

Related terms

Compare with

Further reading

Ready to build your backend?

Start your project on Back4app in minutes — database, auth, APIs, and Cloud Code included. No credit card required.

Written and reviewed by Back4app Engineering, Back4app Engineering · Published 2026-07-25