Skip to content

How to Copy RLS Policies Between Supabase Projects

Published Jul 5, 2026 · 9 min read

To copy RLS policies between Supabase projects, export them from the source database — either as CREATE POLICY statements generated from the pg_policies catalog view, or as part of a schema-only pg_dump / supabase db dump, which includes policies by default — and apply the SQL to the target project. The catch is that policies rarely travel alone: they depend on functions referenced in their USING and WITH CHECK clauses, on ALTER TABLE ... ENABLE ROW LEVEL SECURITY, and on grants to the anon, authenticated, and service_role roles. This guide covers all three export paths, the dependency order that makes restores succeed, and how to verify the policies actually arrived.

Why do RLS policies go missing when you recreate a Supabase schema?

Row Level Security is the part of a Supabase schema that fails silently. If you recreate tables by hand in a new project — from memory, from the table editor, or from an incomplete SQL snippet — three things commonly go wrong:

  1. RLS is enabled but no policies exist. With ENABLE ROW LEVEL SECURITY and zero policies, Postgres denies all rows to non-superusers. Your app returns empty arrays instead of errors, which looks like a data bug, not a security config bug.
  2. RLS is not enabled at all. The opposite failure: the table works perfectly in staging — because every request can read every row. You find out when the Supabase security advisor flags it, or worse, when someone else does.
  3. Policies exist but reference missing functions. A policy like USING (public.is_org_member(org_id)) fails to create if the helper function was never copied, and the whole restore script aborts halfway through.

Because RLS lives in the database rather than in your application code, it is invisible in code review and easy to forget. That is why every serious approach to cloning a Supabase project treats policies as first-class objects, not an afterthought.

How do I see all RLS policies in a Supabase project?

Every policy in the database is visible in the pg_policies system view. Run this in the SQL editor of the source project:

select schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename, policyname;
  • qual is the deparsed USING expression, with_check the WITH CHECK expression.
  • roles is the array of roles the policy applies to — in Supabase typically {authenticated}, {anon}, or {public}.
  • cmd is the command (SELECT, INSERT, UPDATE, DELETE, or ALL), and permissive tells you whether the policy grants or restricts access.

pg_policies only shows policies — it does not tell you whether RLS is actually enabled on each table. Check that separately via pg_class:

select n.nspname as schema, c.relname as table, c.relrowsecurity as rls_enabled
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r'
order by c.relname;

A table with rls_enabled = false is fully exposed to the API roles regardless of what policies you copy over.

How do I export RLS policies as SQL from pg_policies?

You can generate CREATE POLICY statements directly from the catalog:

select format(
  'create policy %I on %I.%I as %s for %s to %s%s%s;',
  policyname, schemaname, tablename,
  lower(permissive), lower(cmd),
  array_to_string(roles, ', '),
  coalesce(' using (' || qual || ')', ''),
  coalesce(' with check (' || with_check || ')', '')
) as ddl
from pg_policies
where schemaname = 'public';

Run the output against the target project, preceded by alter table <table> enable row level security; for every table that had it enabled.

Two honest caveats with this approach. First, qual and with_check are deparsed expressions: Postgres normalizes what you originally wrote, so the text may be schema-qualified or parenthesized differently than your migration files — fine for execution, annoying for diffing. Second, this exports only policies. Functions, triggers, and grants need their own extraction, which is exactly where hand-rolled scripts start to sprawl.

How do I export RLS policies with pg_dump?

pg_dump includes row security policies in schema dumps by default — there is even a dedicated --no-policies flag to turn them off, which confirms they are on unless you say otherwise. A schema-only dump gives you policies, functions, triggers, indexes, and grants in one correctly ordered file:

# Via the Supabase CLI (recommended — knows which schemas Supabase manages)
supabase db dump --db-url "$SOURCE_DB_URL" -f schema.sql

# Or with plain pg_dump, limited to your application schema
pg_dump "$SOURCE_DB_URL" --schema-only --schema=public -f schema.sql

In the resulting file, each policy appears as a CREATE POLICY statement alongside ALTER TABLE ... ENABLE ROW LEVEL SECURITY, and functions appear as CREATE FUNCTION before the policies that reference them. Apply it to the target with psql --single-transaction --file schema.sql.

Don't confuse this with --enable-row-security, which controls whether data dumps respect RLS during export — it has nothing to do with whether policy definitions are included.

The classic pg_dump pitfall on Supabase: dumping the entire database drags in the managed auth, storage, and realtime schemas, and restoring those into another Supabase project produces "already exists" and permission errors. And if you customized the auth or storage schemas — say, a trigger on auth.users that creates a profile row — Supabase's own backup-and-restore guide says you must restore those changes separately (supabase db diff --schema auth,storage helps you find them). We cover these failure modes in depth in our pg_dump and pg_restore guide for Supabase.

How do I copy policies and functions with Supabase migrations?

If you maintain migrations, the target project should get its policies the same way the source did:

supabase link --project-ref <source-ref>
supabase db pull          # writes the current remote schema to a migration file

supabase link --project-ref <target-ref>
supabase db push          # applies migrations to the target

supabase db pull captures policies, functions, and triggers that were created through the dashboard and never made it into a migration file — a very common drift source. The limitation is timing: migrations only stay authoritative if every future change goes through them. One quick "temporary" policy edit in the dashboard and your source and target quietly diverge again, which matters a lot once you run a real dev → staging → prod workflow.

What do RLS policies depend on?

Policies are leaf nodes in a dependency graph. Copy them in the wrong order and the restore aborts. The order that works:

  1. Extensions and enums — types used in table columns and function signatures.
  2. Tables — a policy cannot exist without its table.
  3. Functions — anything referenced in USING / WITH CHECK must exist before CREATE POLICY runs, or you get ERROR: function public.is_org_member(uuid) does not exist. Built-ins like auth.uid() and auth.jwt() exist in every Supabase project, so those are safe; your own helper functions (security definer role checks, tenant lookups) are the ones that go missing.
  4. Policies and triggers — triggers additionally depend on their trigger functions, so "copy the functions too" applies twice.

This is why "supabase copy database functions" and "supabase copy rls policies" are really the same task: a schema-level export (pg_dump, supabase db dump, or a full clone) keeps the graph intact, while cherry-picking policies out of pg_policies forces you to reassemble it by hand.

What about grants and the anon, authenticated, and service_role roles?

RLS decides which rows a role can see; GRANT decides whether the role can touch the table at all. Supabase provisions anon, authenticated, and service_role in every project, so the roles themselves already exist in your target — but the grants on your tables, functions, and sequences do not appear until you copy them:

grant usage on schema public to anon, authenticated;
grant select, insert, update, delete on all tables in schema public to authenticated;

A schema dump includes GRANT/REVOKE statements unless you pass -x/--no-privileges, so pg_dump-based copies handle this for you. Two role facts worth remembering while testing: service_role bypasses RLS entirely (so it proves nothing about your policies), and default privileges (ALTER DEFAULT PRIVILEGES) affect only tables created after they run.

How do I verify the policies actually arrived?

Never assume a restore worked because it printed no errors — psql without ON_ERROR_STOP happily continues past failed statements. Verify with data:

-- Run on BOTH projects and diff the output
select tablename, count(*) as policies,
       array_agg(policyname order by policyname) as names
from pg_policies
where schemaname = 'public'
group by tablename
order by tablename;

Then check the three things counts don't catch:

  • RLS enablement: compare the pg_class.relrowsecurity query from above on both sides.
  • Expressions: diff qual and with_check per policy — a policy with the right name and wrong expression is the worst outcome, because it looks migrated.
  • Behavior: make one API request as anon and one as an authenticated user against the target. Empty results where you expect rows means a missing grant or an over-restrictive policy.

Is there an automated way to copy RLS policies with verification?

SupaClone exists because this checklist is exactly the kind of work that gets done correctly once and sloppily every time after. It clones a Supabase project's structure — schemas, tables, RLS policies, functions, triggers, indexes, views, enums, and extensions — into a fresh, empty target project using native pg_dump/pg_restore with a baseline-aware plan that skips the Supabase-managed auth, storage, and realtime schemas correctly. You connect both projects via Supabase OAuth, no connection strings.

The part that matters for RLS: every run ends with a field-by-field verification — each policy, function, and trigger in the source is checked against the target — and a full report of what was cloned, skipped, or failed, plus manual steps for things that can't be copied (secrets never are). Instead of eyeballing two pg_policies outputs, you get the diff done for you.

Hand-written SQL (pg_policies)pg_dump / psqlCLI migrationsSupaClone
RLS policiesManual reassemblyIncludedIncluded after db pullIncluded
Functions & triggersSeparate extractionIncludedIncluded after db pullIncluded
Managed schemas (auth/storage)N/AManual exclusion neededHandled by CLISkipped automatically
Grants for anon/authenticatedManualIncluded (unless -x)Included if migratedIncluded
Dashboard-made driftCapturedCapturedOnly after re-pullCaptured
Verification that policies matchManual diffManual diffManual diffAutomatic, field-by-field

Pro is $7/month, $70/year, or $84 lifetime, and the 14-day free trial includes one full successful clone — enough to move every policy, function, and trigger into a new staging environment and read the verification report before paying anything.

FAQ

Does pg_dump include RLS policies?

Yes. pg_dump includes row security policies in schema dumps by default, as CREATE POLICY statements together with ALTER TABLE ... ENABLE ROW LEVEL SECURITY. The --no-policies flag excludes them; --enable-row-security is unrelated — it only controls whether data dumps respect RLS.

How do I list all RLS policies in a Supabase project?

Query the pg_policies system view: select * from pg_policies where schemaname = 'public';. It shows each policy's table, name, roles, command, and the USING/WITH CHECK expressions. Check whether RLS is enabled per table separately via pg_class.relrowsecurity.

Why does my copied policy fail with "function does not exist"?

The policy's USING or WITH CHECK clause references a helper function that was not copied first. Create all custom functions before running CREATE POLICY. Supabase built-ins like auth.uid() exist in every project and never cause this.

Does copying RLS policies also copy my data?

No. Policies, functions, and triggers are schema objects; a schema-only dump or a SupaClone run transfers structure without any rows. Data transfer is a separate step (SupaClone's data cloning is coming soon).

Do I need to re-enable RLS on the target after copying policies?

If you restored a full schema dump, the ENABLE ROW LEVEL SECURITY statements are included. If you copied policies individually from pg_policies, you must run alter table <table> enable row level security; yourself — policies on a table with RLS disabled have no effect.

Clone your Supabase project without the pg_dump pitfalls

SupaClone copies schemas, tables, RLS policies, functions, and triggers into a fresh project — verified after every run. 14-day free trial, 1 clone included.

Start free trial