Row-Level Security
Row-Level Security (RLS) restricts which rows a given role can see or modify. When enabled on a table, every query is filtered through one or more policies that define access rules — no application-layer filtering required.
RLS is the foundation of the Browser SDK data access model: the public data API issues queries under a role resolved from the user’s JWT, and RLS policies enforce per-user visibility.
Enable RLS
Section titled “Enable RLS”-- Enable RLS (default-deny: no policies = no rows visible to non-owners)ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
-- Disable RLS (all rows visible to all roles)ALTER TABLE todos DISABLE ROW LEVEL SECURITY;Once enabled, non-superuser and non-owner roles see zero rows until you create at least one policy.
Force RLS on Table Owners
Section titled “Force RLS on Table Owners”By default, the table owner bypasses RLS. Use FORCE to apply policies to the owner as well:
ALTER TABLE todos FORCE ROW LEVEL SECURITY;
-- Remove forceALTER TABLE todos NO FORCE ROW LEVEL SECURITY;Note: Superusers always bypass RLS, even with
FORCE.
Create Policies
Section titled “Create Policies”A policy defines a boolean expression that filters rows for a specific operation. If the expression evaluates to true, the row is accessible.
CREATE POLICY policy_name ON table_name [AS { PERMISSIVE | RESTRICTIVE }] [FOR { SELECT | INSERT | UPDATE | DELETE }] [TO role_name [, ...]] [USING (expression)] [WITH CHECK (expression)];USING vs WITH CHECK
Section titled “USING vs WITH CHECK”| Clause | Purpose | Applies to |
|---|---|---|
USING | Filter which existing rows are visible/modifiable | SELECT, UPDATE, DELETE |
WITH CHECK | Validate new or modified row values | INSERT, UPDATE |
Per-Operation Rules
Section titled “Per-Operation Rules”| Operation | USING | WITH CHECK | Behavior |
|---|---|---|---|
| SELECT | Required | Not allowed | Filters visible rows. |
| INSERT | Not allowed | Required | Validates inserted values. |
| UPDATE | Required | Optional | USING selects updatable rows; WITH CHECK validates new values. Defaults WITH CHECK to USING if omitted. |
| DELETE | Required | Not allowed | Filters deletable rows. |
Examples
Section titled “Examples”Row ownership — users see only their own data:
CREATE TABLE todos ( id SERIAL PRIMARY KEY, user_id TEXT NOT NULL, task TEXT, done BOOLEAN DEFAULT false);
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
-- Users can only see their own todosCREATE POLICY user_select ON todos FOR SELECT USING (user_id = current_user);
-- Users can only insert todos for themselvesCREATE POLICY user_insert ON todos FOR INSERT WITH CHECK (user_id = current_user);
-- Users can only update their own todosCREATE POLICY user_update ON todos FOR UPDATE USING (user_id = current_user) WITH CHECK (user_id = current_user);
-- Users can only delete their own todosCREATE POLICY user_delete ON todos FOR DELETE USING (user_id = current_user);Public + private visibility:
CREATE TABLE posts ( id SERIAL PRIMARY KEY, author TEXT, published BOOLEAN DEFAULT false, content TEXT);
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Anyone can see published postsCREATE POLICY see_published ON posts FOR SELECT USING (published = true);
-- Authors can also see their own unpublished draftsCREATE POLICY see_own ON posts FOR SELECT USING (author = current_user);Policy Combination Rules
Section titled “Policy Combination Rules”Permissive Policies (Default)
Section titled “Permissive Policies (Default)”Multiple PERMISSIVE policies for the same operation are combined with OR — if any policy allows the row, it is accessible.
-- Row is visible if published=true OR author=current_userCREATE POLICY see_published ON posts FOR SELECT USING (published = true);CREATE POLICY see_own ON posts FOR SELECT USING (author = current_user);Restrictive Policies
Section titled “Restrictive Policies”RESTRICTIVE policies are combined with AND on top of the permissive result. Every restrictive policy must pass.
-- Must be published AND visible by permissive policiesCREATE POLICY must_be_published ON posts AS RESTRICTIVE FOR SELECT USING (published = true);Final evaluation:
(permissive_1 OR permissive_2 OR ...) AND restrictive_1 AND restrictive_2 AND ...At least one PERMISSIVE policy must match for any row to be accessible, regardless of restrictive policies.
Alter and Drop Policies
Section titled “Alter and Drop Policies”-- Change the USING expressionALTER POLICY see_own ON posts USING (author = current_user OR role = 'admin');
-- Drop a policyDROP POLICY see_own ON posts;
-- Drop only if it exists (no error)DROP POLICY IF EXISTS see_own ON posts;Bypass Mechanisms
Section titled “Bypass Mechanisms”| Mechanism | Bypasses RLS? | Bypasses FORCE RLS? |
|---|---|---|
| Superuser | Always | Always |
| Table owner | Yes | No (must obey policies) |
BYPASSRLS role attribute | Yes | Yes |
BYPASSRLS Role Attribute
Section titled “BYPASSRLS Role Attribute”Grant a role the ability to bypass all RLS policies:
CREATE ROLE admin_role LOGIN PASSWORD 'pw' BYPASSRLS;
-- Grant or revoke laterALTER ROLE admin_role BYPASSRLS;ALTER ROLE admin_role NOBYPASSRLS;SECURITY DEFINER Functions
Section titled “SECURITY DEFINER Functions”Functions declared SECURITY DEFINER execute with the identity of the function owner (typically the table owner), bypassing RLS:
CREATE FUNCTION all_posts()RETURNS SETOF postsLANGUAGE SQLSECURITY DEFINERAS $$ SELECT * FROM posts $$;Functions declared SECURITY INVOKER (the default) execute with the caller’s identity and obey RLS normally.
Security Barrier
Section titled “Security Barrier”RLS predicates are applied as security barrier qualifiers. This prevents user-supplied WHERE clauses from leaking data through side-channel functions:
-- Even if a malicious function is used in WHERE, it cannot see rows hidden by RLSSELECT * FROM secrets WHERE side_channel_fn(secret_column);-- RLS USING clause filters rows BEFORE the WHERE clause evaluatesSystem Catalog
Section titled “System Catalog”Inspect RLS configuration through standard catalog views:
-- Check if RLS is enabled on a tableSELECT relname, relrowsecurity, relforcerowsecurityFROM pg_classWHERE relname = 'todos';
-- List all policies on a tableSELECT policyname, cmd, permissive, roles, qual, with_checkFROM pg_policiesWHERE tablename = 'todos';
-- Check role attributesSELECT rolname, rolbypassrlsFROM pg_rolesWHERE rolname = 'app_user';RLS with the Browser SDK
Section titled “RLS with the Browser SDK”The Browser SDK uses RLS to enforce per-user data access. The flow:
- Configure BYO JWT authentication on your database.
- Enable RLS and create policies that reference
current_useror a custom claim. - The browser SDK attaches the user’s JWT to each request.
- The public data API validates the JWT and issues a connect token with the resolved role.
- Queries execute under that role — RLS policies filter results automatically.
-- Example: todos visible only to the JWT subjectCREATE POLICY user_todos ON todos FOR SELECT USING (user_id = current_user);// Client-side — SDK handles auth and RLS transparentlydb9.auth.setSession({ accessToken: userJwt });const { data } = await db9.from('todos').select('*');// Only rows where user_id matches the JWT subject are returnedRelated Docs
Section titled “Related Docs”- Browser SDK — client-side data access with RLS
- Auth & Roles — role management and grants
- Security & Auth — publishable keys and BYO JWT
- Production Checklist — RLS setup guidance