docs/self-hosting/migration.mdx
To move a Reactive Resume installation from v4 to v5, you set up a new v5 instance alongside your existing v4 instance, then transfer your users and resumes across.
<Info> This page is for **v4 → v5 data migration** only. For normal v5 upgrades, use the [Self-Hosting with Docker](/self-hosting/docker) guide. v5 schema migrations run automatically on app startup. </Info> <Warning> This guide applies only to infrastructure and backups you are authorized to operate. It does not grant access to hosted Reactive Resume data. Only the hosted service operator can verify whether a hosted snapshot exists and authorize access to it. </Warning> <Warning> **Keep your v4 instance running** until you have migrated all data to v5 and checked that everything works. That way you have a fallback if the migration goes wrong. </Warning>Before starting the migration, ensure you have:
<CardGroup cols={2}> <Card title="Running v4 Instance">Your existing Reactive Resume v4 instance should be running and accessible.</Card> <Card title="New v5 Instance"> A fresh Reactive Resume v5 instance set up and running. Follow the [Self-hosting with Docker](/self-hosting/docker) guide if you haven't done this yet. </Card> <Card title="Database Access"> Access to both your v4 PostgreSQL database (source) and v5 PostgreSQL database (target). </Card> <Card title="Backup">A recent backup of your v4 database. Always back up before any migration.</Card> </CardGroup>The best migration approach depends on the size of your instance:
<CardGroup cols={2}> <Card title="Manual Migration" icon="hand"> **Best for**: Small instances with a handful of resumes. Uses the built-in Import Dialog to manually convert resumes one at a time. </Card> <Card title="Automated Migration" icon="robot"> **Best for**: Large instances with many users and resumes. Uses migration scripts to batch-process all users and resumes automatically. </Card> </CardGroup>Use a recovery case when an owner changed resumes after an earlier migration. Keep the case record private and outside Git because it may connect account and resume identifiers. Record these fields before inspecting resume content:
no-op, export-copy, or blockedCase IDs, source resume IDs, and non-null target resume IDs must contain a non-whitespace character after trimming and must not contain Unicode control or format characters. Valid identifiers are preserved verbatim.
These hashes prove content equality only; they never prove ownership, source authenticity, or recipient identity.
An authorized operator should follow this order:
<Steps> <Step title="Verify source availability"> Confirm that a source snapshot exists and record when it was captured. If no source snapshot exists, report that factual limit. Recovery tooling cannot reconstruct records that are absent from every available source. </Step> <Step title="Verify owner and mapping"> Verify the requester using the operator's approved account-ownership process. Confirm the old-to-new owner mapping; a matching email address, resume title, or username alone is not ownership proof. Stop if either check is incomplete. </Step> <Step title="Compare without writing"> Build one serialized JSON comparison request containing the case IDs, safety flags, and source and target values. The comparator accepts only this request string, not an object argument. Source and target data must already conform exactly to the current v5 resume-data schema before content hashes are calculated. Raw v4 exports are unsupported, and the comparator performs no format conversion. Identical content is a `no-op`. Source-only or divergent content is an `export-copy`. Missing identity evidence, mapping, valid current-v5 data, or a source snapshot is `blocked`. </Step> <Step title="Deliver a separate private export"> Default to a private JSON export. Never overwrite the current v5 resume. After the recipient and delivery channel are approved, deliver the recovered JSON privately so the owner can import it as a separate resume. Record source and delivered hashes outside Git and confirm they match. </Step> </Steps>Repository contributors can rehearse this decision with the pure comparator in
tooling/recovery/compare-resume.ts. It accepts one serialized JSON comparison request and rejects object arguments.
The request's source and target values must already conform exactly to the current v5 resume-data schema. It does not
accept raw v4 exports or perform legacy conversion; review the historical converter at tag v5.0.20 separately before
processing legacy-format data. The comparator produces a deterministic dry-run manifest and has no database, network,
or write path. Use synthetic IDs and content only; keep any operational manifest outside the repository.
If you have only a few resumes to migrate, the simplest approach is to use the Import Dialog feature in v5.
<Steps> <Step title="Export from v4"> In your v4 instance, go to each resume and export it as JSON. This creates a portable file containing all your resume data. </Step> <Step title="Import into v5"> In your new v5 instance: 1. Sign in or create a new account 2. Click **Create Resume** or use the **Import** option 3. Select the **Reactive Resume v4** format 4. Upload your exported JSON file The import process automatically converts the v4 format to v5. </Step> <Step title="Verify and repeat"> Review the imported resume to ensure all data transferred correctly. Repeat for each resume you need to migrate. </Step> </Steps> <Tip> The Import Dialog handles the schema conversion automatically, so you don't need to worry about format differences between v4 and v5. </Tip>For instances with many users and resumes, use the migration scripts to automate the process. The migration happens in two phases: first users, then resumes.
To run the migration scripts, you need the following installed on your host machine:
<CardGroup cols={1}> <Card title="Node.js Runtime"> **tsx** - TypeScript execution environment. Install globally with: ```bash npm install -g tsx ``` </Card> <Card title="Environment Loader"> **dotenvx** (or any tool to load `.env` files). Install globally with: ```bash npm install -g @dotenvx/dotenvx ``` Alternatively, you can use `dotenv`, `direnv`, or export the variables manually. </Card> <Card title="Reactive Resume Source Code"> Clone the Reactive Resume repository and check out the last tag that includes the migration scripts: ```bash git clone https://github.com/amruthpillai/reactive-resume.git cd reactive-resume git checkout tags/v5.0.20 pnpm install ``` </Card> </CardGroup> <Info> The v4 migration scripts live in the `v5.0.20` tag. Use that checkout only to run the migration scripts against your v4 and v5 databases; keep your actual v5 deployment on the latest version. </Info>Create a .env file in the root of the repository with the following variables:
# Connection string to your NEW v5 PostgreSQL database (target)
DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resume_v5"
# Connection string to your OLD v4 PostgreSQL database (source)
PRODUCTION_DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resume_v4"
<Warning>PRODUCTION_DATABASE_URL is used only by these migration scripts. It is not a runtime app variable.</Warning>
The user migration script transfers all user accounts, authentication data, and two-factor settings from v4 to v5.
dotenvx run -- tsx scripts/migration/user.ts
What this script does:
scripts/migration/user-id-map.json) that links old user IDs to new onesExpected output:
⌛ Starting user migration...
📥 Fetching users batch from production database (OFFSET 0)...
📋 Found 1000 users in this batch.
📝 Preparing to bulk insert 1000 users...
✅ Bulk inserted 1000 users in 245.3 ms (avg 0.2 ms/user)
💾 Progress saved at offset 1000
📦 Processed 1000 users so far...
📊 Migration Summary:
Users created: 1000
Accounts created: 1000
Two-factor entries created: 50
Skipped (already exist): 0
⏱️ Total migration time: 1234.5 ms (1.23 seconds)
✅ User migration complete!
After users are migrated, run the resume migration script. This script depends on the user ID mapping created in the previous step.
dotenvx run -- tsx scripts/migration/resume.ts
What this script does:
<Info>Like the user script, the resume migration also saves progress and can be resumed if interrupted.</Info>
Expected output:
⌛ Starting resume migration...
📥 Fetching resumes batch from production database (OFFSET 0)...
📋 Found 2500 resumes in this batch.
📝 Preparing to bulk insert 2500 resumes...
✅ Bulk inserted 2500 resumes in 892.1 ms (avg 0.4 ms/resume)
💾 Progress saved at offset 2500
📦 Processed 2500 resumes so far...
📊 Migration Summary:
Resumes created: 2500
Statistics created: 2500
Skipped (userId not found or already exist): 0
Errors: 0
⏱️ Total migration time: 5678.9 ms (5.68 seconds)
✅ Resume migration complete!
Both migration scripts support graceful shutdown and resume:
scripts/migration/user-progress.json and scripts/migration/resume-progress.json track the current migration statescripts/migration/user-id-map.json maps v4 user IDs to v5 user IDsCtrl+C to stop the migration safely. Progress is saved before exit.After completing the migration:
<Steps> <Step title="Verify data integrity"> Sign in to your v5 instance and spot-check several user accounts and resumes to ensure data transferred correctly. </Step> <Step title="Test functionality"> - Create a test resume and export it as PDF - Verify social sign-in works (if configured) - Check that two-factor authentication works for migrated users </Step> <Step title="Update DNS/Proxy"> Once verified, update your DNS records or reverse proxy to point to the new v5 instance. </Step> <Step title="Decommission v4"> After confirming everything works and allowing a grace period, you can safely shut down your v4 instance. </Step> </Steps>The migration scripts handle these conversions automatically.