Infrastructure only — no existing data or tables modified.
src/db/migrate.js — programmatic runner, fires at boot after
the existing idempotent initDatabase()
migrations/1744600000000... — intentionally empty example, documents
the file shape. Registered in the new
pgmigrations tracking table so it won't
rerun.
.node-pg-migraterc.json — CLI config (migrations-dir, utc naming)
docs/migrations.md — workflow + conventions
package.json — migrate:up/down/new/status npm scripts
(status is a direct pgmigrations query
since node-pg-migrate v7 lacks a status
subcommand)
src/utils/sessions.js:
- parseUserAgent now recognizes the Capacitor wrapper (UA suffix
"PedScribe-Android" / "PedScribe-iOS") and labels sessions
"PedScribe (Android)" instead of "Chrome on Android".
Going forward: schema changes go in /migrations as versioned files
with up() + down(); the inline init in database.js is the implicit
baseline for everything already in production.
3.2 KiB
Database Migrations
The app uses node-pg-migrate for versioned, reversible schema changes.
How it works
Boot sequence:
- Baseline init —
src/db/database.jsrunsCREATE TABLE IF NOT EXISTSandALTER TABLE ADD COLUMN IF NOT EXISTSfor the existing schema. This is the implicit baseline — everything that was in place before migrations were introduced. Idempotent on every boot. - Migrations —
src/db/migrate.jsruns every file in/app/migrations/that hasn't already been recorded in thepgmigrationstable, in filename order. Each applied migration is inserted intopgmigrationsso it only runs once.
New schema changes should go in versioned migration files, not in the
inline database.js init.
Creating a migration
docker exec -w /app pediatric-ai-scribe npm run migrate:new -- add_avatar_url
Creates a file like migrations/1744601234567_add_avatar_url.js with
empty up() and down() functions. Edit it:
exports.up = (pgm) => {
pgm.addColumn('users', {
avatar_url: { type: 'text', notNull: false }
});
pgm.createIndex('users', 'avatar_url');
};
exports.down = (pgm) => {
pgm.dropIndex('users', 'avatar_url');
pgm.dropColumn('users', 'avatar_url');
};
Full API: https://salsita.github.io/node-pg-migrate/
Running migrations
Migrations apply automatically on app boot. To run them manually (e.g. before a restart):
docker exec -w /app pediatric-ai-scribe npm run migrate:up
Rolling back
Roll back the most recent migration:
docker exec -w /app pediatric-ai-scribe npm run migrate:down
This calls the file's down(). If down() is empty or missing, the
rollback is a no-op but the migration is removed from pgmigrations
— meaning the next up will reapply it.
Viewing state
Which migrations have been applied:
docker exec -w /app pediatric-ai-scribe npm run migrate:status
Or directly:
docker exec pedscribe-db psql -U pedscribe -d pedscribe \
-c "SELECT id, name, run_on FROM pgmigrations ORDER BY id;"
Raw SQL migrations
If pgm's JS helpers are limiting, drop to SQL:
exports.up = (pgm) => {
pgm.sql(`
CREATE INDEX CONCURRENTLY idx_audit_log_action
ON audit_log (action)
WHERE action IN ('login', 'login_failed', 'session_idle_timeout');
`);
};
Note: CREATE INDEX CONCURRENTLY cannot run inside a transaction. For
that you need exports.disableTransaction = true; in the migration file.
Conventions
- One logical change per file. Don't bundle unrelated alters.
- Always write
down()unless rollback is fundamentally impossible (e.g., dropping a column that had unique data). - Name files by what the change does (
add_foo,backfill_bar), not the ticket number. - Migrations run in filename order — the timestamp prefix ensures order across checkouts from different devs.
- Never edit an already-applied migration. Write a new one to fix it.
Relationship to the inline init
src/db/database.js still runs on every boot and handles the pre-migration
baseline. Do not add new schema changes there — use migrations. The inline
init will gradually shrink as old CREATE TABLE statements age out.