RowShield
Guides

Bolt.new shipped tables without RLS

Bolt scaffolds schema fast, and schema is where security lives. A generated migration that creates a table rarely creates the policies to go with it.

There are two distinct end states and they need different fixes: RLS never enabled, and RLS enabled with no policies behind it.

Rules that check this

RLS off: everything is readable

The table is world-readable to the anon key. Critical, and the fix is to enable, force, and add policies in one go.

ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.your_table FORCE ROW LEVEL SECURITY;

RLS on with no policies: everything is denied

Postgres denies every row to every non-owner role. Your data is safe and your app is quietly broken — a list that renders empty, a form that fails without an error.

This one is high rather than critical, because nothing is exposed. It usually means someone enabled RLS and stopped there.

Generated policies use your columns

RowShield reads the table definition and infers the ownership column — `user_id`, `owner_id`, `created_by`, `author_id` and similar — then generates SELECT, INSERT, UPDATE and DELETE policies scoped to it.

When it cannot find one, it says so and emits a marked TODO rather than inventing a column. A policy referencing a column that does not exist fails to run, which is worse than no policy at all.

Reading a generated migration critically

Three lines decide whether a Bolt-generated migration is safe, and none of them are the CREATE TABLE itself. Look for ENABLE ROW LEVEL SECURITY immediately after each table definition; look for at least one policy per command the app will actually perform; and look for WITH CHECK on anything that writes. Their absence is not a style issue — each missing line is a distinct failure mode with its own severity.

Watch for the subtler tells as well. Policies scoped only to service_role look authoritative and bind nothing. USING (true) placeholders indicate scaffolding that was never narrowed. A migration that drops and recreates a table takes any existing policies down with it unless they are rewritten in the same file.

None of this makes Bolt unusual — every code generator shares the blind spot because policies are application semantics the generator cannot infer. What differs is velocity: generated migrations land faster than review habits adapt, which is why scheduled catalog scans exist to catch what the next prompt breaks rather than what this one did. The catalog view also settles arguments quickly: whatever the prompt history claims, the deployed schema is the artifact users actually depend on. A five-minute catalog read settles debates that otherwise consume an afternoon of speculation, which is reason enough to keep the queries bookmarked.

When findings arrive, triage by failure mode rather than file order. Exposed tables first — they are live leaks with active remediation SQL attached. Deny-all tables second, because users are silently losing data and will file it as an app bug. Decorative or placeholder policies third; they are latent rather than active. This ordering mirrors severity in the product, so the default sort is usually right, but understanding why it is right makes the fixes stick: each class fails differently, and each fix prevents a different next incident.

Frequently asked

Can I run this from CI?
Yes. The CLI exits 0 when clean, 1 at or above your `--fail-on` threshold, and 2 when the scan itself could not run, so a pipeline can tell an unsafe schema from a broken scanner.
Does it check the auth and storage schemas?
It deliberately skips them. Supabase platform schemas ship RLS-disabled tables by design; reporting them buries the one real finding under thirty fake ones.

Check your project in about ten seconds

Paste a URL. No signup, no writes, nothing stored.

Run the free audit
bolt supabase tables without rlsbolt.new supabase service_role exposedvibe coding supabase security