Should You Disable Row Level Security? (And Safer Options)
To disable Row Level Security on a table, run alter table public.your_table disable row level security;. On a Supabase-style backend, though, that makes every row of the table readable and writable by anyone who has your anon key, and the anon key ships in your frontend. For any table in an exposed schema, a better fix almost always exists: a permissive policy for public data, the service role on the server, or a table outside the API.
This article covers the commands, what exactly turns off, the few cases where disabling RLS is fine, and a table of safer options for each reason people reach for the switch.
#How to disable row level security
-- Turn RLS off for one table
alter table public.todos disable row level security;
-- Turn it back on
alter table public.todos enable row level security;
-- Check which tables in public have RLS off
select relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r' and not c.relrowsecurity;Disabling RLS doesn't delete your policies. They stay on the table but are ignored until you enable RLS again. You can confirm with select * from pg_policies where tablename = 'todos';.
#What "RLS off" actually means here
In plain Postgres, RLS off just means normal grants decide access. That's fine when only your own server connects.
A Supabase-style backend is different. The REST API is public, and every request runs as one of three database roles:
| Key used | Role | With RLS off |
|---|---|---|
| Anon key (in your app) | anon | Full access the grants allow: usually select, insert, update and delete |
| User's access token | authenticated | Same, for every user, on every row |
| Service role key (server only) | service_role | Bypasses RLS anyway |
The default setup grants anon and authenticated access to tables in public, with RLS as the thing that narrows it down. Take RLS away and this works from any browser:
curl 'https://<project-ref>.p.stitchdb.com/rest/v1/todos?select=*' \
-H "apikey: <anon key copied from your site's JavaScript>"So does a DELETE without a filter. This is the most common data leak in apps built quickly on this stack, and it's why tools and dashboards warn loudly about tables with RLS off.
#Why people turn RLS off
Usually one of these:
- "My query returns no rows." RLS is on and no policy allows the request.
- "My insert fails with a policy violation." There's a
usingpolicy but nowith checkthat matches. - "It's public data anyway." Product catalogs, blog posts, a public leaderboard.
- "Only my backend uses this table."
- "I'm just prototyping."
- "The policy makes queries slow."
Each has a better answer than turning RLS off.
#Safer options, reason by reason
| Reason | What to do instead |
|---|---|
| No rows come back | Add a select policy for the access you want. Empty results with RLS on are the safe default, not a bug. |
| Inserts fail | Add with check (or a separate for insert policy) that matches the row you insert, for example user_id = auth.uid(). |
| Public data | A read-only policy: for select to anon, authenticated using (true). Writes stay blocked. |
| Only the backend uses it | Keep RLS on with no policies and use the service role key on the server, or move the table to a schema the API doesn't expose. |
| Prototyping | Write the owner-only policy now. It's four lines, and prototypes become production. |
| Slow policies | Index the columns the policy filters on, and wrap function calls in a subquery so they run once per query. |
#Public read, no public write
alter table public.products enable row level security;
create policy "Anyone can read products" on public.products
for select to anon, authenticated
using (true);
-- No insert/update/delete policy: only the service role can change products.This gives you exactly what "it's public anyway" meant, without letting anyone with the anon key rewrite your prices.
#Backend-only tables
Two good options:
-- Option 1: RLS on, no policies. The API returns nothing to anon/authenticated;
-- your server uses the service role key, which bypasses RLS.
alter table public.webhook_events enable row level security;
-- Option 2: keep the table out of the API entirely.
create schema if not exists internal;
alter table public.webhook_events set schema internal;A schema that isn't in the API's exposed schemas can't be reached through REST at all. Your server reaches it over a normal Postgres connection.
#Owner-only access in four lines
create policy "Users manage their own rows" on public.notes
for all to authenticated
using (user_id = auth.uid())
with check (user_id = auth.uid());#Fast policies
A policy runs as a filter on every query. Two habits keep it fast:
-- Index what the policy compares
create index on public.notes (user_id);
-- Evaluate auth.uid() once per statement instead of once per row
create policy "Users read their own notes" on public.notes
for select to authenticated
using (user_id = (select auth.uid()));If a policy needs a join (for example "member of this team"), put the lookup in a security definer function with a fixed search_path, revoke execute from public, and grant it only to the roles that need it.
#When disabling RLS is actually fine
- The table is not in an exposed schema and only trusted server code connects. RLS adds little there. Many teams still keep it on as a second layer.
- A one-off maintenance script running as the owner. Even then you don't need to disable RLS: table owners bypass it unless
force row level securityis set, and the service role bypasses it too. - Local development with throwaway data, as long as the migration you ship turns it on. Most leaks start as "I'll add policies later".
One related setting is worth knowing: alter table ... force row level security; makes RLS apply to the table owner as well. Useful when your app connects as the owner role and you want policies to protect you from your own bugs.
#Other ways RLS gets bypassed without anyone disabling it
- Views. A view runs with its owner's rights unless
security_invokeris on, so it can expose rows that the table's policies hide:alter view public.my_view set (security_invoker = on);. - Materialized views have no RLS at all. Don't put them in an exposed schema if they contain private data.
security definerfunctions callable through the API run as their owner. Check who can execute them.- A leaked service role key. It bypasses every policy. Keep it out of frontend bundles and mobile apps.
#How StitchDB handles this
StitchDB uses the same roles and the same auth.uid() / auth.jwt() functions, so every policy above works unchanged (compatibility). It adds a few guard rails aimed squarely at the "RLS off" problem:
- RLS Guard turns Row Level Security on for every new table in
public, however it's created: SQL editor, migrations, an ORM or an AI tool. It's on by default for new projects (exposure check). - Block exposing migrations. With this setting on,
stitchdb db pushand the migrations API refuse a migration that leaves a new critical exposure, such as a table with RLS off. - Exposure check lints your policies (RLS off, always-true conditions, writable-by-anyone, views running with owner rights, public buckets) and runs a live test per table as an anonymous visitor and as a random signed-in user, inside transactions that are rolled back.
- Check access in Studio runs every policy as a chosen user for a chosen row and explains in plain words which policy let them in or kept them out (Row Level Security). That's usually faster than turning RLS off to see whether it's the cause.
- Advisors flag tables with RLS off, views that bypass RLS, sensitive-looking columns the API can read, and realtime tables without RLS (advisors).
Turning the guard and blocking on for an existing project:
curl -X PATCH "https://stitchdb.com/platform/v1/projects/<ref>/security/settings" \
-H "Authorization: Bearer $STITCHDB_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "rls_guard": true, "enforce": "block" }'Supabase has its own security advisor and RLS warnings in its dashboard, and both platforms let you write the same policies. Pick on other grounds; just don't ship a table with RLS off. For more patterns, see Row Level Security in Postgres and the Postgres security checklist.
#FAQ
#How do I disable RLS on a Supabase table?
Run alter table public.<table> disable row level security; in the SQL editor. Re-enable it with enable row level security. Your policies are kept either way.
#Is it safe to disable RLS if my app checks permissions?
No, not for tables in an exposed schema. Your app's checks don't apply to someone calling the REST API directly with your anon key.
#Why does my table return no rows after enabling RLS?
RLS with no matching policy denies everything to anon and authenticated. Add a select policy for the rows each role should see.
#Does the service role key respect RLS?
No. It runs as service_role, which bypasses RLS. That's why it belongs only on servers you control.
#Does disabling RLS delete my policies?
No. They stay attached to the table and take effect again when you re-enable RLS.
Start with RLS Guard on by default: create a free StitchDB project.