VibeSecurely
Blog
SupabaseVibe codingApp security

The Supabase Security Checklist for Vibe-Coded Apps

The VibeSecurely team8 min read

If you built your app with Lovable, Bolt, v0, Cursor, or Replit, there is a good chance Supabase is your backend. It is a brilliant tool, it hands you a Postgres database, authentication, and file storage in minutes. But it ships with one default that has burned hundreds of vibe-coded apps: new tables are public until you say otherwise.

Here is the uncomfortable part. By design, Supabase puts your project URL and a public "anon" key right in your front-end code. That is fine, if your database is locked down. If it is not, anyone who opens your site's JavaScript can talk to your database directly and read every row you forgot to protect. A 2025 analysis tied to CVE-2025-48757 found that roughly 10% of scanned apps shipped with public-readable tables because Row-Level Security was never turned on.

This checklist walks through exactly how to lock down a Supabase app, in plain language, in the order that matters most. Work through it before you put real users behind your app.

Rule 1: Enable Row-Level Security on every table

This is the whole ballgame. Row-Level Security (RLS) is what tells Postgres "only return the rows this specific user is allowed to see." With RLS off, the public anon key can read, and often write, the entire table. With RLS on and no policy, the table returns nothing to the public, safe by default.

The rule is simple: every table in your public schema must have RLS enabled. No exceptions.

Do this: In the Supabase dashboard, open the Table Editor (or Authentication, then Policies) and confirm every table shows "RLS enabled." For any that do not, run alter table your_table enable row level security;. A table with RLS on but no policies denies all public access by default, which is exactly the safe starting point.

Rule 2: Write policies with auth.uid(), never client-supplied IDs

Turning RLS on is half the job. Now you write policies that say who can do what. The single most important habit: anchor every policy to auth.uid(), the ID of the logged-in user as verified by Supabase, never to a user ID sent from the browser, which an attacker can change.

For example, to let users read only their own rows, your SELECT policy's condition would be auth.uid() = user_id. Write a separate policy for each action you actually allow (select, insert, update, delete), and keep each one as tight as possible. If only the owner should edit a record, your update policy must enforce exactly that.

Common mistake: policies that trust a user ID from the request body. If the client can set it, the client can impersonate anyone. Always derive identity from auth.uid().

Rule 3: Keep the service_role key on the server, always

Supabase gives you two keys. The anon key is public and meant for the browser. The service_role key is an admin key that bypasses RLS entirely, it can read and write everything. If it ever reaches your front-end bundle, your whole database is exposed, RLS or not.

Do this: Use service_role only in server-side code, environment variables, edge functions, your own backend. Never in React components, client JavaScript, or anywhere shippable to the browser. If it has ever been exposed, rotate it immediately in the dashboard.

Rule 4: Lock down Storage buckets

Supabase Storage has the same trap as tables. A public bucket can be listed and read by anyone. If you uploaded user files, invoices, ID photos, receipts, to a public bucket, they may be enumerable by strangers.

Do this: Make buckets private unless they genuinely hold public assets, and add Storage policies (the same auth.uid() pattern) so users can only reach their own files. Use signed, time-limited URLs for access instead of making the whole bucket public.

Rule 5: Don't trust the client for anything that matters

RLS protects your data, but your app's logic still runs partly in the browser, and anything in the browser is editable. Prices, quantities, roles, feature flags, and "is this user an admin?" checks must be enforced on the server, in Postgres policies, database functions, or edge functions, never assumed from client state.

Do this: Move sensitive operations into Postgres functions (RPC) or edge functions where you control the logic, and validate every input. Compute anything that touches money or permissions server-side.

Rule 6: Get the auth basics right

Supabase Auth is solid, but the defaults still need attention:

  • Turn on email confirmation so people cannot sign up as someone else.
  • Add rate limiting or a CAPTCHA on auth endpoints to slow credential stuffing.
  • Require strong passwords, and use MFA for any privileged accounts.
  • Confirm email changes to the old address to prevent account takeover.

Rule 7: Test it like an attacker

Do not assume your policies work, prove it.

Do this: In the Supabase SQL editor, use the role switcher to run queries as anon and as different authenticated users. Try to read another user's rows. Try to write where you should not. If anything comes back that should not, your policy has a hole. Supabase's own docs walk through this.

The 60-second Supabase audit

Run through this before launch:

  • Every table in the public schema has RLS enabled.
  • Every table has explicit policies, all anchored to auth.uid().
  • The service_role key appears nowhere in client code or the repo.
  • Storage buckets are private, with per-user policies and signed URLs.
  • Prices, roles, and permissions are enforced server-side, not in the browser.
  • Email confirmation and auth rate limiting are on.
  • You have queried as anon and another user and confirmed you cannot reach data you should not.

When the checklist isn't enough

This list closes the doors that get vibe-coded Supabase apps breached. But policies can be subtly wrong, an update that is too permissive, an RPC that leaks, a logic flaw no checklist anticipates. That is where a human pentest earns its keep: someone who attacks your app the way a real attacker would and tells you exactly what is still open. For the broader picture beyond Supabase, see our guide to the 7 flaws AI coding tools ship by default.

If you want a human to pressure-test your Supabase app before launch, a real pentest starts at $499, and you can see a sample report first.

Frequently asked questions

Is Supabase secure for production?
Yes - Supabase can be perfectly secure in production, but only if you configure it. The defaults are the risk: new tables ship with Row-Level Security off, and the public anon key lives in your browser code. With RLS enabled and proper policies on every table, and the service_role key kept server-side, a Supabase app is production-safe.
What is the difference between the anon key and the service_role key?
The anon key is public and meant for the browser; on its own it can only do what your RLS policies allow. The service_role key is an admin key that bypasses RLS entirely and must never appear in client code - if it leaks, your whole database is exposed regardless of your policies.
Do I really need RLS on every table?
Yes. Any table in the public schema without RLS enabled is readable, and often writable, by anyone with the public anon key, which is shipped to every visitor's browser. Enable RLS on every table and add explicit policies anchored to auth.uid().
How do I test my Supabase RLS policies?
Use the role switcher in the Supabase SQL editor to run queries as anon and as different authenticated users, then try to read or modify rows you should not be able to. If anything you should not see comes back, your policy has a gap.