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

151 lines
6.8 KiB
Markdown

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