pediatric-ai-scribe-v3/docs/migrations.md
Daniel 857ed341f5 docs: rewrite architecture, authentication, configuration, deployment, ai-providers, speech, database, learning-hub, migrations, developer-guide for public audience
- Drop first/second-person voice; reference-style prose throughout
- Remove stale information; align with current code (argon2id primary, hybrid cookie/Bearer auth, sliding 24h idle, AES-256-GCM PHI at rest, backup codes, node-pg-migrate, collation-drift guard, multi-arch Docker, auto-version pipeline)
- Preserve all technical accuracy and code examples
- Remove any remaining references to separate PedsHub Quiz app
- Keep consistent tone across files (tables + code blocks, imperatives where needed)
- api-reference.md and developer-guide.md route tables expanded to reflect current routes (billing, sessions)
2026-04-15 00:26:38 +02:00

2.9 KiB

Database migrations

The project uses node-pg-migrate for versioned, reversible schema changes layered on top of the idempotent baseline init in src/db/database.js.

Boot sequence

  1. initDatabase() in src/db/database.js — the baseline.
    • CREATE TABLE IF NOT EXISTS for every legacy table.
    • ALTER TABLE ADD COLUMN IF NOT EXISTS for every column added before the migration tool existed.
    • Collation-drift check + auto REINDEX DATABASE on mismatch.
    • COLLATE "C" conversion for lookup-critical indexes (gated by migration.text_indexes_c flag in app_settings).
  2. src/db/migrate.js — runs every file in /app/migrations/ that isn't already recorded in the pgmigrations table, in filename order. Each applied file is inserted into pgmigrations so it runs exactly once.

All new schema changes go in versioned migration files, not in the inline baseline.

Creating a migration

docker exec -w /app pediatric-ai-scribe npm run migrate:new -- add_avatar_url

Produces migrations/<utc-ms>_add_avatar_url.js with empty up() and down(). Edit:

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/

Commands

Command Effect
npm run migrate:up Apply all pending
npm run migrate:down Roll back the most recent one
npm run migrate:new -- <name> Scaffold a new file
npm run migrate:status Dump pgmigrations as a table

Or SQL directly:

docker exec pedscribe-db psql -U pedscribe -d pedscribe \
  -c "SELECT id, name, run_on FROM pgmigrations ORDER BY id;"

Raw SQL inside a migration

exports.up = (pgm) => {
  pgm.sql(`
    CREATE INDEX CONCURRENTLY idx_audit_action
    ON audit_log (action)
    WHERE action IN ('login', 'login_failed', 'session_idle_timeout');
  `);
};

CREATE INDEX CONCURRENTLY cannot run in a transaction. Add exports.disableTransaction = true; to the file when using it.

Conventions

  • One logical change per file. No bundling unrelated alters.
  • Always write down() unless rollback is impossible (e.g., dropping a column that had unique data).
  • Name files by what they do (add_foo, backfill_bar), not ticket numbers.
  • Filename UTC-ms prefix drives ordering across forks / PRs.
  • Never edit an already-applied migration. Fix forward with a new file.

Rollback semantics

migrate:down runs the file's down() and removes its row from pgmigrations. An empty / missing down() still clears the row — the next up reapplies the migration. Treat missing down as "no-op rollback" and document it in the file header.