pediatric-ai-scribe-v3/docs/architecture.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

6.8 KiB

Architecture

Self-hosted, single-tenant clinical documentation platform. Dockerized Node.js server + PostgreSQL + vanilla-JS SPA. No build step on the frontend.

Stack

Layer Technology
Runtime Node.js 20 (Alpine) + Express 4
Database PostgreSQL 16 with pgvector extension
Frontend Vanilla JavaScript SPA, service-worker cache
Mobile Capacitor 6 wrapper (Android + iOS)
Container Docker Compose (app + db)
Reverse proxy External (Caddy, Nginx, Traefik — any)

Repository layout

server.js                        # Express entry
Dockerfile                       # node:20-alpine base
docker-compose.yml               # app + postgres
migrations/                      # node-pg-migrate files (versioned)
scripts/
  maintenance.js                 # REINDEX / collation-drift CLI
  release.sh                     # semver bump + tag + push

src/
  db/
    database.js                  # pg pool, idempotent baseline init, helpers
    migrate.js                   # programmatic node-pg-migrate runner
  middleware/
    auth.js                      # JWT + session-table validation, sliding idle
    logging.js                   # request log
  utils/
    ai.js                        # callAI() multi-provider router
    models.js                    # model registry + server-side whitelist
    prompts.js                   # prompt templates (DB-overridable)
    crypto.js                    # AES-256-GCM (PHI at rest)
    passwords.js                 # argon2id with bcrypt fallback + rehash
    sessions.js                  # token hashing, UA parser, session-id gen
    platform.js                  # isMobileClient() detection
    redact.js                    # PHI redactor for audit details
    auditQueue.js                # batched audit/api/access log writer
    fileType.js                  # magic-byte upload verification
    promptSafe.js                # <UNTRUSTED_*> LLM prompt wrapper
    logger.js                    # audit/api/access + Loki shipper
    errors.js                    # generic 500 responder
    models.js, prompts.js, ai.js # AI provider + model + prompt management
    embeddings.js                # Vertex / LiteLLM / OpenAI embeddings
    transcribe*.js, tts*.js      # STT / TTS provider clients
  routes/                        # 27 Express routers (auth, hpi, soap, …)

public/                          # SPA
  index.html                     # shell, loads components on demand
  sw.js                          # service worker (cache shell, network-first API)
  js/                            # 24 vanilla JS modules
  components/                    # per-tab HTML fragments
  css/styles.css
  models/                        # bundled Whisper WASM + model files

mobile/                          # Capacitor wrapper
  capacitor.config.json          # appId com.pedshub.scribe
  src/                           # launcher (server-URL picker)
  android/                       # generated AS project + native Java

.github/workflows/
  auto-version.yml               # conventional-commits → semver bump → tag
  android-release.yml            # signed APK on tag push
  docker-publish.yml             # multi-arch image on tag push
  version-bump.yml               # manual dispatch override
  build-apk.yml                  # legacy TWA APK

Request pipeline

request
  → helmet          (CSP, HSTS, X-Content-Type-Options, …)
  → CORS            (APP_URL + CORS_ORIGINS whitelist, fail-closed in prod)
  → cookieParser
  → express.json    (10 MB cap)
  → rate limiters   (general 200 req/min, per-endpoint tighter on auth)
  → static          (public/ with no-cache on HTML, 1h on JS/CSS; ?v=BUILD_ID busts cache per deploy)
  → route           (27 routers under /api/*)
    → authMiddleware (on protected routes: JWT, DB session check, 24h idle, last_activity update)
  → handler
  → response

On boot, server.js:

  • Validates JWT_SECRET and DATA_ENCRYPTION_KEY — refuses to start in production without them.
  • Runs initDatabase() (idempotent baseline) then node-pg-migrate (versioned delta).
  • Checks pg_database collation version; auto-REINDEXes + refreshes on drift.
  • Reads git HEAD for BUILD_ID; injects ?v=BUILD_ID into every local /js/*.js and /css/*.css reference in index.html.
  • Registers SIGTERM/SIGINT handlers that drain the audit queue and close the pool before exit.

Auth model

Hybrid, runtime-selected by User-Agent and X-Client header:

Client Token transport Persistence Idle policy
Web browser ped_auth httpOnly cookie, sameSite=lax 30 d maxAge (sliding) 24 h from last write request
Capacitor app (PedScribe-Android / Capacitor UA) Authorization: Bearer <jwt> iOS Keychain / Android EncryptedSharedPreferences via capacitor-secure-storage-plugin No server-side idle check (persistent)

Sessions are validated against user_sessions.token_hash on every request. Any logout / password-change / admin-revoke drops the row and the next request gets 401. The service worker clears its caches on logout so a stale shell never shows PHI on a shared workstation.

Conventional-commits auto-tag workflow can push a new semver tag using a RELEASE_PAT PAT secret so downstream release workflows fire on the tag push (the default GITHUB_TOKEN is blocked from triggering other workflows by design).

Frontend

Single HTML document with #auth-screen and #main-app sections. Tabs are per-feature HTML fragments under public/components/ fetched on demand. JS modules talk via window globals and CustomEvent on document — no bundler, no framework. Loader order is fixed in index.html.

authFetch.js installs a global fetch interceptor that treats any 401 on an authenticated request as a signal to clear local session state and redirect to login. A BroadcastChannel('pedscribe-auth') pushes that signal to sibling tabs so logging out in one tab drops UI in every open tab.

Docker topology

Container Image Internal port External
pediatric-ai-scribe ped-ai-local:latest (built from repo) 3000 127.0.0.1:3552
pedscribe-db pgvector/pgvector:pg16 5432 not exposed

Named volumes: pgdata (database), scribe-logs (filesystem audit logs). Application health-check polls GET /api/health.

A reverse proxy terminates TLS and forwards to 127.0.0.1:3552. The app is never bound to a public interface directly.

Service worker

sw.js implements two strategies:

  • Shell assets (/, /js/*, /css/*, /components/*) — cache-first.
  • /api/* — network-first with cached fallback. Ensures fresh data online, last-known-good when offline.

Precached on install: index.html, core JS, main stylesheet, login component. Cleared on logout (caches.keys() → caches.delete()).