Translating Firebase rules thinking into RLS thinking
Teams arriving at Supabase from Firebase carry a mental model that is mostly right and precisely wrong in the places that matter. Firestore security rules and Postgres Row Level Security both implement deny-by-default authorisation close to the data, and the intuition transfers. The grammar does not: paths versus predicates, document scope versus relation scope, and above all the activation story — Firestore enforces its rules invisibly and immediately; Postgres requires you to switch RLS on per table before any of it engages.
This page is the translation guide: concept mappings that hold, the places Firebase instincts mislead, a worked example converting a representative rule into policies, and a migration order that avoids the classic outage. Honesty first, since this page guides process rather than a scan: RowShield does not automate Firebase migration planning, and nothing here implies a checker for your rules files — the automated parts begin once you have a Supabase project to audit.
RowShield does not detect this yet. This guide gives you the catalog queries to check it yourself. The nine rules that do ship are listed on the rules index.
Two grammars for one problem
The concepts map cleanly even where the syntax diverges. Firestore's request.auth.uid comparisons become predicates comparing auth.uid() with an ownership column. Paths like /users/{uid}/documents become a documents table with a user_id column — hierarchy flattens into foreign keys. Signed-in checks translate to targeting policies at the authenticated role. Deny-by-default holds in both systems, with one enormous caveat covered below.
Validation logic differs most. Firestore rules routinely embed data validation — type checks, field constraints — because there is no other enforcement layer. In Postgres that responsibility splits: NOT NULL, CHECK constraints and foreign keys handle structure declaratively, while policies handle only authorisation. Ported rules that validate shape inside policy expressions fight the database; the same checks expressed as constraints are faster and clearer.
Where Firebase thinking misleads
First and largest: enforcement is not automatic. A Firestore database denies unauthorised reads the moment it exists; a Postgres table with RLS disabled grants everything the schema grants allow — and Supabase schemas grant the anon role generous read access by design. Migrated data landing in plain tables is world-readable until ENABLE ROW LEVEL SECURITY runs per table. The default you trusted in Firebase must be constructed in Postgres.
Second: granularity inverts. Firestore rules attach to documents along paths; RLS attaches to relations with predicates over columns, so "this document belongs to this user" becomes "every row carries its owner, and policies filter on it" — which demands the ownership column actually exist and be populated. Third: WITH CHECK has no Firestore analogue, so write-path authorisation feels optional and is not. Fourth: testing differs — you cannot reason about RLS from the client alone, because empty results ambiguously mean denied or empty; testing means issuing requests as the anon identity and reading the catalog.
A worked translation
Consider a representative Firestore rule guarding user-owned documents — the shape most migrated apps carry somewhere in their history. It reads plainly, enforces immediately, and translates into two Postgres statements plus one habit worth naming. The original appears first as a comment; the policies beneath it are the working equivalent:
Three details carry the weight of the translation. Targeting the policies at the authenticated role replaces the signed-in requirement. The USING clause governs reads while WITH CHECK governs writes — replacing the single allow expression. And the wrapped subquery form of auth.uid() is deliberate: semantics unchanged, planning materially better on large tables.
-- Firestore original, guarding user-owned documents:
-- match /documents/{docId} {
-- allow read, write: if request.auth.uid == resource.data.ownerId;
-- }
-- Supabase equivalent, assuming a flattened table:
CREATE POLICY "documents_owner_select"
ON public.documents
FOR SELECT
TO authenticated
USING ((SELECT auth.uid()) = owner_id);
CREATE POLICY "documents_owner_write"
ON public.documents
FOR UPDATE
TO authenticated
USING ((SELECT auth.uid()) = owner_id)
WITH CHECK ((SELECT auth.uid()) = owner_id);A migration order that avoids the outage
Migrate schema first with ownership columns populated and verified — a policy filtering on an empty column locks everybody out politely and totally. Then write policies while reads still run through the old system, so there is no time pressure. Enable RLS per table together with that table's policies, never globally in advance. Finally, test each table as an anonymous caller before cutting traffic, expecting permission errors where the old system denied and rows only where it allowed.
Once the Supabase side exists, verification automates well: RowShield's catalog audit evaluates RLS state and policy correctness on every scan, the probe confirms what an anonymous caller can actually read on the deployed app, and drift alerts catch the tables added during migration crunch that never received their policies. It is an independent product, unaffiliated with and not endorsed by Supabase, reading pg_catalog metadata only — never rows — and the planning judgment above remains human work by design.
Frequently asked
- Is Postgres RLS deny-by-default like Firestore rules?
- Only once enabled: an RLS-enabled table denies every row to non-owner roles until a policy grants access, matching Firestore instincts. A table with RLS disabled is the opposite — grants decide, and Supabase grants anon broad read by design — so enablement per table is the first habit to port from Firebase thinking.
- Do I still need policies if my server uses admin credentials?
- Server-side code holding service_role or direct database credentials bypasses RLS entirely, so policies protect nothing against it — but the moment any client-side code talks to Supabase, which is the norm after migration, policies are the only thing bounding the public key. Write them for the surface that exists, not the one you hope to avoid.
- Can I convert my rules file mechanically?
- Small portions translate directly — ownership comparisons become predicates — while path hierarchies flatten into columns and validation migrates into constraints, requiring judgment per collection. Treat mechanical conversion as drafting, then review every output against the actual table design before enabling anything in production.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit