# Architecture Self-hosted clinical documentation platform. Dockerized Node.js server, PostgreSQL, Redis, and vanilla-JS SPA. No build step on the frontend. ## Stack | Layer | Technology | |---|---| | Runtime | Node.js 24 (Alpine, digest-pinned) + Express 4 | | Database | PostgreSQL 16 via the digest-pinned `pgvector/pgvector:pg16` image | | Cache / state | Redis for operational cache, prompt suggestions, and queue groundwork | | Frontend | Vanilla JavaScript SPA, service-worker cache | | Mobile | Capacitor 6 wrapper (Android + iOS) | | Container | Docker Compose (app + db + Redis) | | Observability | Prometheus metrics at `/metrics`; structured app logs in files, Postgres, and optional Loki | | Reverse proxy | External (Caddy, Nginx, Traefik — any) | ## Repository layout ``` server.js # Express entry Dockerfile # node:24-alpine base, plus pandoc, python3/python-pptx/python-docx, poppler 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 sttProvider.js, ttsProvider.js # speech-to-text and text-to-speech routing documentExport.js # pptx/docx/pdf export slideSpec.js, docSpec.js # markdown -> typed spec for the renderers deckSchema.js, deckBuild.js # the deck a presentation is designed as deckReview.js # optional vision pass over a rendered deck routes/ # Express routers (auth, hpi, soap, patient education, …) public/ # SPA index.html # shell, loads components on demand sw.js # service worker (cache shell, network-first API) js/ # vanilla JS modules, no build step components/ # per-tab HTML fragments css/styles.css .forgejo/workflows/ docker-build.yml # dev + main: tests, image; main also publishes deploy.yml # manual dispatch: deploy.sh against the host ``` ## 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 (feature 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`. Post-note helpers such as billing suggestions, don't-miss review, and patient education handouts are reusable browser-side actions backed by authenticated JSON APIs. The patient education helper generates a parent-facing plain-text draft from the edited note and keeps the clinician in the review loop before copying or sharing. `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 | | `ped-ai-redis` | Redis | 6379 | not exposed | Named volumes: `pgdata` (database), `scribe-logs` (filesystem audit logs), and Redis data if persistence is enabled by compose. 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()`). ## Clinical Assistant And MCP The clinical assistant can call an external MCP-backed retrieval service. Ped-AI remains responsible for the user workflow, provider selection, prompts, and display. MCP remains responsible for Nextcloud access, indexing, retrieval, and vector search. Clinical answer response caching is intentionally disabled; Redis is used for operational metadata and prompt suggestions, not answer reuse. ## Speech Browser Whisper and browser-local Whisper model downloads are removed from runtime. Speech-to-text routes through LiteLLM; upstream provider choice belongs in LiteLLM config. Browser-native Web Speech remains available only when explicitly enabled by user settings and browser support. ## Operational map The sections above describe the code. These describe the running system: who owns what, what crosses each boundary, and where the truth lives when two places disagree. ### Ownership Ped-AI is a self-hosted Express application with a browser frontend, PostgreSQL storage, Redis operational state, LiteLLM model routing, and optional MCP-backed clinical retrieval. | Area | Owner | Notes | |---|---|---| | Web app | Ped-AI | Auth, UI, clinical workflows, admin settings, notes, My Resources, bedside tools | | Database | PostgreSQL | Users, sessions, settings, saved app data, audit/API/access logs | | Operational cache | Redis | Prompt suggestions, lightweight state, queue groundwork; not clinical answer caching | | Model gateway | LiteLLM | Text, speech and image model discovery and routing | | Clinical retrieval | MCP service | Nextcloud access, indexing, search, rerank, source metadata | | Reverse proxy | Caddy or equivalent | TLS and public routing | ### Request Flow Normal app request: ```txt browser -> reverse proxy -> Express middleware -> auth/session check when protected -> route handler -> PostgreSQL/Redis/provider calls as needed -> JSON or HTML fragment response ``` Clinical Assistant request: ```txt browser -> Ped-AI clinical assistant route -> MCP semantic search for indexed clinical sources -> Ped-AI builds grounded answer prompt -> LiteLLM chat model -> Ped-AI returns answer plus source metadata -> browser renders markdown, citations, and source cards ``` Ped-AI owns the user workflow and rendering. MCP owns retrieval and indexed source metadata. LiteLLM owns model routing. ### Runtime Boundaries | Boundary | Main Risk | Current Direction | |---|---|---| | Browser to Ped-AI | XSS, stale shell, session handling | Sanitized rendering, httpOnly cookie for web, cache busting | | Ped-AI to PostgreSQL | schema drift, slow queries | migrations, maintenance checks, indexes where needed | | Ped-AI to Redis | unavailable operational state | Redis is useful but should not hold required clinical answers | | Ped-AI to LiteLLM | provider downtime, wrong model mode | metadata-based model discovery and timeouts | | Ped-AI to MCP | retrieval latency/failure | explicit MCP client layer and graceful fallback messages | | MCP to Nextcloud | stale indexed metadata | scanner/indexer updates source metadata over time | ### Source Of Truth | Data | Source Of Truth | |---|---| | User accounts and sessions | Ped-AI PostgreSQL | | Admin app settings | Ped-AI PostgreSQL `app_settings` | | Clinical source documents | Nextcloud and MCP index | | Clinical source title/path shown to users | MCP result metadata, especially indexed `file_path` | | Clinical answer text | Generated per request; intentionally not cached | | Model availability | LiteLLM metadata and configured fallbacks | ### Deployment Shape Production usually runs: ```txt Caddy/TLS -> pediatric-ai-scribe container -> pedscribe-db container -> ped-ai-redis container -> LiteLLM endpoint -> MCP endpoint ``` The app should stay private behind the reverse proxy. Do not expose PostgreSQL, Redis, MCP internals, or provider keys publicly. ### Design Principles - Keep Ped-AI stateless enough to run more than one app container. - Keep clinical answer generation live and source-grounded; do not cache final clinical answers. - Prefer model capability metadata over model-name regexes. - Prefer indexed file names and paths over embedded PDF metadata for source titles. - Keep renderer fixes narrow and tested because LLM markdown is messy. - Keep old frontend globals working until the affected feature is intentionally converted to ESM.