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
| Supabase | Becomes in jerrycan |
|---|---|
| Tables, columns, types | Entities and typed fields in modules |
| Foreign keys | belongs_to relations with on_delete |
Enums and CHECK IN (...) | Field values |
| Unique constraints, indexes | Field flags |
| Canonical RLS (owner, membership, folder-per-user, public read) | Tenant guards plus isolation tests |
auth.users | A users module, JWT auth, and a user seed that preserves bcrypt hashes |
storage.buckets | The storage block with the same owner and visibility rules |
supabase_realtime publication | The realtime.changes block |
| pg_cron jobs | Scheduled 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.mdgives 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.