PostgreSQL

Row-Level Security in PostgreSQL: Better Than Oracle VPD

Rakesh Mamidala·Founder & Lead Engineer··7 min read

VPD, Without the PL/SQL

Oracle’s Virtual Private Database enforces row-level access by attaching a PL/SQL policy function that returns a predicate string, which Oracle appends to every query. It works — but it’s a body of procedural code to write, test and maintain separately from the schema. PostgreSQL’s Row-Level Security is the same idea, done declaratively and in-core: the rule lives on the table, as SQL.

Enable, then write policies

sql
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

-- what rows this role can SEE
CREATE POLICY tenant_read ON invoices
  USING (tenant_id = current_setting('app.tenant_id')::int);

-- what rows this role can WRITE (INSERT/UPDATE must satisfy this)
CREATE POLICY tenant_write ON invoices
  FOR ALL
  USING      (tenant_id = current_setting('app.tenant_id')::int)
  WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);

USING filters what’s visible; WITH CHECK constrains what can be written (so a tenant can’t insert a row belonging to someone else). Tenant context comes from current_setting() — the RLS equivalent of Oracle’s SYS_CONTEXT — set per session or per transaction by your app or pooler.

The footgun: owners and superusers bypass RLS

By default, the table owner and any superuser skip RLS entirely. That’s a nasty surprise for a multi-tenant table whose owner is also the app role. Force it:

sql
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;

Permissive vs restrictive

Multiple permissive policies combine with OR (any one passing grants access); restrictive policies combine with AND (all must pass). Start with a permissive tenant policy, then layer restrictive ones for extra guards (e.g. “and only non-archived rows”).

Why it beats VPD for migrations

It’s declarative and transparent — the access rule is visible in the schema, not buried in a policy function. There’s no dynamic predicate assembly to audit, it migrates with the table instead of as a separate codebase, and the planner sees the predicate directly. VPD policies map cleanly onto CREATE POLICY statements — usually with far less code.

Carry your access model across

DBMigrateAIPro flags Oracle VPD policies during assessment and maps them to PostgreSQL RLS policies, so row-level security survives the migration instead of getting rebuilt from scratch. Free for Year 1.