Implementing Row Level Security in Supabase: decision guide and validation
Choose database‑enforced RLS over Edge Function gating for tenant isolation in Supabase, with policy SQL, trade‑offs and validation steps.
21 Apr 2026, 00:09 UTC

Decision: enforce per‑user row access in Supabase PostgREST
You need tenant isolation on a PostgreSQL table that is exposed directly by Supabase PostgREST. The useful takeaway is to enforce access in the database with Row Level Security so the auto‑generated API cannot leak rows even if a client query is wrong. Use an Edge Function only for transformations that must run close to the data, not as a security boundary.
Options compared
| Option | Enforcement point | Latency | Complexity | Leak risk |
|---|---|---|---|---|
| RLS policy in PostgreSQL | PostgreSQL, evaluated per row | Zero extra network hop | SQL, JWT claims | Low when policies are correct |
| Edge Function proxy | Cloudflare Workers sandbox | ~50 ms cold‑start added | TypeScript, auth check | Medium, can be bypassed |
| App‑level filtering | Client or service‑role key | Zero | Application code | High, relies on correct queries |
Trade‑offs
RLS policies can be defined via SQL or the Supabase dashboard and control per‑user access to rows based on JWT claims. Enforcement happens inside Postgres, so a bad client query still cannot read other tenants. The main risk is policies that reference column‑level JWT claims may cause unexpected access if the claim is missing or malformed. Enabling RLS without a permissive policy blocks all access, including service role.
Concrete RLS policy for tenant_id
Assume a table projects with columns tenant_id uuid and owner_id uuid. The JWT issued by Supabase Auth contains app_metadata.tenant_id and sub as the user identifier.
-- Enable RLS; without it all rows are readable
alter table projects enable row level security;
drop policy if exists projects_tenant_isolation on projects;
-- Allow users to select rows belonging to their tenant
create policy projects_tenant_isolation on projects
for select
using (tenant_id = current_setting('request.jwt.claims', true)::json->>'app_metadata'->>'tenant_id');
-- Allow insert only for own tenant and owner
create policy projects_insert on projects
for insert
with check (
tenant_id = current_setting('request.jwt.claims', true)::json->>'app_metadata'->>'tenant_id'
and owner_id = auth.uid()
);
Validation checklist
Verify column exposure. Inspect the generated PostgREST metadata endpoint for the table to see which columns are returned by default. Run from a terminal with a project reference placeholder.
curl -H 'apikey: ${SUPABASE_ANON_KEY}' https://{project-ref}.supabase.co/rest/v1/projects?select=*&limit=1
Expected check: response contains only columns you intend to expose. Remove sensitive columns from select or use a view.
Confirm JWT claims. In Supabase dashboard go to Authentication to Users and inspect the JSON claims for a test user. Ensure app_metadata.tenant_id is present and matches row values.
Test access with a test JWT. Create two test users in different tenants, insert one row per tenant, and query with each user's JWT. A correct policy returns only the owning tenant's rows and returns an empty set for the other.
Limitations: RLS does not hide columns, only rows. Edge Functions have a 50 ms cold‑start and a 1 MB memory limit, unsuitable for heavy compute. Database webhooks via triggers can invoke external services on INSERT/UPDATE/DELETE but do not replace access control.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.