The anon key is public by design, and only RLS limits it
When Bolt.new wires up a Supabase integration, it places the Supabase project URL and the anon key into the client bundle. The browser needs both to talk to the database directly, so this is the integration working as intended, not a leak. The anon key ships in the JavaScript every visitor downloads, which means it is readable by anyone who opens the URL, opens DevTools, or runs a script against the page.
So the question that matters before launch is not whether the anon key is exposed. It is. The question is what that key is allowed to do once someone has it. Supabase serves every table through an auto-generated REST API, and the anon key authenticates a request as the anonymous role. Row Level Security is the thing that decides which rows that role gets back. With RLS off or permissive on a given table, the anon key reads and often writes that table directly, with no login involved.
That is the concrete launch risk: a Bolt scaffold that works on the first try because the database is open, shipped to a public URL before anyone checked which tables the anon key can reach. This note stays scoped to the Bolt-plus-Supabase wiring and how to probe it. The general mechanics of RLS — policies, the anon role, deny-all — are covered in the Lovable and Supabase RLS post. For the broader Bolt launch surface, see the Bolt.new app security defaults note.
- Observable from outside: the anon key sits in the bundle, and any table it can reach answers a REST request from a clean browser.
- Only possible until you test it: that a specific table is over-exposed. RLS is per-table, so you cannot tell from the key alone.
- This is an authorization gap (which rows the anon role may touch), not a broken login. The key is doing exactly what an anon key does.
Why a Bolt scaffold lands here
Bolt generates a working table and a working query in the same pass. A prompt like "store user submissions in Supabase" produces the table, the client call, and UI that reads and writes through the anon key — and it all works immediately. It works precisely because nothing is standing in front of the table yet. RLS is opt-in per table in Supabase, so a freshly scaffolded table has no row-level rules, and the auto-generated REST API serves it to the anon role by default.
The trouble is that a table being open is invisible from inside the app. The feature functions, the demo records save and load, so the build looks finished. The step that does not happen on its own is the per-table review where someone decides who should be allowed to read and write each table from outside. Bolt's fast deploy then turns the preview into a public URL before that review happens, so the anon key's real reach is live before anyone has measured it.
- The app reading and writing its own tables proves the wiring works, not that the tables are scoped.
- A second table added in a later prompt starts open again — checking one table tells you nothing about the next.
- The anon key being wired correctly is what hides that it is also the only thing in front of the data.
Probe which tables the anon key can reach
You can measure this on your own project without any attack. The goal is to act as the anonymous caller, exactly as an outside visitor would, and see which tables answer.
First, find the project URL and anon key. Open the deployed app in a clean browser session, open DevTools, and watch the Network tab for requests to your-project.supabase.co/rest/v1/. The request URL gives you the project host, and the apikey header (or the bundle text itself) gives you the anon key. Search the loaded JavaScript for supabase.co and for the key shape if you want to confirm where it lives.
Then query each table's REST endpoint as the anonymous caller, against your own project only. A read is a GET to /rest/v1/your_table?select=* with the anon key in the apikey header. Repeat it once per table you scaffolded — payments, profiles, submissions, admin, settings, whatever the app created.
Do not stop at reads. A table can be readable, writable, or both, and a read-only check misses the writable case entirely.
- Suspicious result: rows come back from a table that should require a logged-in user. That means the anon role can read it.
- Safer result: an empty array or a permission error for a table that should be private. An empty array with RLS on and no matching policy is the expected deny, not a bug.
- The Supabase side of the same check: open the Database linter under Advisors in the dashboard. It flags tables exposed through the REST API with RLS disabled, which is the fast way to list candidates before you probe each one.
Test writes, then close each table
To check writes, send a POST to /rest/v1/your_table with a small test payload and the anon key, again against your own project. A response that creates a row means the anon role can insert into that table. Try a PATCH against an existing row id the same way to check updates. A write that succeeds with only the anon key is a clear signal the table is open for writing, which is usually worse than a readable one — it lets an anonymous caller add or change records, not just see them.
The fix is per table, and the safe place to start is closed. Enable RLS on every table that holds user, account, payment, or admin data. Enabling RLS with no policy denies all access; the table goes dark, and you then open only the paths you actually intend. Starting from deny-all and adding policies is far safer than leaving a table open while you decide what it should allow.
After you enable RLS and write policies, re-run the same GET and POST probes. The reads that should be private should now return an empty array or a permission error, and the writes that should be blocked should fail.
- Turn RLS on for every non-public table first, even before the policies are written. A closed table beats an open one.
- Write one policy per intended operation — select, insert, update, delete — rather than a single broad USING (true) rule, which puts you right back where you started.
- Scope reads and writes to the row owner where that applies, so a logged-in user touches their own rows and no one else's.
- Never put the service_role key anywhere Bolt can ship it to the client. It bypasses RLS entirely, so a service_role key in the bundle hands every table to every visitor. If one ever shipped, rotate it and treat it as leaked.
RLS controls which rows the anon role may read or change. It does not validate what a write contains. A caller you allow to insert their own row can still submit malformed or abusive values, so input validation and constraints stay a separate job from closing the table.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check detects the Supabase anon key in your client bundle and probes the accessible REST endpoints from the public surface, the same outside view an anonymous visitor has. When a table returns rows to the anonymous caller, the scan flags that exposure with severity and fix direction, so an open table shows up in your own check rather than someone else's.
What it does not do is write your policies. Deciding which rows each role should see, and authoring correct per-table RLS in Supabase, is work you do yourself and should pair with a manual look at your auth and data model. Launch Check is not a penetration test, a code review, or a guarantee that the app is secure.
A clean public-surface scan means the obvious anon-key exposures are handled — not that every table is correctly scoped. The scan sees what is reachable from outside with the anon key; it does not read your policy logic or confirm the policies you wrote match your intent. Treat a flagged table as a launch blocker and a clean result as one signal, not proof. Next step: list your tables, probe each one as the anon caller for reads and writes, then run a focused Launch Check against the public URL before you share it.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.