- 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)
9.1 KiB
Database schema
PostgreSQL 16 with pgvector. Image pgvector/pgvector:pg16, data in the
pgdata volume. Connection pool: 20 max, 30 s idle timeout, 5 s connect
timeout.
Schema is managed in two layers:
- Baseline init —
src/db/database.js. IdempotentCREATE TABLE IF NOT EXISTS+ALTER TABLE ADD COLUMN IF NOT EXISTS. Runs on every boot. Represents everything that predated the migration tool. - Versioned migrations —
migrations/vianode-pg-migrate. All new schema changes go here. Seedocs/migrations.md.
Extensions
CREATE EXTENSION IF NOT EXISTS vector;
Tables
users
Core accounts. Local-auth + OIDC federation + per-user preferences.
| Column | Type | Notes |
|---|---|---|
| id | SERIAL PK | |
| TEXT UNIQUE NOT NULL | ||
| password | TEXT NOT NULL | argon2id hash (primary) or bcrypt hash (legacy / rehashed on next login). For OIDC-auto-created users: random hex, not verifiable. |
| name | TEXT | |
| role | TEXT | user / admin / moderator |
| email_verified | BOOLEAN DEFAULT false | |
| verify_token, verify_expires | TEXT, BIGINT | Email verification |
| totp_secret, totp_enabled | TEXT, BOOLEAN DEFAULT false | 2FA |
| totp_backup_codes | TEXT | JSON array of bcrypt hashes of 10-character recovery codes. Consumed atomically on login. |
| oidc_sub | TEXT | IdP subject identifier (when linked) |
| disabled | BOOLEAN DEFAULT false | Soft disable |
| nextcloud_url, nextcloud_user, nextcloud_token, nextcloud_folder | TEXT | WebDAV credentials. nextcloud_token stored AES-256-GCM encrypted (prefix enc1:). |
| reset_token, reset_expires | TEXT, BIGINT | Password reset |
| stt_model, tts_voice | TEXT | Per-user STT/TTS override |
| webdav_learning_path | TEXT | Learning Hub file-browser root |
| created_at, updated_at | TIMESTAMPTZ DEFAULT NOW() |
user_sessions
Authoritative session registry.
| Column | Type | Notes |
|---|---|---|
| id | TEXT PK (UUID) | |
| user_id | INTEGER FK users.id | |
| token_hash | TEXT NOT NULL | SHA-256 of the JWT. Index idx_sessions_token_hash uses COLLATE "C" for ICU-drift immunity. |
| ip_address, user_agent | TEXT | |
| device_label | TEXT | Parsed from UA (Chrome on Android, PedScribe (Android), etc.) |
| created_at, last_activity | TIMESTAMPTZ DEFAULT NOW() | last_activity only updated on POST/PUT/DELETE/PATCH, throttled to once per 10 min |
app_settings
Key-value runtime config. 2-minute in-memory cache.
| Column | Type | Notes |
|---|---|---|
| key | TEXT PK | Also COLLATE "C" |
| value | TEXT | Plain or JSON |
| updated_at | TIMESTAMPTZ DEFAULT NOW() | |
| updated_by | INTEGER FK users.id |
audit_log
Human-level security and action audit. Writes are batched (1 s flush) by
src/utils/auditQueue.js.
| Column | Type | Notes |
|---|---|---|
| id | SERIAL PK | |
| user_id | INTEGER FK users.id | Null for unknown-user attempts |
| action | TEXT NOT NULL | e.g. login, login_failed, session_idle_timeout, password_changed, generate_soap, 2fa_backup_code_used |
| category | TEXT DEFAULT 'general' | auth, clinical, integration, export, documents, phi_access |
| details | TEXT | Free-form, PHI-redacted via src/utils/redact.js |
| ip_address, user_agent | TEXT | |
| model_used, tokens_used, duration_ms | TEXT, INT, INT | LLM-call fields (optional) |
| status | TEXT DEFAULT 'success' | |
| timestamp | TIMESTAMPTZ DEFAULT NOW() |
api_log
Per-request AI-call telemetry.
| Column | Type | Notes |
|---|---|---|
| id | SERIAL PK | |
| user_id | INTEGER FK users.id | |
| endpoint | TEXT | Route path |
| method | TEXT | |
| status_code | INTEGER | |
| request_size, response_size | INTEGER | Bytes |
| model_used | TEXT | |
| tokens_input, tokens_output | INTEGER | |
| cost_estimate | NUMERIC | USD estimate (hardcoded rates; OpenRouter uses live pricing) |
| duration_ms | INTEGER | |
| ip_address, error | TEXT | |
| timestamp | TIMESTAMPTZ DEFAULT NOW() |
access_log
Auth-only event stream.
| Column | Type | Notes |
|---|---|---|
| id | SERIAL PK | |
| user_id | INTEGER FK users.id | |
| action | TEXT | login, logout, failed_login, … |
| ip_address, user_agent | TEXT | |
| success | BOOLEAN | |
| timestamp | TIMESTAMPTZ DEFAULT NOW() |
saved_encounters
Draft/complete encounter workspace. Auto-expires (default 7 d,
site.auto_delete_days).
| Column | Type | Notes |
|---|---|---|
| id | SERIAL PK | |
| user_id | INTEGER FK users.id ON DELETE CASCADE | |
| label | TEXT NOT NULL DEFAULT 'Untitled' | Unique-per-user within active rows |
| enc_type | TEXT NOT NULL DEFAULT 'encounter' | encounter, dictation, soap, sickvisit, wellvisit, hospital, chart, milestones |
| transcript | TEXT | |
| generated_note | TEXT | |
| partial_data | TEXT | JSON of in-progress form state |
| status | TEXT DEFAULT 'active' | |
| version | INTEGER NOT NULL DEFAULT 1 | Optimistic lock. POST with expected_version mismatch returns 409. |
| idempotency_key | TEXT | Prevents duplicate creates from double-submit |
| created_at, updated_at | TIMESTAMPTZ DEFAULT NOW() | |
| expires_at | TIMESTAMPTZ | Default NOW() + 7 days |
user_memories
Per-user clinical-style hints injected into AI prompts.
| Column | Type | Notes |
|---|---|---|
| id | SERIAL PK | |
| user_id | INTEGER FK users.id ON DELETE CASCADE | |
| category | TEXT NOT NULL DEFAULT 'custom' | physical_exam, ros, encounter_format, custom, template_*, correction_* |
| name | TEXT NOT NULL | |
| content | TEXT NOT NULL | |
| created_at, updated_at | TIMESTAMPTZ DEFAULT NOW() |
audio_backups
Retry store for failed-transcription audio.
| Column | Type | Notes |
|---|---|---|
| id | SERIAL PK | |
| user_id | INTEGER FK users.id | |
| module | TEXT | encounter, dictation, etc. |
| mime_type | TEXT | |
| size_bytes, compressed_bytes | INTEGER | |
| audio_data | BYTEA | Gzip → AES-256-GCM (0x01 version prefix). Legacy rows (prefix 0x1F = raw gzip) pass through. |
| created_at, expires_at | TIMESTAMPTZ | 24 h default |
user_documents
Metadata for files in S3-compatible object storage. File bytes stay in S3.
| Column | Type | Notes |
|---|---|---|
| id | SERIAL PK | |
| user_id | INTEGER FK users.id | |
| s3_key | TEXT | Object storage key (prefixed with user id) |
| filename, mime_type | TEXT | |
| size_bytes | INTEGER | |
| description | TEXT | |
| created_at | TIMESTAMPTZ DEFAULT NOW() |
learning_categories, learning_content, learning_questions, learning_options, learning_progress
Learning Hub CMS tables. learning_content.embedding is VECTOR(768) for
semantic search (pgvector IVFFLAT index). See docs/learning-hub.md.
developmental_milestones
AAP-aligned pediatric milestone reference data. Age group + domain keyed.
| Column | Type | Notes |
|---|---|---|
| id | SERIAL PK | |
| age_group | TEXT | 2 months, 4 months, 1 year, … |
| domain | TEXT | motor, language, social, cognitive |
| milestone_text | TEXT | |
| sort_order | INTEGER | |
| created_at, updated_at | TIMESTAMPTZ DEFAULT NOW() |
pgmigrations
Created and managed by node-pg-migrate. Records applied migration filenames
- run time. Never edit by hand.
Indexes
Core btree indexes — see database.js for the full list.
users(email)—COLLATE "C"(lookup-critical auth path)user_sessions(token_hash)—COLLATE "C"audit_log(user_id),audit_log(timestamp),audit_log(action),audit_log(category)api_log(user_id),api_log(timestamp),api_log(endpoint)access_log(user_id),access_log(timestamp)saved_encounters(user_id),saved_encounters(expires_at),saved_encounters(idempotency_key)user_memories(user_id, category)audio_backups(user_id),audio_backups(expires_at)user_documents(user_id)learning_content(category_id)learning_progress(user_id, content_id)developmental_milestones(age_group, domain)
The COLLATE "C" indexes are immune to ICU library version changes between
Postgres image upgrades — silent index corruption from libc / ICU drift
cannot affect auth lookups.
Collation drift handling
On startup, src/db/database.js compares pg_database.datcollversion with
pg_database_collation_actual_version(). On mismatch it runs
REINDEX DATABASE + ALTER DATABASE … REFRESH COLLATION VERSION and logs
the event. npm run maint:reindex runs the same operation manually.
Auto-cleanup
Hourly job (plus 10 s after startup):
DELETE FROM saved_encounters WHERE expires_at < NOW();
DELETE FROM audio_backups WHERE expires_at < NOW();
DELETE FROM user_sessions WHERE last_activity < NOW() - INTERVAL '30 days';
(The session cleanup is optional safety — the idle middleware deletes stale rows eagerly.)
PHI at rest
| Column | Protection |
|---|---|
users.nextcloud_token |
AES-256-GCM via src/utils/crypto.js, prefix enc1: |
audio_backups.audio_data |
Gzip → AES-256-GCM, 0x01 version prefix |
audit_log.details |
Redacted (SSN, phone, email, DoB regex; 500-char cap; note-body heuristic truncation) |
| Error responses | Generic 'Request failed' on 500s; full detail stays in logger.error / Loki |