Database backup & restore for local development.
Dumps all table data to JSON before an ORM migration reset, then restores it after. Works directly at the database level — ORM-agnostic, framework-agnostic.
ORM migration resets (prisma migrate reset, drizzle-kit push --force-reset, etc.) wipe all data — dropping tables, re-running migrations, and re-seeding. This destroys development data that may be hard to recreate.
db-restore saves your data before the reset and merges it back after, preserving both your development data and any new seed data from migrations.
| Database | Driver |
|---|---|
| PostgreSQL | pg |
| MySQL | mysql2 |
| SQLite | better-sqlite3 |
# Create a profile (one-time)
pnpx github:skydiver/db-restore setup myproject
# Before migration reset: dump your data
pnpx github:skydiver/db-restore myproject dump
# Run your ORM reset
prisma migrate reset # or drizzle-kit push --force-reset, etc.
# After migration reset: restore your data
pnpx github:skydiver/db-restore myproject restorepnpx github:skydiver/db-restore setup <name> Create a new database profile interactively
pnpx github:skydiver/db-restore <name> dump [--out <dir>] Dump all tables to JSON
pnpx github:skydiver/db-restore <name> restore [--in <dir>] Restore tables from JSON dump
pnpx github:skydiver/db-restore profiles List all saved profiles
pnpx github:skydiver/db-restore remove <name> Delete a profile
Interactive profile creation. Prompts for connection details, tests the connection, and saves the profile. Passwords are never stored — they're prompted before each dump/restore.
$ pnpx github:skydiver/db-restore setup myproject
? Provider: postgres
? Host: localhost
? Port: 5432
? Database: myproject_dev
? User: postgres
? Password: ********
Testing connection... Connected.
Profile "myproject" saved.
Connects to the database, discovers all tables (excluding ORM migration tables), and writes each table's data to a JSON file.
$ pnpx github:skydiver/db-restore myproject dump
┌──────────┬──────┐
│ Table │ Rows │
├──────────┼──────┤
│ users │ 42 │
│ posts │ 128 │
│ comments │ 301 │
├──────────┼──────┤
│ Total │ 471 │
└──────────┴──────┘
Dump saved to ~/.config/db-restore/dumps/myproject (3 files)
If a previous dump exists, you'll be asked to archive it (.tar.gz), discard it, or cancel.
Reads the dump files and writes data back using UPSERT — your development data is merged with any new seed data from migrations.
$ pnpx github:skydiver/db-restore myproject restore
┌──────────┬──────┬──────────┐
│ Table │ Rows │ Strategy │
├──────────┼──────┼──────────┤
│ users │ 42 │ upsert │
│ posts │ 128 │ upsert │
│ comments │ 301 │ upsert │
├──────────┼──────┼──────────┤
│ Total │ 471 │ │
└──────────┴──────┴──────────┘
Restore complete (471 rows across 3 tables)
Lists all saved profiles in a table.
Deletes a saved profile.
- Connect to the database
- Discover tables (auto-excludes ORM migration tables)
- For each table: read columns, primary keys, and all rows
- Encode special types (bytes, bigint, decimal, datetime, json) as JSON-safe wrappers
- Write one JSON file per table + a
_metadata.json
- Connect to the database
- Disable foreign key checks
- For each table in the dump:
- Detect schema drift (added/removed columns) and warn
- Decode type wrappers back to native values
- Tables with primary keys: UPSERT (insert or update on conflict)
- Tables without primary keys: TRUNCATE + INSERT
- Reset auto-increment sequences (PostgreSQL)
- Re-enable foreign key checks
UPSERT preserves new seed data from migrations while restoring your dump data:
- Rows in dump matching DB primary key — updated with dump values
- Rows in dump with no match — inserted (your dev data)
- Rows in DB with no match in dump — kept (new seed data)
If the database schema changed between dump and restore:
- Column in both dump and DB — included in restore
- Column in dump but not in DB — skipped with warning
- Column in DB but not in dump — uses DB default with warning
- Table in dump but not in DB — skipped with warning
Special database types are encoded as JSON wrappers to prevent data loss:
| Type | JSON Representation |
|---|---|
| bytes | { "__type": "bytes", "value": "..." } |
| bigint | { "__type": "bigint", "value": "..." } |
| decimal | { "__type": "decimal", "value": "..." } |
| datetime | { "__type": "datetime", "value": "..." } |
| json | { "__type": "json", "value": ... } |
Primitive types (int, float, string, boolean, null) are stored as-is.
ORM migration tables are automatically excluded from dumps:
| Table | ORM |
|---|---|
_prisma_migrations |
Prisma |
__drizzle_migrations |
Drizzle |
knex_migrations |
Knex |
knex_migrations_lock |
Knex |
typeorm_migrations |
TypeORM |
SequelizeMeta |
Sequelize |
SequelizeData |
Sequelize |
mikro_orm_migrations |
MikroORM |
objection_migrations |
Objection |
_cf_KV |
D1 (Cloudflare) |
~/.config/db-restore/
profiles/
myproject.json
dumps/
myproject/
_metadata.json # provider, timestamp, table list, format version
users.json # one file per table
posts.json
comments.json
Each table file contains column metadata and all rows:
{
"table": "users",
"primaryKeys": ["id"],
"columns": [
{ "name": "id", "type": "integer" },
{ "name": "email", "type": "character varying" },
{ "name": "created_at", "type": "timestamp without time zone" }
],
"rows": [
{
"id": 1,
"email": "dev@example.com",
"created_at": {
"__type": "datetime",
"value": "2026-01-15T10:30:00.000Z"
}
}
]
}Connection profiles are stored in ~/.config/db-restore/profiles/. Passwords are never persisted.
{
"name": "myproject",
"provider": "postgres",
"host": "localhost",
"port": 5432,
"database": "myproject_dev",
"user": "postgres"
}SQLite profiles store the file path instead of connection details:
{
"name": "myproject",
"provider": "sqlite",
"path": "./data/dev.db"
}pnpm install
pnpm dev # Watch mode with tsx
pnpm test # Run tests with Vitest
pnpm typecheck # Type check with tsc
pnpm lint # Lint with Biome
pnpm build # Bundle with tsup