- 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)
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_SECRETandDATA_ENCRYPTION_KEY— refuses to start in production without them. - Runs
initDatabase()(idempotent baseline) thennode-pg-migrate(versioned delta). - Checks
pg_databasecollation version; auto-REINDEXes + refreshes on drift. - Reads git HEAD for
BUILD_ID; injects?v=BUILD_IDinto every local/js/*.jsand/css/*.cssreference inindex.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()).