pediatric-ai-scribe-v3/docs/migrations.md
Daniel 17fa3d56f4 Add node-pg-migrate for versioned schema changes + better mobile UA labels
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.
2026-04-14 05:06:19 +02:00

3.2 KiB

Database Migrations

The app uses node-pg-migrate for versioned, reversible schema changes.

How it works

Boot sequence:

  1. Baseline initsrc/db/database.js runs CREATE TABLE IF NOT EXISTS and ALTER TABLE ADD COLUMN IF NOT EXISTS for the existing schema. This is the implicit baseline — everything that was in place before migrations were introduced. Idempotent on every boot.
  2. Migrationssrc/db/migrate.js runs every file in /app/migrations/ that hasn't already been recorded in the pgmigrations table, in filename order. Each applied migration is inserted into pgmigrations so 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.