Platform runtime
The application is a modular monolith: domains are separated by module boundaries and lint rules, not by network hops, which keeps a clinical transaction in one database transaction. Every request passes a fixed middleware chain, and every AI request additionally passes compliance, consent, budget, audit and output-scanning gates before a model is called. The HTTP surface — 700+ routes — is described by a generated manifest that CI checks on every change.
Modular monolith
ADR 0010 keeps Faurenis a single deployable application with internal boundaries. A dental visit touches Scribe, Perio, Copilot, artifacts, facts and audit at once; splitting those into services would turn one atomic transaction into a distributed one, with no corresponding gain at this scale. Services are split out only for a different runtime (GPU, Chromium) or a hard isolation boundary (no database credentials).
- Client
- Location
src/— React 19, Vite, Zustand- Rule
- May not import server code
- Shared
- Location
shared/— protocols, deterministic clinical engines, policies- Rule
- Portable: no Express, no React, no Node-only infrastructure
- Server
- Location
server/— routes, services, schema, middleware, integrations- Rule
- May not import client code; database access only through scoped helpers
- Satellites
- Location
services/*— renderer, imaging, LLM worker- Rule
- Own image, own service account; no in-process domain logic
| Layer | Location | Rule |
|---|---|---|
| Client | src/ — React 19, Vite, Zustand | May not import server code |
| Shared | shared/ — protocols, deterministic clinical engines, policies | Portable: no Express, no React, no Node-only infrastructure |
| Server | server/ — routes, services, schema, middleware, integrations | May not import client code; database access only through scoped helpers |
| Satellites | services/* — renderer, imaging, LLM worker | Own image, own service account; no in-process domain logic |
Request pipeline
Global middleware runs on every request in this order:
- Request id — a correlation id carried through logs, traces and audit rows.
- CORS for the native shells, then a per-request CSP nonce and Helmet security headers.
- Cookie parsing and retirement of legacy cookies.
- Structured HTTP logging with PII masking.
- Body limits: 1 MB by default, raised only for document and imaging upload routes.
- OpenTelemetry tracing and a request timeout.
- Route-group middleware: authentication, a rate limiter per route class,
no-storecaching for anything carrying clinical data.
The AI gate chain
Every route under /api/ai adds a second chain between authentication and the handler. Each link can refuse the request; none of them can be skipped by a handler.
- 1
- Gate
- Clinic AI rate limiter
- Refuses when
- A clinic exceeds its AI request rate
- 2
- Gate
- Compliance guard
- Refuses when
- The clinic’s compliance state does not permit AI processing (HTTP 451)
- 3
- Gate
- Consent attestation
- Refuses when
- AI-processing consent is missing, stale against the current form version, or uncertain — fails closed (HTTP 451)
- 4
- Gate
- Plan budget guard
- Refuses when
- The plan’s token allowance is exhausted
- 5
- Gate
- AI cost budget guard
- Refuses when
- The clinic’s monthly USD cap is reached (HTTP 429); overrides may only lower the cap
- 6
- Gate
- Overage debit guard
- Refuses when
- Metered overage cannot be debited
- 7
- Gate
- Audit logger
- Refuses when
- Records the request to the audit chain
- 8
- Gate
- Output PII scanner
- Refuses when
- Inspects the model’s reply for identifiers before it leaves the server
| Order | Gate | Refuses when |
|---|---|---|
| 1 | Clinic AI rate limiter | A clinic exceeds its AI request rate |
| 2 | Compliance guard | The clinic’s compliance state does not permit AI processing (HTTP 451) |
| 3 | Consent attestation | AI-processing consent is missing, stale against the current form version, or uncertain — fails closed (HTTP 451) |
| 4 | Plan budget guard | The plan’s token allowance is exhausted |
| 5 | AI cost budget guard | The clinic’s monthly USD cap is reached (HTTP 429); overrides may only lower the cap |
| 6 | Overage debit guard | Metered overage cannot be debited |
| 7 | Audit logger | Records the request to the audit chain |
| 8 | Output PII scanner | Inspects the model’s reply for identifiers before it leaves the server |
The route manifest
architecture/manifests/route-protocol-manifest.json is generated from the router itself. For every registration it records method, path, audience, authentication, clinic scope, middleware, body limit, status codes, content types and streaming mode. CI regenerates it and fails on any difference, so an API change is always a visible, reviewed diff.
- HTTP registrations
- 700+
- Server-sent event streams
- 8
- WebSocket paths
- 4
- Server service modules
- 970+
- TypeScript files under
server/services
Identity and sessions
- Sign-in: Google OAuth with server-side code exchange, Sign in with Apple, magic link, password, and a one-time token hand-off for the native apps.
- Access tokens are JWTs signed with PS256 by a Cloud KMS key; the public keys are published as JWKS.
- Refresh tokens are stored hashed, single-use, with a 14-day lifetime. Presenting a consumed token revokes its entire family.
- TOTP multi-factor authentication with backup codes; MFA secrets are encrypted with AES-256-GCM.
- Idle-session policy shared between client and server, a revocation registry checked by REST and WebSocket paths alike.
- Identity (
auth_users) is separate from authorization (users, roles, clinic membership).
Client architecture
The client is a React 19 single-page application built by Vite, with Zustand stores, route-level code splitting and a performance budget enforced by a bundle-size ratchet. The public site is prerendered to static HTML in both locales. Clinical working state that must survive a crash or a dropped connection is kept in an encrypted local recovery store, which is always subordinate to the server (REC-01) and wiped on sign-out.
- Microphone
- Approach
- One ownership registry for the whole app; spoken output observes it and yields to clinical capture
- 3D and imaging
- Approach
- three.js through React Three Fiber; Cornerstone for DICOM; pixels are never rewritten
- Internationalisation
- Approach
- i18next with 17,000+ keys per language and CI-enforced parity; locale bundles split for the public site
- Offline and skew
- Approach
- Old clients remain valid against new servers across a flag boundary; destructive assumptions require a minimum-version gate (REC-10)
| Concern | Approach |
|---|---|
| Microphone | One ownership registry for the whole app; spoken output observes it and yields to clinical capture |
| 3D and imaging | three.js through React Three Fiber; Cornerstone for DICOM; pixels are never rewritten |
| Internationalisation | i18next with 17,000+ keys per language and CI-enforced parity; locale bundles split for the public site |
| Offline and skew | Old clients remain valid against new servers across a flag boundary; destructive assumptions require a minimum-version gate (REC-10) |
server.tsserver/middleware/architecture/manifests/route-protocol-manifest.jsondocs/adr/0010-modular-monolith-boundaries.mddocs/adr/0011-http-contract-ownership.md