Version française · Markdown version, readable by an agent · v0.3.0
Migrate from Supabase
This page moves a Supabase project to a Bastide project: the schema and data, the authentication users, the storage buckets and objects. Your application keeps supabase-js: it changes the project address and key.
Before you start
- A Bastide project created with its backend (
project_create_with_backend), empty: the importer refuses a target project that is already occupied, before writing anything. - The direct connection address of the source database (role
postgres, direct connection or session mode), itsservice_rolekey, and itsanonkey if you want to keep the legacy keys. - An operator: during the invitation-only beta, the import is an operator action, not a registry operation. It opens the tenant database as administrator to install the extensions.
- Read the "Known incompatibilities" section below first: a client that relies on SAML, on the S3 endpoint or on
postgres_changesdoes not move over as is.
1. Run the import
The source's secrets do not go through the command line, which ps shows to every user of the host: they are read from the environment.
SUPABASE_DB_URL='postgres://postgres:<password>@db.<ref>.supabase.co:5432/postgres' SUPABASE_SERVICE_ROLE_KEY='<service_role>' SUPABASE_ANON_KEY='<anon>' pnpm importer:supabase <bak_…> --stockage https://<ref>.supabase.co --rapport rapport.json
--stockage is required as soon as the source has a bucket; without it the import stops and says so. The order of the steps is not free: inventory and target judged empty, users, schema and data (policies and grants recomposed, in one transaction), storage (a checksum per object), catalogue comparison and row recount, report.
The exit code gives the outcome: 0 imported, 3 imported with discrepancies to read in the report, 1 interrupted or failed, 2 not done (missing argument or variable). On failure the importer undoes nothing: it writes into a new project, which you remove if needed; every step that writes does so in a transaction, and the report says which one stopped.
The importer writes nothing to the source: no token revoked, no switch to read-only.
2. Double run: run both sides before switching
As long as you do not switch, the source remains your live application. The procedure:
1. Import as above. The import is a copy at a given instant. 2. Keep the original keys. Supplied at import, SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY are stored by fingerprint and remain accepted for twelve months, every response carrying Deprecation: true and Sunset (see LIM-09). A copy of your application can therefore point at Bastide without changing its keys: only the project address changes. 3. Run a copy of the application on Bastide, next to the production that stays on Supabase: the same project address replaced, the same keys, the same journey. Replay your ordinary journeys: sign-up, confirmation, sign-in, read, write, upload. 4. Compare. Read the report (--rapport): catalogue compared, rows recounted, checksum of each object. Compare what your journeys return on both sides. 5. **Move to the sb_publishable_… and sb_secret_… keys** (backend.key.create) before the twelve months expire, then revoke the legacy keys. 6. Switch. There is no continuous replication: writes made on the source after the import do not follow. The source's read-only window (TR-194, EX-MIG-140) is planned for lot L2 and does not exist yet. To switch without loss, suspend the application's writes yourself, run the import into a NEW empty Bastide project, check the report, then change the production address.
A rollback before step 6 has no effect on the source: do nothing on the Supabase side, and remove the Bastide project.
3. Connect the application
Replace the project address and key. Supabase's paths are held with no rewriting: https://<project>.<domain>/rest/v1/…, /auth/v1/…, /realtime/v1/…. The supabase-js headers are Supabase's: the key in apikey, a session token in Authorization.
Known incompatibilities
Each limit below is reproduced by a test (pnpm limites); the public page is https://bastide.app/limites-supabase.
LIM-01: a REST response carries at most 1000 rows, truncated without error; paginate (Range, orlimitandoffset).LIM-02: a resource embeds only along a declared foreign key, otherwise 400PGRST200; declare the key, or expose a view.LIM-03: in the pooler's transaction mode (the one of direct connections, the default),NOTIFYdoes not reach a client that doesLISTEN; listen from a session-mode connection.LIM-04: in transaction mode,pg_advisory_lockdoes not exclude another connection of the same project; usepg_advisory_xact_lock, or session mode.LIM-05: in transaction mode, session state is not isolated (SET, temporary table,WITH HOLDcursor); useSET LOCAL,ON COMMIT DROP, or session mode.LIM-06: not served are enterprise SAML sign-in (signInWithSSO), identity linking (linkIdentity) and/auth/v1/settings(404,chemin_auth_inconnu). An enterprise sign-in goes through Keycloak over OpenID Connect. The twelvesignInWithOAuthproviders and two-factor authentication are served.LIM-07: the S3-compatible endpoint (/storage/v1/s3) is not served (404,chemin_inconnu); it will come withEX-DATA-370(lot L2). Thestorage-jsstorage is served.LIM-08: realtime serves broadcast and presence, not listening to database changes: apostgres_changessubscription is refused. Useselect realtime.diffuser_les_changements('public.my_table').LIM-09: the legacyanonandservice_rolekeys are valid only for an imported project, its two original keys only, for twelve months. Any other gets 401cle_refusee.LIM-10:updateUseronly changes the password; an address or metadata is refused (400,changement_d_adresse_ailleurs,metadonnees_non_rangees). UsePOST /auth/v1/user/email, and a project table for metadata.LIM-11: a table is granted to the API roles only if it carries row-level security AND at least one policy; the roles carry the database name as a prefix, so agrant … to authenticatedwritten as is is refused (code42704).
Other measured gaps: the returned user does not carry last_sign_in_at (the field is absent, not null); backup codes, web3 and address change by the client are neither measured nor served. The pass rate of the official supabase-js integration suite (95% required by EX-DATA-090) has not been played yet: it assumes asymmetric keys with a JWKS endpoint, planned for lot L2.
The bricks under the data plane are six, pinned by digest (the bricks matrix): PostgreSQL 17, pgvector, Supavisor, PostgREST, imgproxy (free edition) and SeaweedFS. The proven client is @supabase/supabase-js 2.117.2. The import only carries over the extensions admitted by the target project's tier.
See also: Migrate from Vercel, Leave Bastide.