Fix guide · updated 2026-11-07
Security checklist for AI-built apps before you launch
RLS Disabled in Public · Table public.profiles is public, but RLS has not been enabled.
AI builders write an app in an afternoon. They also make the same security mistakes over and over, and public scans of launched AI-built apps keep finding them. Go through these eight checks before real users and real money arrive. Most take a few minutes.
1. Row-level security is on for every table
Your Supabase key is inside your app, so anyone can find it. Row-level security (RLS) is the only thing deciding what that key can read and change.
Check: Supabase dashboard → Advisors → Security Advisor. Every RLS Disabled in Public warning is a table anyone can read and edit right now.
Fix: enable RLS on each one, then add policies for who may read and write. With RLS on and no policies, nobody can use the table at all, so do both. Our row-level security guide has the SQL.
2. No policy says "everyone" on private data
A policy written as using (true) means anyone. That's fine for a public product list and a disaster for orders, messages or profiles.
Check: Authentication → Policies, every table. Private data should check the owner, like (select auth.uid()) = user_id.
3. No secret keys in the browser
Only the public anon or publishable Supabase key belongs in your app's code. Secret keys there are readable by every visitor.
Check: open your live site → browser developer tools → Sources, and search all files for service_role, sk_live_ (Stripe), sk- (OpenAI) and whsec_. Also check that none of these sit in a variable starting with VITE_.
Fix: if you find one, create a new key at the provider and delete the old one first, because the old one is already public. Then move that call into a server function (a Supabase Edge Function) that keeps the key on the server.
4. Users can't make themselves admin
A very common hole: the profiles table has a plan, role, credits or is_admin column, and users are allowed to update their own row. Then anyone can open the browser console and give themselves the paid plan.
Fix: only let users change the safe columns:
revoke update on public.profiles from authenticated;
grant update (full_name, avatar_url) on public.profiles to authenticated;
Replace the two names with the columns users may edit. Plans and roles then change only from your server, for example from your payment webhook.
5. Payments are confirmed by the server
An order must become "paid" only when your server receives Stripe's webhook with a valid signature. Never trust a "payment successful" page or a value the browser sends: both can be faked. See our Stripe webhook guide for a working, signed webhook.
6. Private files are in private buckets
In Supabase Storage, a bucket marked Public lets anyone with the link open its files. ID documents, invoices and private uploads belong in a private bucket, with policies saying who can read them.
7. Server functions check who is calling
Supabase Edge Functions check for a logged-in user by default. Keep that on for everything except webhooks, which check the sender's signature instead. Inside each function, use the user from the verified login, never a user id sent in the request body.
8. Login settings are tightened
- Email confirmation on, with a real email provider (see our email guide).
- Redirect URLs limited to your own domains (see our redirect guide).
- Sensible limits in Authentication → Rate Limits, so nobody can hammer your sign-up or password reset.
Bonus: security headers
Ask your host to send basic security headers (Content-Security-Policy, X-Frame-Options, X-Content-Type-Options). On Netlify and Cloudflare Pages that's a _headers file; on Vercel, a headers section in vercel.json.
Want this done for you?
We run this whole checklist on your app and fix what we find as part of our Launch fix: your app live, payments working, and customer data locked down.