patternnextjsMajor
Supabase: a server-side postgres.js/pg connection as the 'postgres' role ignores every RLS policy (BYPASSRLS)
Viewed 0 times
Supabase stacks where the postgres role has rolbypassrls (checked on Postgres 17 local stack)
bypassrlsrow level security ignoredpostgres roleserver actionspostgres.jsmulti-user permissions
Problem
An app on Supabase enables RLS on every table and writes policies (e.g. "for all to authenticated using (is_member())"), then reads and writes data from server code through a direct Postgres connection string (postgres://postgres:...@host:5432 or the local 54322 port) with postgres.js or node-postgres. Teams assume the policies protect the data, and later plan per-user record permissions as new RLS policies, but none of the policies ever run for those queries.
Solution
Check it first: select rolname, rolsuper, rolbypassrls from pg_roles where rolname in ('postgres','authenticated','anon','service_role'). If the server uses 'postgres', treat RLS as defense in depth for the public API only, and enforce access in the app: one auth guard at the top of every page, route handler and server action, plus SQL helper functions (e.g. can_view(record, user)) that every list, search, export and write query calls, with the acting user passed via set_config('app.actor_id', ..., true) inside the transaction so triggers can check it too. If you really want RLS to apply, the server transaction must run "set local role authenticated" and set request.jwt.claims (or a custom GUC the policies read) before its queries, and you must review every security definer function, which still runs with its owner's rights.
Why
In Supabase the 'postgres' role is not a superuser but has rolbypassrls = true (as does service_role). Any session logged in as that role skips row level security entirely. RLS only applies to requests that arrive through PostgREST/Realtime as anon or authenticated, or to sessions that explicitly switch role.
Gotchas
- Policies that call a security definer helper still look correct in tests run through the API, which hides the problem for server code
- Background jobs (queues, imports, automations) usually must bypass user permissions on purpose; keep a distinct source/actor marker so audit logs show them
- Moving to per-user rights later means auditing every exported server action and every read helper that selects from the table directly
Code Snippets
See which roles skip RLS
select rolname, rolsuper, rolbypassrls from pg_roles
where rolname in ('postgres','authenticated','anon','service_role');
-- typical Supabase result: postgres f t | service_role f t | authenticated f f | anon f fMake one server transaction obey RLS
begin;
set local role authenticated;
select set_config('request.jwt.claims', json_build_object('sub', '<user uuid>', 'role', 'authenticated')::text, true);
-- queries here now go through the policies
commit;Context
Planning multi-user record permissions in an app that already has RLS policies on all tables but connects from the server with the database connection string.
Revisions (0)
No revisions yet.