Skip to content
Loadout

Fix

Supabase RLS with no policy is deny-all

Answer

Enable RLS on every public table and add policies only for the operations clients should be allowed to do, because any operation without a policy is denied. A table without RLS is readable and writable through the Data API under the default grants.

  • Next.js 16.4.1
  • React 19.3
  • @supabase/ssr 0.12.7
  • @supabase/supabase-js current, installed alongside @supabase/ssr

Last verified 2026-10-15

Agents add a table in public and then either leave RLS off or enable RLS and write no policies. Those are different failures, and both are in the database section of the Next.js + Supabase pack. The pack pins Next.js 16.4.1, React 19.3, @supabase/ssr 0.12.7, and @supabase/supabase-js (current, installed alongside @supabase/ssr). Do not generate Pages Router, middleware.ts, or @supabase/auth-helpers-nextjs.

Enable RLS on every table in public. Add policies only for the operations clients should be allowed to do, because any operation without a policy is denied. Do not add a select, insert, update, and delete policy on every table. A missing policy denies that operation. A table without RLS is readable and writable through the Data API under the default grants. The env name from the current docs is NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY. The secret key and the service-role key stay server-only, with no NEXT_PUBLIC_ prefix. Put the policies in a migration file. The done checklist says new tables have RLS enabled plus policies, in a migration file.

Select named columns. Do not .select("*") and do not return the whole row to the client. The sample in the pack reads notes with .select("id, title, created_at") and .eq("user_id", claims.sub). That query is not a policy. The policies decide which operations the Data API allows. Do not add select("*") to get a column you forgot. Name the column.

Validate mutation input with Zod, then check getClaims(), then write. Return a typed { ok, error } object, not a thrown string. An action is a public HTTP endpoint even if no button calls it, so every action re-checks auth with getClaims(). getClaims() is how you decide whether a request is allowed. getSession() only reads the raw token, and you never trust session.user for authorization. That split is /fixes/supabase-ssr-getsession-vs-getclaims. The interceptor filename is a different trap, on /fixes/nextjs-16-middleware-to-proxy.

Last verified 2026-10-15. doc-verified; TypeScript snippets pass tsc --strict (TypeScript 5, @types/react 19) with zod and the Supabase client stubbed.

Next.js + Supabase pack — Next.js 16.4 and Supabase SSR, with the APIs that actually work.

doc-verified; TypeScript snippets pass tsc --strict (TypeScript 5, @types/react 19) with zod and the Supabase client stubbed

Sample from the pack

The named-column select in the Next.js + Supabase rules file.

const { data, error } = await supabase
  .from("notes")
  .select("id, title, created_at")
  .eq("user_id", claims.sub)

Sources in the pack

Questions

What happens if a Supabase table has RLS and no policy?
Enable RLS on every public table and add policies only for the operations clients should be allowed to do, because any operation without a policy is denied. A table without RLS is readable and writable through the Data API under the default grants.
What if the Supabase table has no RLS?
A table without RLS is readable and writable through the Data API under the default grants. Enable RLS on every public table and add policies only for the operations clients should be allowed to do, because any operation without a policy is denied.
Is .select("*") allowed on a Supabase query?
No. Select named columns. Do not .select("*") and do not return the whole row to the client. The sample selects id, title, and created_at.
When was this Supabase RLS rule last checked?
Last verified 2026-10-15. doc-verified; TypeScript snippets pass tsc --strict (TypeScript 5, @types/react 19) with zod and the Supabase client stubbed.