How to migrate from Supabase to jerrycan

A step-by-step guide to moving a Supabase project onto jerrycan: export the schema, run one migrate command, and get a compiled Rust backend you own, with your users' passwords and row-level security intact.

Most “migrate off Supabase” guides end with you running the whole Supabase platform yourself. This one does not. jerrycan migrate reads an export of your Supabase project and translates it into a compiled Rust backend you own: your schema becomes typed models, your row-level security becomes tenant guards with tests, and your users keep their existing passwords. It is one command, and it never guesses. Anything it cannot translate safely is written into a report for you to resolve, not silently dropped.

If you are still deciding, jerrycan vs Supabase covers the trade-offs, and the self-hosting cost breakdown explains why running Supabase yourself is the expensive path.

Key takeaways

  • One command turns a Supabase export into a scaffolded, compiling jerrycan app.
  • Tables become entities, foreign keys become relations, and RLS becomes tenant isolation that is verified by generated cross-tenant tests.
  • Migrated users log in with their existing passwords. Their bcrypt hashes are preserved and upgraded to argon2 on the next login.
  • No Supabase secret is ever copied into the new app. You rotate them.
  • What cannot be translated safely (Edge Functions, custom RLS, plpgsql) becomes a machine-readable gap report, so nothing ships as a guess.

What “lossless” means here

The migration is deterministic. Your schema, foreign keys, enums, indexes, unique constraints, the canonical row-level security policies, auth.users, storage buckets, the realtime publication, and pg_cron jobs all translate mechanically into a jerrycan design. The correctness backstop is not trust, it is tests: every tenant-scoped table gets generated isolation tests plus cross-tenant negative controls. If a policy translated wrong, the gate goes red before you ship.

Step 1: export your Supabase project

The supported path is offline. You produce a directory with supabase, psql, and pg_dump, and never point the tool at a live database. Set DB_URL to your Supabase connection string first.

mkdir -p export/data export/storage export/functions
cd export

# 1. Schema (required): public + auth + storage.
supabase db dump --schema public,auth,storage -f schema.sql
# or: pg_dump --schema-only --schema=public --schema=auth --schema=storage "$DB_URL" > schema.sql

# 2. Table data as CSV, one file per table (\N marks NULL).
psql "$DB_URL" -c "\copy (select * from public.leads) to 'data/public.leads.csv' with (format csv, header true, null '\N')"
# also dump auth.users and auth.identities: data/auth.users.csv, data/auth.identities.csv

# 3. Storage bucket config (object bytes are optional).
psql "$DB_URL" -Atc "select coalesce(json_agg(b), '[]'::json) from storage.buckets b" > storage/buckets.json

# 4. Scheduled jobs (pg_cron).
psql "$DB_URL" -c "\copy (select jobname, schedule, command from cron.job) to stdout" > cron.sql

You end up with this layout. schema.sql is the only required file; the rest fill in data, files, functions, and schedules if you have them.

export/
  schema.sql                     # required
  data/<schema>.<table>.csv      # \N = NULL, header row present
  storage/
    buckets.json                 # storage.buckets as a JSON array
    objects/<bucket>/<key>       # object bytes (optional)
  functions/<name>/index.ts      # Edge Function sources (become gap items)
  cron.sql                       # pg_cron jobs

Step 2: run the migration

jerrycan migrate --from supabase export --out ./my-app --name my-app
cd my-app
jerrycan db migrate     # bring the schema up
jerrycan db seed        # apply the streamed seed (resumable, safe to re-run)
jerrycan gen-tests --module leads
jerrycan check          # green gate, including generated cross-tenant isolation tests

migrate writes a scaffolded app: a design.json, a streamed and resumable data seed, a machine-readable gap-report.json, and a MIGRATION.md with a secret-rotation checklist. check compiles the whole thing and runs the generated tests. A green gate means the translated schema and isolation rules actually hold.

What migrates automatically

SupabaseBecomes in jerrycan
Tables, columns, typesEntities and typed fields in modules
Foreign keysbelongs_to relations with on_delete
Enums and CHECK IN (...)Field values
Unique constraints, indexesField flags
Canonical RLS (owner, membership, folder-per-user, public read)Tenant guards plus isolation tests
auth.usersA users module, JWT auth, and a user seed that preserves bcrypt hashes
storage.bucketsThe storage block with the same owner and visibility rules
supabase_realtime publicationThe realtime.changes block
pg_cron jobsScheduled jobs[]

Integer and UUID user identities both migrate. A Supabase auth.users.id UUID round-trips through the session, JWT, and tenant guard exactly like an integer primary key does.

Step 3: work the gap report

jerrycan migrate will not invent behavior it cannot verify. Everything it could not translate safely lands in gap-report.json as an actionable item, sorted so the blocking ones come first. Typical entries:

  • Non-canonical RLS it did not recognize. It tells you the exact policy and suggests implementing it as a handler guard, rather than guessing at your intent.
  • Edge Functions (Deno TypeScript). These become handlers or jobs you port, with the original source referenced.
  • plpgsql functions and triggers, ported to Rust with your judgment.
  • Realtime Broadcast and Presence topics, which live in your client code and get reconstructed against jerrycan’s realtime channels.

Work it top down and let jerrycan check tell you when the app is whole again.

Passwords and secrets

Two details make the difference between “your users keep working” and “everyone must reset their password.”

  • Passwords are preserved. Supabase stores bcrypt hashes. The migrated user seed keeps them, jerrycan-auth verifies bcrypt for migrated users, and it transparently upgrades the hash to argon2 on the next successful login. Nobody gets logged out.
  • Secrets are never copied. Your Supabase JWT secret, anon key, and service-role key are never written into the new app. MIGRATION.md gives you a rotation checklist so you set fresh values. Migrated data is scanned too, so a stray secret sitting in a data column is flagged rather than embedded.

Repoint the frontend and deploy

The migrator does not touch your frontend. MIGRATION.md includes an endpoint map from your old Supabase calls to the new jerrycan routes, so you repoint your client to your own backend. From there it is the normal jerrycan loop: jerrycan package produces a container, a hardened Kubernetes manifest, and an SBOM, and your backend runs on your own server. No dashboard, no platform bill.

Frequently asked questions

Will my users have to reset their passwords? No. Bcrypt hashes migrate as-is and log in unchanged, then upgrade to argon2 on the next login.

Does it migrate row-level security? Yes, for the canonical policy shapes (owner, membership-join, folder-per-user, public read, authenticated). Anything non-standard is reported, never guessed, and the generated cross-tenant tests prove the result.

What about my data and stored files? Table rows migrate through the seed. Storage bucket configuration migrates, and object bytes copy over when you include them in the export.

Is jerrycan actually open source? Yes. It is MIT licensed, memory-safe Rust, and every crate is published on crates.io. You own the code and the data.

Point your AI agent at your export directory, ask it to migrate your Supabase project, and it can drive this whole loop for you. When it is done, you have the same functionality, running on a backend that is yours.

try the quickstart → more posts