Tools / RLS converter

Supabase RLS converter

Convert Supabase row-level security to plain Postgres, in your browser. Your SQL never leaves this page.

Runs entirely in your browser (WebAssembly). The SQL you paste or add is never uploaded - the page downloads the converter once and does the rest locally.

Destination
Output convention

Policies move onto app.* accessor functions over transaction-local settings. The database stops knowing JWTs exist.

Role model

On CapyDB your app connects as the owner of its tables, so row security is FORCEd. Split roles need CREATE ROLE, which a CapyDB database role does not have.

User id type

Use text when your user ids are not uuids, such as Clerk's user_... ids.

01

What it converts

Supabase RLS is standard CREATE POLICY plus a context only Supabase provides: auth.uid() and auth.jwt(), the anon, authenticated and service_role roles, and a JWT injected into every request. That context layer, not the policies, is what keeps a project on Supabase.

The converter re-homes the whole layer so your policies run on plain Postgres and keep doing the same authorization work.

  • -auth.uid(), auth.jwt(), auth.role() and auth.email() become accessor functions over transaction-local settings, each JWT claim promoted to its own setting.
  • -TO anon and TO authenticated become predicates, and service_role becomes a service path that also passes restrictive policies.
  • -FOR ALL policies are split per command, and column defaults such as default auth.uid() are rewritten.
  • -The report lists the settings your application now owes the database - your integration checklist.

02

Two conventions

Vanilla, the default, is the idiomatic plain-Postgres setup: your app verifies the caller and states typed facts per transaction (app.user_id, app.org_id). Nothing in the database knows about JWTs.

Supabase-compat is the lift-and-shift: an auth.* shim over request.jwt.claims, so policies port nearly verbatim. Its service path is the claims role: set "role": "service_role" only from server code that used the service key.

Both run on CapyDB with the single role model: the cell's owner connects, row security is FORCEd, and no roles need to be created.

03

What it will not guess

Anything that cannot be ported mechanically is reported, not mistranslated:

  • -Policies that reference auth.users - that table stays on Supabase; port the columns you need first.
  • -Policies on Supabase-managed schemas such as storage and realtime.
  • -Function bodies that call auth.*, listed with the policies that depend on them.
  • -SECURITY DEFINER functions that write to a FORCEd table - they no longer bypass row security - with the fix. Writes through EXECUTE are not detected.

04

Nothing leaves your browser

The converter is capyrls, compiled to WebAssembly. The page fetches it once as a static file; your SQL is read from the text box and from files you pick, converted in the page, and offered back as downloads.

No account, no upload, no logging of what you paste. The same engine runs in the CLI if you would rather not use a browser at all.

05

The same converter on the command line

capydb migrate rls runs this converter against your repository or, most faithfully, against the live Supabase database. capyrls is MIT-licensed and works for any Postgres destination.

# from your repo (finds supabase/migrations) or straight from the live database
$ capydb migrate rls
$ capydb migrate rls --source-url "$SUPABASE_DIRECT_URL" --uid-type text

# standalone, MIT, any Postgres
$ go install github.com/capydatabase/capyrls/cmd/capyrls@latest
$ capyrls convert supabase/migrations