Supabase RLS checklist: lock down your database before launch
Your Supabase URL and anon key ship to every browser. That’s by design: Supabase relies on Postgres Row Level Security to decide which rows each request can touch. If RLS is off, or a policy is too loose, your anon key is effectively a master key. Use this checklist before you launch and after every AI-generated migration.
1. Enable RLS on every table in exposed schemas
Tables created in the SQL editor or by migrations don’t always have RLS on. Find the ones that don’t:
Then enable it on each one:
With RLS enabled and no policies, the table is locked to everyone except the service_role. That’s a safe default, and you add access back one policy at a time.
2. Add owner-only policies
The most common pattern: each row has a user_id column referencing auth.users, and users can only see and change their own rows.
Wrapping auth.uid() in (select …) lets Postgres cache it per query. Supabase recommends this for performance. Also add an index on user_id.
3. Hunt for dangerous policies
using (true)orwith check (true)on insert, update or delete means anyone can write.- Policies targeting the
anonrole on tables holding personal data. - Update policies with
usingbut nowith check. A user could reassign a row to someone else. - Role or plan columns (
is_admin,plan,credits) that users can update on their own profile row. Move these to a separate table only the backend writes.
4. Secure Storage buckets
Storage access is controlled by RLS policies on storage.objects. Public buckets serve any file to anyone with the URL, which is fine for avatars but wrong for invoices or uploads. For private per-user folders:
5. Watch views and security-definer functions
Views run with the permissions of their creator by default, which can bypass RLS. On Postgres 15+, create them with with (security_invoker = true). Functions marked security definer also bypass RLS. Keep them out of exposed schemas or check auth.uid() inside them.
6. Keep the service_role key server-side
The service_role (or new sb_secret_) key ignores RLS entirely. It belongs only in Edge Functions or your backend. Never put it in a VITE_ or NEXT_PUBLIC_ variable. ShipGrader decodes every JWT in your bundles and flags service_role as critical.
7. Run the Security Advisor and test as a stranger
- Dashboard → Advisors → Security Advisor: fix every error and warning.
- Sign up a second test account and confirm it can’t see the first account’s data.
- Log out and confirm the anon role sees only what you intend to be public.
Copy-paste checklist
- RLS enabled on every table in
public - Owner policies for select / insert / update / delete
- No
truewrite policies - Privileged columns not user-editable
- Storage buckets private unless truly public
- Views use
security_invoker - service_role key only on the server
- Security Advisor clean