Migrating

Move the project you already have.

Studio → Import connects to a Supabase project, a plain Postgres database, Appwrite, Firebase or a pg_dump file; shows you exactly what is in it; and copies what you tick while streaming progress. The part people get burned by is passwords, so that is where this page starts.

Passwords

Whether your users have to reset anything

Supabase and Postgres store bcrypt. Baselyra copies the hash verbatim behind a bcrypt$ marker, and when such a user signs in it asks Postgres to verify it — select $1 = crypt($2, $1) through pgcrypto, which is already installed. bcrypt is not reimplemented anywhere in this codebase. On the first successful sign-in the row is quietly re-hashed to scrypt, so the imported hashes retire themselves as your users come back. Nobody is emailed. The migration is invisible.

preserved

Supabase, Postgres, and bcrypt dumps

Everyone signs in with the password they already had. Nothing to announce, nothing to support, no drop-off.

lost

Appwrite

Appwrite’s Users API never returns a password hash, and it hashes with argon2 by default. Accounts arrive with email and metadata intact and an unusable password marker; recovery is the only way in.

lost

Firebase

Firebase uses a modified scrypt keyed with a per-project signer key that Google holds. Even with the hash there is nothing to verify against. Same outcome: recovery.

“Unusable” is precise, not vague. encrypted_password is set to a marker that parses as no scheme at all. Verification rejects it after burning an equivalent scrypt derivation, so the response timing does not distinguish those accounts from any other — which is what stops the import from becoming an account-enumeration oracle.
after an Appwrite or Firebase import
# One recovery email per imported address. The endpoint answers 200
# whatever happens, so a bad address in your list is not a failure.
while read EMAIL; do
  curl -sS -X POST "$URL/auth/v1/recover" \
    -H 'content-type: application/json' \
    -d "{\"email\":\"$EMAIL\"}"
done < addresses.txt

The run

Four calls, and one of them streams

SupabasePostgresAppwriteFirebaseSQL dumpPOST /admin/v1/import/testconnect and report the versionPOST /admin/v1/import/inspectlist tables, users, buckets, policiesPOST /admin/v1/import/runcopy the selection, streaming progressGET /admin/v1/import/historyevery run, with its warningsYour Baselyra projectpublic.*tables, types, keys, indexesauth.userswith bcrypt hashes where thesource stored bcryptstorage.objectsfiles and their metadataRLSon, on every imported tablepoliciestranslated where possibleWhat does not come— argon2 and scrypt passwords— database functions and triggers— extensions— edge / cloud functions— scheduled jobs— provider-specific settingsThe run log names every one it skipped.
All four require a Studio token or the service key; the three POST routes also require at least the admin platform role.
  1. Test the connection

    POST /admin/v1/import/test answers with the source’s version and whether the credentials work, before you have committed to anything. For a Postgres source use the direct connection string, not the transaction-mode pooler — a server-side cursor needs a session.

  2. Inspect what is there

    POST /admin/v1/import/inspect returns every table with its row estimate, columns, primary key, foreign keys, indexes, whether RLS is on and its policies; the user count and which hash algorithm they use; the buckets with object counts and total bytes; and a list of warnings. Read the warnings. They say which passwords will not survive.

  3. Select and run

    POST /admin/v1/import/run takes a selection — the tables you ticked, and whether to bring users and buckets — and answers text/event-stream. Phases arrive in order: connect, schema, rows, constraints, users, objects, done. A comment heartbeat every fifteen seconds keeps a proxy from cutting a long table copy.

  4. Read the history

    GET /admin/v1/import/history returns every run with its summary and its warnings, from control.import_runs. The Studio shows the same list. Nothing about a migration is only in a log line you already scrolled past.

Per source

What each one needs, and what it costs you

{
  "source": "supabase",
  "config": {
    // The DIRECT connection string, not the transaction pooler.
    "connectionString": "postgresql://postgres:PASSWORD@db.abcdefgh.supabase.co:5432/postgres",
    // Optional, and only used to download storage objects.
    "restUrl":    "https://abcdefgh.supabase.co",
    "serviceKey": "eyJ…"
  }
}

Tables with types, keys and indexes; auth.users with bcrypt hashes intact; policies translated where possible. Without restUrl and serviceKey, bucket and object metadata is imported but the file bytes are not, and the run says so.

Behaviour

How a run treats your data

It refuses to overwrite

The default mode is create, which refuses a target table that already holds rows and names it in the error. replace truncates first, and you have to ask for it. That refusal is the guard between “import into an empty database” and “silently destroy what is already here”.

Schema, then rows, then constraints

Every table is created before any data moves, so a child can load before its parent; foreign keys and indexes are added at the end and validated once over a finished table rather than row by row. Faster, and it sidesteps ordering problems entirely.

One transaction per table

A cancellation or a bad row leaves that table empty rather than half-filled. Tables already finished stay finished.

Streamed, never buffered

Postgres sources read through a server-side cursor and insert in batches of a thousand rows — fewer if the table is wide enough that a thousand rows would exceed the protocol’s 65,535 bind-parameter limit. A ten-million-row table does not materialise in memory.

Files three at a time

Streamed source to disk. A file that fails is recorded in failedObjects and the run continues, because one unreadable object should not end a migration of ten thousand.

RLS comes back on

Every imported table has row level security enabled. Policies are translated where the source had any — and where it did not, you have a table with RLS on and no policy, which denies everything until you write one. That is the safe failure direction.

Afterwards

The checklist nobody sends you

  1. Audit every table for policies

    An imported table has RLS on. If no policy came with it, nothing can read it until you write one — which is correct, and will look like a bug for about ten minutes.

  2. Re-create what did not come

    Database functions and triggers, extensions, edge or cloud functions, scheduled jobs and provider-specific settings are not copied. The run log names every one it skipped.

  3. Send recovery mail if the source was Appwrite or Firebase

    And tell people it is coming, so it does not read as a phishing attempt.

  4. Check the inferred types on a Firebase import

    A sampled schema is a starting point. Widen or narrow columns before you build on them.

  5. Point a staging client at it before you cut over

    Run your three most awkward queries. One level of embedded resources is supported; a nested-two-deep select is not, and you would rather learn that now.

baselyra.sarimtools.comBaselyrabaselyrapostgres:17OverviewBUILDDatabaseSQL EditorAuthenticationStorageRealtimeAPI DocsAI AssistantCONFIGUREImportEmailSettingsdatabase upImportStart importTest connectionSupabasePostgresAppwriteFirebaseSQL dumpconnection stringpostgres://postgres:••••••••@db.abcdefgh.supabase.co:5432/postgresFound in the sourcepublic.posts48,210 rowspublic.comments191,004 rowspublic.profiles2,140 rowspublic.rooms312 rowsauth.users2,140 users · bcryptstorage: avatars1,904 objectsPasswordspreservedbcryptHashes are copied verbatim behinda bcrypt$ marker. Your users sign inwith the password they already have.Appwrite (argon2) and Firebase(keyed scrypt) cannot be carried;those users need a recovery email.rows · public.comments163,400 rows copied · 3 of 4
Inspect first. The warnings list is the most valuable thing on the page.
You can migrate twice. Import into a throwaway instance, try your queries, throw it away, and do the real one later with what you learned. Two containers and a volume; docker compose down -v is the undo.

Start with a copy

Nothing here touches your source database.

A Postgres or Supabase source is read inside a repeatable read read only transaction, through a server-side cursor; the API sources are read through their own read APIs. The riskiest thing a run can do is fill a disk on the machine you are importing into.