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.
This page is for v4 → v5 data migration only. For normal v5 upgrades, use the Self-Hosting with
Docker guide. v5 schema migrations run automatically on app startup.
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.
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.
Recover one owner’s resumes without overwriting v5
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:
Recovery case ID
Source snapshot capture time
Owner verification status
Source resume ID and mapped target resume ID, if one exists
Source and target content hashes
Proposed outcome: no-op, export-copy, or blocked
Case 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:
1
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.
2
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.
3
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.
4
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.
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.
1
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.
2
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.
3
Verify and repeat
Review the imported resume to ensure all data transferred correctly. Repeat for each resume you need to migrate.
The Import Dialog handles the schema conversion automatically, so you don’t need to worry about format differences
between v4 and v5.
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.
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.
Reactive Resume Source Code
Clone the Reactive Resume repository and check out the last tag that includes the migration scripts:
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.
Create a .env file in the root of the repository with the following variables:
.env
# 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"
Double-check your connection strings! DATABASE_URL should point to your new v5 database and
PRODUCTION_DATABASE_URL should point to your old v4 database. Mixing these up could cause data loss.
PRODUCTION_DATABASE_URL is used only by these migration scripts. It is not a runtime app variable.
Preserves two-factor authentication settings and backup codes
Creates a mapping file (scripts/migration/user-id-map.json) that links old user IDs to new ones
The script saves progress automatically. If interrupted (Ctrl+C), you can run it again and it will resume from where
it left off.
Expected 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:
Fetches resumes in batches from the v4 database
Converts each resume from v4 format to v5 format automatically
Links resumes to the correct users using the ID mapping
Migrates resume statistics (views, downloads)
Preserves visibility settings (public/private) and lock status
Like the user script, the resume migration also saves progress and can be resumed if interrupted.
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:
Progress files: scripts/migration/user-progress.json and scripts/migration/resume-progress.json track the current migration state
User ID mapping: scripts/migration/user-id-map.json maps v4 user IDs to v5 user IDs
Graceful shutdown: Press Ctrl+C to stop the migration safely. Progress is saved before exit.
Resume migration: Run the script again to continue from where you left off
Preserve progress files and the user ID mapping as recovery evidence. Do not delete them or replay the historical
scripts against a populated target as a recovery shortcut. Review the v5.0.20 scripts, source backup, mapping, and
target state before any rerun.
Sign in to your v5 instance and spot-check several user accounts and resumes to ensure data transferred correctly.
2
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
3
Update DNS/Proxy
Once verified, update your DNS records or reverse proxy to point to the new v5 instance.
4
Decommission v4
After confirming everything works and allowing a grace period, you can safely shut down your v4 instance.
Users who signed up with email/password can continue using their existing passwords. No password reset is required after migration.
Profile pictures are not migrated
User profile pictures (avatars) are stored as references in the database. If you were using S3 storage, ensure your v5
instance has access to the same bucket, or users may need to re-upload their avatars.
Resume images and uploads
Similar to profile pictures, any images embedded in resumes need to be accessible from your v5 instance. Consider
migrating your storage bucket or updating references as needed.
OAuth provider changes
If you’re using custom OAuth providers, ensure the same providers are configured in v5 with matching client IDs. Users
authenticate with the same provider ID, so mismatched configurations will cause login failures.
Schema differences
The v5 schema has some changes from v4:
visibility (public/private) is now isPublic (boolean)
Resume title is now name
Some resume data fields have been reorganized
The migration scripts handle these conversions automatically.
Script fails with 'PRODUCTION_DATABASE_URL is not set'
Ensure your .env file contains both DATABASE_URL and PRODUCTION_DATABASE_URL, and that you’re using a tool like dotenvx to load them before running the script.
Users are skipped during migration
Users are skipped if: - Their email already exists in the v5 database - Their username already exists in the v5
database - They were already migrated in a previous run Check the console output for skip reasons.
Resumes are skipped during migration
Resumes are skipped if: - The associated user wasn’t migrated (user ID not in mapping file) - A resume with the same
slug already exists for that user - They were already migrated in a previous run
Resume data parsing fails
Historical scripts can create default empty data when a v4 resume cannot be parsed. Treat that result as a failed
conversion, not a recovered resume. Preserve the source export, review the v5.0.20 converter, and use the Import
Dialog only after valid source data is confirmed.
Migration is slow
The scripts process data in batches to avoid overwhelming the database. For very large instances:
Consider running the migration during off-peak hours
Ensure both databases have adequate resources
The batch size can be adjusted in the script files if needed