Your app feels secure because users must log in, but without supabase rls anyone with the anon key can read every row in your public tables.
Preview saves work. The dashboard shows data. You ship to production and assume the login wall protects customer records. It does not. The Supabase client in your browser ships with a public anon key. Without row level security, that key is enough to query tables through the REST API. Security researchers and scrapers do not need your UI.
Your database is public by default
Supabase creates Postgres tables with Row Level Security disabled unless you turn it on. The anon key is designed for browser use. It is not a password. It identifies your project and applies whatever policies you define. No policies means open access within the table grants Supabase sets for the anon role.
What attackers or curious visitors can do without logging in:
- List every row in exposed tables with a direct REST call
- Insert spam rows if INSERT is granted and no policy blocks it
- Update or delete data when policies and grants allow it
- Download fields you thought were hidden behind React state
Your React app only fetches rows for the logged-in user because you wrote the query that way. The API does not enforce that filter until RLS adds it on the server. Anyone can replay the same endpoint with different parameters.
AI coding tools generate supabase.from('notes').select('*') and auth screens. They skip the SQL policy step because it is invisible in the UI. You discover the gap when someone pastes your project URL into a script.
Row level security is still off on your tables
Enabling RLS is a per-table switch in Supabase. It is not global. You can lock profiles and leave messages wide open by mistake. The Table Editor shows a shield icon when RLS is on.
Check in the Supabase Dashboard:
- Open Table Editor and select each user-data table.
- Look for RLS enabled on the table header or open Authentication β Policies.
- Any table without the shield is public to the anon key within Postgres grants.
Or run in the SQL editor:
select tablename, rowsecurity
from pg_tables
where schemaname = 'public';
rowsecurity = false means anyone with the anon key can access rows according to role grants. Turning RLS on without policies blocks everything, including your app, until you add allow rules.
Login alone does not flip this switch. Supabase auth gets users a session. RLS decides which rows that session may touch. Both must be configured for production.
Write an insert policy so saves work again
After you enable RLS, inserts often fail with new row violates row-level security policy. That is expected. Default deny is the safe posture. You must add policies for SELECT, INSERT, UPDATE, and DELETE as needed.
Typical pattern for user-owned rows:
alter table public.notes enable row level security;
create policy "Users read own notes"
on public.notes for select
using (auth.uid() = user_id);
create policy "Users insert own notes"
on public.notes for insert
with check (auth.uid() = user_id);
Common mistakes after enabling RLS:
- INSERT policy missing β saves fail silently in the UI
user_idnot set on insert β policy check fails- Policy references
auth.uid()but client uses anon role without login - Only SELECT policy added β reads work, writes do not
- Service role key used in the browser β bypasses RLS and leaks god-mode access
Test each policy as an authenticated user in the SQL editor or with the API using a real session JWT, not the service role. Preview may use a service role in server code while production uses anon in the client β behavior diverges the same way auth URLs do.
Why AI builds skip supabase rls
Policies are SQL in the dashboard, not JSX. The model optimizes for visible features: forms, lists, auth buttons. RLS does not appear in the component tree. Generators sometimes paste the anon key into frontend code and call the job done.
Supabase warns in docs that RLS is your responsibility. The warning is easy to miss when the preview already “works.” Treat policies as part of the schema, checked into migrations, reviewed before every deploy.
Enable supabase rls before your live URL is public
Work table by table. Start with tables that hold PII, billing state, or user content.
- Inventory every
publictable your app reads or writes. - Enable RLS on each table that should not be world-readable.
- Add SELECT policies scoped to
auth.uid()or membership rules. - Add INSERT policies with
with checkso users cannot assign rows to someone else. - Add UPDATE and DELETE policies if your app needs them.
- Verify the anon key cannot list rows when logged out (curl or REST client).
- Run the app logged in and confirm normal flows still work.
Ship policies in migration files under version control, not only as manual dashboard clicks. Staging and production then stay aligned.
RLS is one of five production settings AI editors rarely prompt for. Auth URLs, Stripe webhooks, env vars, and TLS fail on the same launch weekend. The lovable deploy checklist covers the rest so you are not fixing leaks one surprise at a time.
Pre-launch checklist
- RLS enabled on every table with user or private data
- SELECT, INSERT, UPDATE, DELETE policies match how the app actually behaves
- Logged-out requests with the anon key return empty or 401, not full tables
- Service role key exists only on the server, never in frontend bundles
- Policies tested against production project, not a duplicate dev project
- Migrations committed so the next deploy does not wipe manual dashboard work
The anon key is public by design. Row level security is the lock on the door. Without it, login is theater.
FAQ
Is my Supabase database public if I never enabled RLS?
Yes. Tables without row level security allow any request signed with your anon key to read or write rows, subject only to Postgres grants. The anon key is embedded in your frontend bundle.
Why do inserts work in preview but fail after I enable supabase rls?
RLS blocks all rows by default when enabled. You must add an INSERT policy that allows authenticated users to insert their own rows. Without it, Supabase returns permission denied even though the same code worked before.
Does supabase rls replace authentication?
No. Auth proves who the user is. RLS decides which rows that user can see or change. You need both: auth for login and policies that tie rows to auth.uid().