1. List everything the app depends on
For a Heroku app that means the Procfile, release command, config names, add-ons and scheduled jobs; for a MySQL database it means schema objects, views, triggers, routines and the queries the application sends. Anything not listed is missed.
- Names only: never values, URLs or passwords.
- Mark unknowns for the account owner to resolve.
2. Choose the replacement for each item
Write the target beside each dependency: same, replaced by a named service, removed with agreement or unknown. A scheduled job needs a replacement and a rule for what happens if it runs twice. A MySQL view needs a hand rewrite because the loader does not move it.
- Decide who pays for the new services.
- Keep the decisions with the plan.
3. Rehearse the copy into an empty target
Copy the data into a new, empty database that you can discard. Heroku's backup restore deletes the target's data first, so never aim it at anything you need. Record every warning and the reason it is safe.
- Name the target so it cannot be confused with the live one.
- Check the extensions the target needs before the rehearsal.
4. Reconcile and decide
Compare per-table counts and checksums, generated-key values and the results of named queries on both sides. Decide each case-sensitivity, zero-date and flag difference and retest. The worked reconciliation table shows what the evidence looks like for an invented database.
- An unexplained difference stops the plan.
- Decisions belong to the data owner, not to the tool.
5. Run workers and jobs on one side at a time
Decide which side's workers and scheduled jobs run at each moment of the cutover, so no job runs twice or on neither side. Run each replacement job once on the new host in a test window and check its effect.
- Stop the old side's jobs before the new side's go live.
- Write the schedule into the plan.
6. Freeze writes, copy once more and switch
Agree a short write freeze, take a verified backup of the source at that moment and keep it untouched until sign-off as your restore point, take a final copy and reconcile again at the frozen moment, when counts must match. Then the domain holder makes the switch. A move that cannot pause writes needs a different technique and a separate plan.
- Check public pages and each named flow after the switch.
- Mail records are separate: the DNS guide explains why.
7. Keep a way back, and keep the old side until sign-off
Leave the old app and database untouched until you sign off. If a live check fails after writes resume, new records must be exported and applied to the old database before you point back; pointing back alone loses them. Rehearse that reverse step on a restored copy before the day. The backup taken at the freeze is the restore point if the reverse step itself goes wrong, but restoring it over the old database discards every record written since, so export those first. Prices on the linked jobs are untested proposals and payment follows the agreed checks.
- Retention of the old side is your decision and your cost.
Sources and limits
- Heroku Dev Center: Heroku PGBackups Checked 2026-10-11.
- A restore deletes all data in the target database first; PGBackups is intended for databases up to 20 GB.
- Heroku Dev Center: Heroku Scheduler Checked 2026-10-11.
- Scheduler jobs are expected but not guaranteed to run, and can in rare cases run twice.
- pgloader: MySQL to PostgreSQL reference Checked 2026-10-11.
- pgloader does not migrate views or triggers and resets sequences after loading.