# 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 20 (Alpine) + Express 4 |
| Database | PostgreSQL 16 with `pgvector` extension |
| 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: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                # LiteLLM embeddings
    transcribe.js, tts.js        # LiteLLM STT / TTS routes
  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/                            # 24 vanilla JS modules
  components/                    # per-tab HTML fragments
  css/styles.css

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

.forgejo/workflows/
  android-apk.yml                # signed APK on tag push; optional Play upload
  docker-build.yml               # Forgejo registry Docker image build

.github/workflows/
  auto-version.yml               # conventional-commits → semver bump → tag
  android-release.yml            # legacy GitHub tag APK release path
  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           (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 <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`.

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.
