Bolt.new builds and the policies that were never written
A Bolt.new build produces its backend as a series of applied statements, and those statements have a characteristic shape: thorough about structure, brief about access. Tables arrive with keys, defaults and foreign keys attached. The CREATE POLICY lines that decide who may read and write each table are a separate act of design, and generation stops short of them far more often than it stops short of a column.
This page is about that specific end state — Row Level Security enabled on a table with zero policies behind it — because it is the least understood of the common outcomes and the one that confuses builders most. It does not leak, so it does not alarm. It denies everything, so it breaks the app in ways that look like bugs anywhere but security. Our existing page bolt-supabase-rls-missing covers the RLS-switched-off variant; this one picks up where enabling RLS leaves you.
Rules that check this
- highRLS enabled but no policies
RLS_NO_POLICIES
The migration that stops one line short
The output below is the typical shape: structurally complete, syntactically valid, applied without complaint. Someone in the loop knew enough to enable RLS — often prompted by an advisor warning or a checklist — and the session ended before the design work of policies began:
Read it once more with access in mind. The table declares its keys and references auth.users, so ownership of each row is knowable from the data itself — which is exactly what makes the missing policies easy to write late and easy to forget entirely. Structure implies nothing about permission; the two concerns live in different statements, and only one of them generates itself.
CREATE TABLE public.workspaces ( id uuid primary key default gen_random_uuid(), owner_id uuid not null references auth.users (id), name text not null, created_at timestamptz not null default now() ); ALTER TABLE public.workspaces ENABLE ROW LEVEL SECURITY; -- No CREATE POLICY statement follows.
Deny-by-default is the feature doing its job
With RLS enabled and no policies, Postgres denies every row on the table to every role except the owner and roles carrying BYPASSRLS. That is correct behaviour, not a malfunction: policies are additive grants onto a default of refusal, and zero policies means zero grants. Your data is, if anything, safer than before — and your application is quietly dead.
The symptoms mislead. Lists render empty rather than erroring, because an RLS denial to a PostgREST collection query is an empty result set. Writes fail with permission errors that surface in console tabs nobody opens. Teams respond by asking the model to "fix it", and the fastest fix on offer is frequently a permissive policy that trades a deny-all for an allow-all — moving from the safest failure to the worst one in a single applied patch.
Finding half-finished tables
The catalog separates this state cleanly from its neighbours. Where rls_enabled true pairs with a policy count of zero, you have found a table that is protected and unusable — the exact signature of setup left halfway. Run this against any project for the full picture:
Run it after every batch of applied migrations rather than occasionally, because this state arrives silently with new tables and never announces itself in application logs. The signature is unambiguous — rls_enabled true beside policy_count zero — while everything else in the output merits only a glance unless you intend to audit policy expressions as well.
SELECT n.nspname AS schema,
c.relname AS table_name,
c.relrowsecurity AS rls_enabled,
(SELECT count(*)
FROM pg_catalog.pg_policies p
WHERE p.schemaname = n.nspname
AND p.tablename = c.relname) AS policy_count
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'p')
AND n.nspname = 'public'
ORDER BY c.relname;Writing the policies that were owed
Each policy is a small sentence about ownership. Identify the column that ties a row to a principal — owner_id here — then grant read and write per command, with WITH CHECK present on inserts so a caller cannot mint rows attributed to someone else. Wrapping auth.uid() in a subquery keeps evaluation once-per-statement rather than per-row:
Add a DELETE policy in the same shape whenever the feature deletes, and resist the shortcut of one permissive ALL policy scoped to nobody in particular. Then verify as a real signed-in user rather than through an administrative connection, because privileged sessions bypass Row Level Security entirely and will happily prove nothing about what ordinary callers experience.
CREATE POLICY "workspaces_owner_select" ON public.workspaces FOR SELECT TO authenticated USING ((SELECT auth.uid()) = owner_id); CREATE POLICY "workspaces_owner_insert" ON public.workspaces FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = owner_id); CREATE POLICY "workspaces_owner_update" ON public.workspaces FOR UPDATE TO authenticated USING ((SELECT auth.uid()) = owner_id) WITH CHECK ((SELECT auth.uid()) = owner_id);
How RowShield sees this state
RowShield reports enabled-with-no-policies as its own rule, distinct from RLS-disabled and from tautological policies, because the severities differ — high rather than critical, since nothing is exposed — and because the remediations differ entirely. A scan names the affected tables and generates the owner-scoped policies from your actual columns, marking clearly where it could not infer an ownership column rather than inventing one.
Because the state tends to recur — every sprint adds tables, some of them stop one line short again — scans diff against the previous run and alert on transitions: a policyless table appearing, a fix regressing, a resolution holding. RowShield is an independent product, unaffiliated with and not endorsed by Supabase; it reads pg_catalog metadata only, never rows, and the introspection statements are exported for audit.
Frequently asked
- Why did my app break right after I enabled RLS?
- Enabling RLS with no policies denies every row to your application roles, so lists empty out and writes fail — the timing makes RLS look guilty when it is actually half-finished protection. Add owner-scoped policies for each command in the same change and behaviour returns with access control attached.
- Is a policyless table ever the correct end state?
- Rarely, and only deliberately — for instance a table meant exclusively for server-side access via service_role, which bypasses RLS anyway. As an accident of generation it is unfinished work: the table is unreachable from the client until policies exist, and nothing else will remind you.
- Is this state dangerous?
- It is the opposite failure: exposure with nothing leaking. The risks are operational — features silently dead, and pressure to "fix" the denial by granting everything to everyone. Severity rankings reflect that: critical for states that leak, high for this one, which blocks rather than discloses.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit