When do you do this? (the exact moment)
The moment is clear and non-negotiable: right before you publish your app or hand it to real users. While you're testing on your own machine, nothing happens. But the second your app is on the internet with other people's data, without these locks you're exposing everything. And since AI won't do it on its own (it's thinking about making it work, not about protecting it), it's on you to ask for it explicitly.
Lock 1 · RLS (so everyone sees ONLY their own stuff)
Without RLS, a malicious user just needs to change an ID in the URL or in a request to see someone else's data. It's the most common vulnerability in Supabase and PostgreSQL apps — and the easiest to exploit. That's why it's lock #1.
I'm using Supabase with PostgreSQL. I have these tables: [list your tables and their user columns, e.g.: posts (user_id), comments (post_id, user_id)]. Set up Row-Level Security so each user can only view/edit/delete their own records. Follow Supabase's official best practices: - Enable RLS on ALL tables (ENABLE ROW LEVEL SECURITY). - Create SEPARATE policies for SELECT, INSERT, UPDATE and DELETE (do NOT use FOR ALL). - IMPORTANT for performance: wrap the user function as (SELECT auth.uid()), NOT a bare auth.uid(), so Postgres evaluates it once per query instead of once per row. - Restrict policies to the authenticated role (TO authenticated), not public, so anonymous visitors can't touch the table at all. - NEVER use user_metadata in policies (the user can modify it). - Add indexes on the columns the policies use (e.g. user_id). - Remember: for an UPDATE to work, you also need a SELECT policy. - Give me the complete SQL ready to paste into the Supabase SQL Editor, and explain in one line what each policy does.
auth.uid() directly in the policy. BUT that makes Postgres evaluate it once for every row — on a table with 100,000 rows, the difference is between a 5-millisecond query and a 5-second one! The trick (documented by Supabase): write it as `(SELECT auth.uid())`. That way it's evaluated just once. That's why the prompt above asks for it explicitly.- Create two test users in your app.
- Log in as user A and create a record.
- Log in as user B: they should NOT see A's record.
- Check Supabase's Security Advisor: it warns you if you left any table without RLS.
Lock 2 · CORS (who's allowed to call your API)
Configure CORS on my backend so that ONLY my production domain (https://my-app.com) and localhost in development can make requests. - Do NOT use "*" as an allowed origin in production. - Only allow the methods I actually use (e.g. GET, POST, PUT, DELETE). - Only allow the headers I need. - Explain exactly where to put each thing based on my stack: [tell it your framework/backend].
Access-Control-Allow-Origin: * with credentials enabled. It's like leaving the door open and hanging a sign that says "come on in, I left the keys inside." If your AI set it up that way "to make it work fast," fix it before you ship.Lock 3 · Security Headers (defense instructions for the browser)
Add the recommended security headers to my app: Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Strict-Transport-Security (HSTS) and Permissions-Policy. - Explain what each one does in one simple line. - Give me the config ready for my framework: [tell it which you use, e.g. Next.js, Express]. - Start the Content-Security-Policy in report-only mode so nothing breaks, and tell me how to tighten it afterward. - Tell me how to verify the result.
The habit · check it before every release
- RLS enabled on every table with user data (and tested with 2 accounts).
- CORS restricted to your domains, never
*with credentials. - Headers in place and scoring an A on securityheaders.com.
- Your secrets (.env, API keys) NEVER pushed to GitHub (see the series' GitHub guide).
- You checked Supabase's Security Advisor and there are no red warnings left.