Bolt + Supabase RLS checklist: enable, scope, test
By StackSiren · Published
Bolt apps that use Supabase talk to the database from the browser with a publishable (or legacy anon) key, so Row Level Security is your real access control. Enable RLS on every table in public, write policies scoped with auth.uid(), and prove them with a signed-out request before you call the job done.
Bolt can connect a Supabase project for auth, tables and Edge Functions (Pro/Teams; see Bolt's Supabase integration docs). The front end still holds a public key. That is fine only when grants and policies are right.
How Bolt + Supabase is wired
Typical shape:
- The browser creates a Supabase client with your project URL and publishable / anon key.
- Signed-in users send their JWT; signed-out visitors act as
anon. - Postgres checks grants first, then RLS policies for that role.
- Edge Functions (when Bolt generates them) run on the server and can hold secrets. Admin work belongs there, not in the browser.
If a service_role or sb_secret_ key appears in client code, treat it as a leak: My Supabase service_role key is exposed.
Bolt's database Security Audit uses the same style of checks as Supabase's Security Advisor. It flags missing RLS and "RLS Policy Always True" rules. Useful, but not a substitute for a second-user test: scanners can't know whether one signed-in user should see another's orders.
Step 1: List every table and its RLS status
In the Supabase SQL editor for the project connected to Bolt:
select c.relname as table_name,
c.relrowsecurity as rls_enabled,
(select count(*) from pg_policies p
where p.schemaname = 'public' and p.tablename = c.relname) as policy_count
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r'
order by rls_enabled, table_name;
Any row with rls_enabled = false is open to whatever grants anon / authenticated already have. Fix those first.
Bolt's docs say it turns RLS on for tables it creates, and that tables you add yourself need RLS turned on too. Don't assume — run the query.
Step 2: Turn RLS on (safe starting point)
alter table public.notes enable row level security;
With RLS on and no matching policy, the Data API returns zero rows for publishable-key clients. Your app may look "broken" until you add scoped policies. That blank state is safer than an always-true fix.
Step 3: Write owner-scoped policies
Most Bolt apps store per-user rows with a user_id uuid column. One policy per action:
create policy "notes_select_own" on public.notes
for select to authenticated
using ((select auth.uid()) = user_id);
create policy "notes_insert_own" on public.notes
for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy "notes_update_own" on public.notes
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "notes_delete_own" on public.notes
for delete to authenticated
using ((select auth.uid()) = user_id);
Details that matter:
to authenticatedblocks signed-out visitors on this table.usingfilters existing rows;with checkvalidates the row being written. An update withoutwith checkcan let a user reassignuser_id.- Wrap
auth.uid()in(select ...)so Postgres can cache it per statement (Supabase's recommended pattern). - Index
user_id:create index on public.notes (user_id); - Optional:
alter table public.notes alter column user_id set default auth.uid();so inserts don't need a client-supplied owner id.
Public read, owner write (blog posts, menus):
create policy "posts_public_read" on public.posts
for select to anon, authenticated
using (published = true);
create policy "posts_owner_write" on public.posts
for all to authenticated
using ((select auth.uid()) = author_id)
with check ((select auth.uid()) = author_id);
Prefer a real condition (published = true) over bare using (true) when you can. When using (true) is genuinely right for public read-only data, pair it with select-only grants so visitors cannot write through an open policy.
Step 4: Check grants (especially new projects)
RLS decides which rows. Grants decide whether the role can touch the table at all. On many projects, new public tables still start with broad grants; Supabase is tightening defaults for new projects from 30 October 2026. A safe pattern:
revoke all on table public.notes from anon, authenticated;
grant select, insert, update, delete on public.notes to authenticated;
-- leave anon out unless signed-out visitors genuinely need the table
Background: our post on the grant change and the Supabase RLS fix guide.
Step 5: Signed-out (and second-user) test
Don't trust the dashboard alone. Against your own project:
# Signed-out read: expect [] or a permission error, never real private rows
curl -s "https://YOUR-PROJECT.supabase.co/rest/v1/notes?select=*&limit=5" \
-H "apikey: YOUR_PUBLISHABLE_KEY"
Then sign in as user A in the app, copy the Authorization: Bearer ... access token from DevTools, and:
curl -s "https://YOUR-PROJECT.supabase.co/rest/v1/notes?select=id,user_id&limit=20" \
-H "apikey: YOUR_PUBLISHABLE_KEY" \
-H "Authorization: Bearer USER_A_ACCESS_TOKEN"
Every user_id should be user A's. Try updating a row you know belongs to user B with user A's token — it should affect zero rows.
Our Supabase RLS checker automates the signed-out half. For a Lovable-oriented walkthrough of the same policies, see Setting up Supabase RLS in a Lovable app — the SQL is the same once Bolt is talking to Supabase.
Prompting Bolt to tighten policies
Bolt's security docs suggest asking in chat, for example:
Check that row-level security is on for every table in my database, and that each policy allows only the access my site needs.
Or, after you decide the model:
Update the policies for the orders table so only signed-in people can view, add, change, or delete their own orders. Use auth.uid() = user_id and add with check on insert and update.
Read the SQL before you approve it. Re-run the curl tests afterwards. If Bolt's Security Audit still shows RLS Policy Always True on a private table, that policy is still too open.
Checklist (copy this)
- Every
publictable hasrls_enabled = true - No
service_role/sb_secret_key in the browser bundle (checker) - Owner tables use
auth.uid()(not merelyauth.uid() is not null) - Insert/update policies have
with check -
anongrants exist only where public access is intentional - Signed-out curl returns no private rows
- Second-user curl cannot see or edit another user's rows
- Storage buckets for user files are private with
storage.objectspolicies
Related reading
- My Supabase service_role key is exposed. What now?
- Are VITE_ environment variables secret?
- I leaked my OpenAI API key. What now?
FAQ
Bolt's Security Audit passed. Am I done? It's a good sign for missing RLS and always-true policies. It can't verify your business rules. Run the two-user test.
Why does my Bolt app show no data after I enabled RLS?
RLS with no matching policy returns zero rows. Add a select policy for authenticated, and check the table has a grant.
Is the Supabase key in my Bolt front end a problem?
Not if it's publishable or anon. It is a problem if RLS lets that role see too much, or if the key is service_role / sb_secret_.
Do I need sign-in for private data? Yes. Without Auth, policies can't tell users apart, so private tables either stay locked or get opened with always-true rules. Bolt's security docs say the same: add sign-in, then scope policies to the signed-in user.
Can StackSiren help? Yes, passively, on sites you own. The first scan is free; Solo is $29/mo and adds scheduled rescans so a new deploy can't quietly undo your policies.
Fix guides for this topic
Platform checklists: Bolt