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

SQL
-- 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 usedRoleWith RLS off
Anon key (in your app)anonFull access the grants allow: usually select, insert, update and delete
User's access tokenauthenticatedSame, for every user, on every row
Service role key (server only)service_roleBypasses 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:

Terminal
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:

  1. "My query returns no rows." RLS is on and no policy allows the request.
  2. "My insert fails with a policy violation." There's a using policy but no with check that matches.
  3. "It's public data anyway." Product catalogs, blog posts, a public leaderboard.
  4. "Only my backend uses this table."
  5. "I'm just prototyping."
  6. "The policy makes queries slow."

Each has a better answer than turning RLS off.

#Safer options, reason by reason

ReasonWhat to do instead
No rows come backAdd a select policy for the access you want. Empty results with RLS on are the safe default, not a bug.
Inserts failAdd with check (or a separate for insert policy) that matches the row you insert, for example user_id = auth.uid().
Public dataA read-only policy: for select to anon, authenticated using (true). Writes stay blocked.
Only the backend uses itKeep 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.
PrototypingWrite the owner-only policy now. It's four lines, and prototypes become production.
Slow policiesIndex the columns the policy filters on, and wrap function calls in a subquery so they run once per query.

#Public read, no public write

SQL
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:

SQL
-- 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

SQL
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:

SQL
-- 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 security is 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_invoker is 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 definer functions 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 push and 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:

Terminal
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.