Files
Reactive-Resume/docs/self-hosting/migration.mdx
T
Amruth Pillai 9a803305c8 fix: address verified v6 app findings
Fix authentication recovery, account imports, application tracking, resume
editing and exports, sharing, API contracts, provider selection, and private
local attachments. Preserve authored content during PDF pagination.

Update guides, generated OpenAPI output, and translation catalogs to match
verified behavior and documented constraints.

Validation: 1,019 tests passed; 12 database/OAuth integration tests skipped.
Ten affected package typechecks, production build, Biome, and package
boundaries passed.
2026-09-30 06:15:09 +02:00

219 lines
11 KiB
Plaintext

---
title: "Migrating from v4"
description: "Move users and resumes from a self-hosted Reactive Resume v4 instance to the current version, by importing resumes one by one or with the migration scripts."
---
Reactive Resume v4 used a different database schema, so you can't upgrade a v4 installation in place. Instead, you set up a new installation next to it and copy your users and resumes across. This page covers that move. To upgrade an existing v5 installation, see [Upgrading to v6](/self-hosting/upgrading-to-v6) instead.
<Warning>
Keep your v4 instance running until every user and resume is in the new installation and you've checked the result. It's your fallback if something goes wrong. Work only on infrastructure and backups you're authorized to operate.
</Warning>
## Before you start
You need:
- Your running v4 instance, and a recent backup of its PostgreSQL database.
- Access to the v4 database (the source) and to a new, empty PostgreSQL database for the new installation (the target).
- A plan for the new installation. [Self-hosting with Docker](/self-hosting/docker) is the usual choice.
## Choose a method
| Method | Best for | How it works |
| --- | --- | --- |
| [Import resumes one by one](#import-resumes-one-by-one) | A handful of resumes | Each person exports their v4 resumes as JSON and imports them into the new installation. |
| [Run the migration scripts](#run-the-migration-scripts) | Many users and resumes | Scripts copy users, sign-in methods, resumes, and statistics straight from database to database. |
## Import resumes one by one
The current version reads v4 JSON exports directly and converts them.
<Steps>
<Step title="Export from v4">
In your v4 instance, open each resume and export it as JSON.
</Step>
<Step title="Import into the new installation">
Sign in to the new installation (create an account first if needed), select **New**, then **Import a resume**, and choose the JSON file. Reactive Resume recognizes the v4 format on its own. See [Importing resumes](/guides/importing-resumes) for details.
</Step>
<Step title="Check and repeat">
Look through the imported resume, especially dates and layout, then repeat for the next one.
</Step>
</Steps>
## Run the migration scripts
The scripts live in the repository at tag `v5.0.20`, the last release that includes them. They were written against the database schema of that release, so you create the target database with that release first, run the scripts, and only then start the current version, which brings the data forward with its own migrations.
<Warning>
Don't run the scripts against a database that a newer release has already set up. They don't fill in columns added later (such as the sign-in `issuer` added in v5.2.8), so migrated users could fail to sign in. Starting the current version after the scripts fills those columns in.
</Warning>
### Prepare the scripts
Install `tsx` and a `.env` loader such as `dotenvx` on the machine that runs the scripts, then check out the tag:
```bash
npm install -g tsx @dotenvx/dotenvx
git clone https://github.com/reactive-resume/reactive-resume.git reactive-resume-migration
cd reactive-resume-migration
git checkout tags/v5.0.20
pnpm install
```
Use this checkout only to run the migration. Your actual installation runs the current release.
Create a `.env` file at the root of the checkout:
```bash .env
# The NEW, empty database (target)
DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resume"
# The OLD v4 database (source)
PRODUCTION_DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resume_v4"
```
<Warning>
Double-check both connection strings. `DATABASE_URL` is the new target and `PRODUCTION_DATABASE_URL` is the old v4 source. Swapping them can destroy data. `PRODUCTION_DATABASE_URL` is used only by these scripts, never by the app.
</Warning>
### Create the target schema
Apply the `v5.0.20` migrations to the empty target database:
```bash
dotenvx run -- pnpm db:migrate
```
### Migrate users
```bash
dotenvx run -- tsx scripts/migration/user.ts
```
The script:
- Reads v4 users in batches and creates matching users in the target.
- Copies sign-in methods (email and password, Google, GitHub, custom OAuth). Two-factor authentication isn't copied: every migrated user starts with it turned off.
- Writes `scripts/migration/user-id-map.json`, which links each v4 user ID to its new ID. The resume script needs it.
- Saves progress to `scripts/migration/user-progress.json` after each batch. If the run stops, run it again to continue from the last saved batch.
It ends with a summary of users created, accounts created, and users skipped.
<Warning>
Migrated two-factor authentication is disabled. Before switching traffic, tell affected users to enable it again on the new installation. Treat the transition as a change to account security.
</Warning>
### Migrate resumes
```bash
dotenvx run -- tsx scripts/migration/resume.ts
```
The script:
- Reads v4 resumes in batches, converts each to the new format, and assigns it to the right user through the ID map.
- Copies view and download statistics, public or private visibility, and the locked state.
- Saves progress to `scripts/migration/resume-progress.json` after each batch, and continues from there if you run it again.
It ends with a summary of resumes created, resumes skipped, and errors.
<Tip>
Each script deletes its progress file when it finishes. Keep `user-id-map.json` as the record of which v4 user became which new user. Don't re-run the scripts against a populated target to "fix" a problem; review the source backup, the map, and the target first.
</Tip>
<Note>
These historical scripts register cleanup on normal exit, not on `SIGINT`. Prefer letting a batch finish before stopping it. After an interruption, retain the progress and ID-map files and verify the last batch before resuming.
</Note>
### Start the current version
Point a current Reactive Resume installation at the target database and start it. On startup it applies every migration since `v5.0.20`, which also fills in the new columns for the migrated users. Watch the logs for `Database migrations completed`.
## After the migration
<Steps>
<Step title="Check the data">
Sign in as a few users and spot-check their resumes.
</Step>
<Step title="Test the essentials">
Download a resume as PDF, and sign in with email and password and with each social provider you use.
</Step>
<Step title="Switch traffic">
Point your DNS or reverse proxy at the new installation.
</Step>
<Step title="Retire v4">
After a grace period with no problems, shut down the v4 instance.
</Step>
</Steps>
## What carries over
<AccordionGroup>
<Accordion title="Passwords">
People who signed up with email and password keep their password. No reset is needed.
</Accordion>
<Accordion title="Profile pictures and images">
The database stores references to pictures, not the files. Give the new installation access to the same storage (for example the same S3 bucket), or people re-upload their pictures.
</Accordion>
<Accordion title="Social and custom OAuth sign-in">
Configure the same providers with the same client IDs in the new installation. Custom OAuth now uses a different callback URL, so update your provider as described in [Single sign-on (SSO)](/self-hosting/sso). On the first sign-in, the provider account is linked to the migrated user by email. For custom OAuth, that works only if the user's email was verified in v4; otherwise the sign-in stops with an "account not linked" error.
</Accordion>
<Accordion title="Two-factor authentication">
Not carried over. Users who had two-factor authentication in v4 sign in with their password alone, and can set it up again in **Settings → Account**.
</Accordion>
<Accordion title="Schema differences">
v4's `visibility` became a public/private flag, a resume's `title` became its `name`, and resume data was reorganized. The scripts and the importer handle these conversions.
</Accordion>
</AccordionGroup>
## Recover one person's resumes later
Sometimes a person changes a resume in v4 after the main migration, or a resume didn't come across. Handle this as a recovery case rather than re-running the scripts, and never overwrite the resume in the new installation.
Keep a private record for each case, outside Git, because it links account and resume identifiers:
- Case ID and the capture time of the source snapshot.
- How you verified the person owns the account.
- Source resume ID and mapped target resume ID, if there is one.
- Source and target content hashes, and the outcome: `no-op`, `export-copy`, or `blocked`.
<Steps>
<Step title="Confirm a source exists">
Check that a source snapshot exists and note when it was taken. Records missing from every available source can't be reconstructed.
</Step>
<Step title="Verify ownership and mapping">
Verify the requester through your approved account-ownership process, and confirm the old-to-new user mapping. A matching email, resume title, or username alone isn't proof. Stop if either check is incomplete.
</Step>
<Step title="Compare without writing">
Identical content is a `no-op`. Content that exists only in the source, or differs, is an `export-copy`. Missing evidence, mapping, valid data, or source is `blocked`. Content hashes prove equality only, never ownership or authenticity.
</Step>
<Step title="Deliver a separate private export">
Send the recovered JSON privately through an approved channel, so the person imports it as a new resume. Confirm the delivered hash matches the source.
</Step>
</Steps>
Contributors can rehearse the comparison with `tooling/recovery/compare-resume.ts`. It takes one serialized JSON request whose source and target already match the current resume schema, produces a dry-run manifest, and never touches a database or the network. It doesn't convert raw v4 data; review the converter at tag `v5.0.20` for that. Use synthetic data only.
## Troubleshooting
<AccordionGroup>
<Accordion title="The script says PRODUCTION_DATABASE_URL is not set">
Put both `DATABASE_URL` and `PRODUCTION_DATABASE_URL` in `.env` and run the scripts through `dotenvx run --` (or export the variables yourself).
</Accordion>
<Accordion title="Users are skipped">
A user is skipped when their email or username already exists in the target, or when an earlier run already migrated them. The console output gives the reason.
</Accordion>
<Accordion title="Resumes are skipped">
A resume is skipped when its owner isn't in the user ID map, when the owner already has a resume with the same slug, or when an earlier run already migrated it.
</Accordion>
<Accordion title="A resume comes across empty">
When a v4 resume can't be parsed, the scripts can create empty default data. Treat that as a failed conversion, not a recovered resume. Keep the source export and import it by hand once you've confirmed it's valid.
</Accordion>
<Accordion title="Migrated users can't sign in">
Check that you started the current version against the target database after the scripts, so its migrations ran. The startup log shows `Database migrations completed`.
</Accordion>
<Accordion title="The migration is slow">
The scripts work in batches to spare the databases. Run them off-peak, give both databases enough resources, or adjust the batch size in the script files.
</Accordion>
</AccordionGroup>