# 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 # 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 ` | 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()`).