If you can tolerate a maintenance window, you can perform a 'cold migration' using an offline SQL script. This is recommended for single-region/single-instance setups or self-hosted dev/staging environments. This process involves stopping traffic, deploying new binaries in LEGACY mode to initialize the schema, running a backfill script, and then manually setting the migration mode to MIGRATED.
Pre-flight
- Confirm versions.
- Schedule a maintenance window.
- Backup your database.
Migration Steps
1. Stop traffic
Drain or block all SuperTokens API traffic. No writes can be in flight during the migration.
2. Deploy new binaries in LEGACY mode
Boot one core+plugin instance against the database. This creates the new tables and columns via GeneralQueries.createTablesIfNotExists.
Verify the schema exists using:
\dt recipe_user_account_infos
\dt recipe_user_tenants
\dt primary_user_tenants
\d+ app_id_to_user_id -- expect time_joined, primary_or_recipe_user_time_joined columns
Shut the instance down before the backfill.
3. Run the offline backfill
Use the migration-backfill.sql script from the supertokens-postgresql-plugin repository.
psql "<connection-uri>" -v app_id="'my-app'" -f migration-scripts/migration-backfill.sql
Note: Use -v app_id="''" to scope to all apps, or -v app_id="'my-app'" to scope to a single app.
4. Verify data integrity
Run the verification queries included at the bottom of migration-backfill.sql. They should all return 0. For absolute confidence, run a canonical dump comparison:
psql "<connection-uri>" -f migration-scripts/dump_old_canonical.sql > old.csv
psql "<connection-uri>" -f migration-scripts/dump_new_canonical.sql > new.csv
diff old.csv new.csv
5. Set tenant to MIGRATED mode
You can do this in one of two ways:
Option A: Direct DB Edit
Update the tenant_configs row directly. Adjust the syntax based on whether your column is text or jsonb:
UPDATE tenant_configs
SET core_config = jsonb_set(
core_config::jsonb,
'{migration_mode}',
'"MIGRATED"'::jsonb
)::text
WHERE connection_uri_domain = '' AND app_id = 'public' AND tenant_id = 'public';
Option B: API Flip
- Boot the new core in
LEGACY mode. - Issue a
PUT /recipe/multitenancy/connectionuridomain/v2 with migration_mode: "MIGRATED". - Shut down the instance.
6. Bring traffic back up
Start all instances. Verify with synthetic users (create, link, update email) and check that GET /migration/mode returns "mode": "MIGRATED" for every CUD.