RLS gives multi-tenant isolation that a forgotten WHERE clause in application code can't break.
Row-level security (RLS) adds per-row filters enforced by PostgreSQL itself. Enable it on a table with ALTER TABLE t ENABLE ROW LEVEL SECURITY; from then on, roles subject to it see no rows until a policy allows them.
A policy has:
- a command (
FOR SELECT,INSERT,UPDATE,DELETEorALL), - target roles (
TO app), USING (expr): which existing rows are visible or can be updated/deleted,WITH CHECK (expr): which new or modified rows are allowed to be written.
Policies are combined with OR (permissive, the default) or AND (AS RESTRICTIVE).
A common multi-tenant pattern: the application sets a session variable per request, SET app.tenant_id = '42' (or set_config('app.tenant_id', '42', true) inside the transaction), and the policy compares tenant_id = current_setting('app.tenant_id')::int. With a connection pool, use the transaction-local form so a value can't leak to the next request.
Important exemptions: superusers and roles with BYPASSRLS ignore RLS, and so does the table owner unless you add FORCE ROW LEVEL SECURITY. So the application must not connect as the owner. Index the columns used in policies; the policy predicate is added to every query, and functions in it should be simple and leakproof-friendly.
Example
CREATE TABLE note (id int PRIMARY KEY, tenant_id int NOT NULL, body text);
INSERT INTO note VALUES (1, 1, 'tenant one'), (2, 2, 'tenant two');
CREATE ROLE web NOLOGIN;
GRANT SELECT, INSERT ON note TO web;
ALTER TABLE note ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON note TO web
USING (tenant_id = current_setting('app.tenant_id')::int)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);
SET ROLE web;
SET app.tenant_id = '1';
SELECT id, body FROM note; -- 1 | tenant one (only)
INSERT INTO note VALUES (3, 2, 'sneaky');
-- ERROR: new row violates row-level security policy for table "note"
RESET ROLE;Remember
- ENABLE ROW LEVEL SECURITY + policies filter rows for every query
- USING filters visible rows; WITH CHECK validates written rows
- Owners and superusers bypass RLS unless FORCE is set
Watch out for
- Testing RLS as the table owner or a superuser and concluding it works (or doesn't).