Supabase RLS checklist before launch
If your vibe-coded app talks to Supabase from the browser, Row Level Security is the difference between “users see their own data” and “anyone with the anon key reads the whole table.”
The Supabase anon key is meant to be public. It lives in your frontend. What keeps it safe is not hiding the key. It is Row Level Security (RLS): Postgres policies that decide which rows each signed-in user can read or write.
This guide is a launch checklist, not a Postgres textbook. Work top to bottom. If any step fails, fix it before you charge anyone.
1. Turn RLS on for every table that holds user data
In the Supabase dashboard, open Authentication → Policies (or the table editor). For each table that stores accounts, profiles, posts, orders, files metadata, or anything tied to a user:
- Confirm RLS is enabled.
- Confirm you did not leave a temporary “allow all” policy from prototyping.
- Prefer explicit policies per operation (SELECT, INSERT, UPDATE, DELETE) over one giant policy you cannot reason about.
A table with RLS disabled is readable by anyone who has the anon key and can guess or enumerate IDs. That is a common vibe-coding failure mode: the app UI only shows “your” rows, but the API still returns others.
2. Write policies that bind to auth.uid()
Most solo apps need the same shape:
- SELECT / UPDATE / DELETE:
auth.uid() = user_id(or whatever column stores the owner). - INSERT:
auth.uid() = user_idso a client cannot insert rows as someone else.
If you use a profiles table keyed by id matching auth.users.id, the policy is usually auth.uid() = id.
Avoid policies that only check “user is logged in” (auth.uid() is not null) on tables with per-user rows. That lets any signed-in user read every row.
3. Never put the service_role key in the browser
The service_role key bypasses RLS. It belongs only on a trusted server: a Next.js route handler, a Supabase Edge Function with secrets, a worker, never NEXT_PUBLIC_*, never a Vite env that ships to the client.
Quick check: search your built JS for service_role and for long JWTs. Or paste your production URL into the free Last Mile pre-launch check, which flags service_role JWTs and notes anon keys so you remember to verify RLS.
4. Storage buckets need policies too
File uploads are easy to leave open. For each bucket:
- Decide public vs private. Avatars might be public; invoices should not be.
- Add storage policies so users can only read/write objects under their own prefix (for example
user_id/...). - Validate file type and size on the server or with storage restrictions. Do not trust the client MIME type alone.
5. Test like an attacker (two accounts)
Create two test users, A and B. While logged in as A:
- In the app, copy a request from DevTools that loads A’s data.
- Swap in B’s UUID (from the URL, the JSON body, or a list endpoint).
- Confirm the request fails (401/403 or empty), not 200 with B’s row.
- Repeat for update and delete.
Also test logged-out access to routes that should require a session. UI hiding is not access control.
6. Views, RPCs and edge functions
Security definer functions and views can accidentally bypass RLS if you are not careful. Before launch:
- List every RPC your client calls. Confirm each one checks the caller or runs as the invoker with RLS still applied.
- Prefer “security invoker” behavior unless you have a specific reason not to.
- If an Edge Function uses the service role, it must re-check authorization itself (session user, ownership) before writing.
7. Staging vs production keys
Use separate Supabase projects (or at least separate keys) for development and production. Never point a public production site at a project full of seed users and relaxed policies.
Launch-day RLS short list
- RLS enabled on every user-data table
- No blanket allow policies left over from demos
- Owner checks use
auth.uid() - service_role only on the server
- Storage policies match your privacy model
- Two-user IDOR test passed
RLS is one slice of a full launch. For secrets, Stripe webhooks, backups and the rest of the list, use the free checklist preview or the full Last Mile kit.
8. Common RLS mistakes in AI-generated apps
Agents often generate a first policy that looks right in the dashboard but fails in practice:
- Policy on the wrong column. The app stores
owner_idbut the policy checksuser_id. Every query returns empty or, worse, you “fix” it by disabling RLS. - Using the service role in the client “just for now.” Temporary becomes production. Search the bundle before every launch.
- Enabling RLS with zero policies. That locks everyone out, so someone toggles RLS off again. Add policies in the same change as enabling RLS.
- Team/admin tables without role checks. An
is_adminflag in a client-readable profile table is not authorization. Check roles in policies or on the server with a trusted claim.
When an agent proposes a policy, ask it to explain in one sentence who can read and write which rows, then run the two-user test yourself.
Related guides
- Supabase anon key exposed: is it a problem?
Short answer: the anon key being visible is normal. Whether that's a problem depends entirely on your Row Level Security. The service_role key is a different story.
- Lovable and Bolt app security before launch
AI app builders get you from idea to working app in an afternoon. They don't decide who should be allowed to read which rows. That part is still yours, and it's checkable without writing code.
- A vibe-coded app launch checklist
Your app runs on a preview URL. Friends can click around. Launch is the moment real users and real money show up. This checklist is the short version of what usually breaks first.
Get launch notes
$ subscribe --launch-notes # new checks, prompts and shipping tips for vibe coders. No spam.