How to Use Row-Level Security Policies
Row-Level Security (RLS) lets PostgreSQL enforce access control at the row level, so different database roles see only the rows they are allowed to see. Use this guide when you need multi-tenant isolation or per-role data filtering enforced at the database layer rather than in application code.
When
When you need multi-tenant isolation or row-level access control at the database level.
Steps
1. Enable RLS on a table
In your schema definition (ModelIR):
typescript
const schema = {
tables: {
tenants: {
columns: {
id: { type: 'uuid' },
tenant_id: { type: 'uuid' },
name: { type: 'text' },
},
rlsEnabled: true,
policies: [
{
name: 'tenant_isolation',
command: 'ALL',
roles: ['app_user'],
using: "tenant_id = current_setting('app.current_tenant')::uuid",
withCheck: "tenant_id = current_setting('app.current_tenant')::uuid",
},
],
},
},
};2. Generated SQL
sql
ALTER TABLE "tenants" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON "tenants"
FOR ALL
TO app_user
AS PERMISSIVE
USING (tenant_id = current_setting('app.current_tenant')::uuid)
WITH CHECK (tenant_id = current_setting('app.current_tenant')::uuid);3. Schema migrations
compareSchemata() detects:
- RLS enabled/disabled changes →
ALTER TABLE ENABLE/DISABLE ROW LEVEL SECURITY - New policies →
CREATE POLICY - Removed policies →
DROP POLICY - Changed policies →
DROP + CREATE(policies are replaced, not altered)
4. Multi-dialect support
RLS is gated by supportsDDLRowLevelSecurity capability flag:
| Adapter | Supported |
|---|---|
| PostgreSQL | Yes |
| MySQL | No (policies silently skipped) |
| SQLite | No (policies silently skipped) |
| DuckDB | No (policies silently skipped) |
Key files
packages/types/src/model-ir.ts—PolicyIRinterfacepackages/adapter-pgsql/src/ddl/ddl-generator.ts— DDL generationpackages/adapter-pgsql/src/ddl/schema-diff.ts— Schema comparisonpackages/adapter-pgsql/src/introspection.ts— Read existing policies frompg_policy
Gotchas
usingandwithCheckare raw SQL strings — no parameter binding, no validation- Policy names must be unique per table
PERMISSIVEis the default; setpermissive: falseforRESTRICTIVEpolicies- Introspection reads from
pg_policycatalog — requires appropriate privileges - Phase ordering: RLS enable (phase 17) runs before policy creation (phase 18) — never reversed
- Changed policies are replaced (DROP + CREATE), not altered — no
ALTER POLICYis emitted