-
Notifications
You must be signed in to change notification settings - Fork 1
Architecture Overview
This page describes the shape of the system: what each workspace owns, where the boundaries are, and which direction dependencies run. It is for anyone forming a mental model before working on a specific part.
One repository, pnpm 10 workspaces, Turborepo for task orchestration, Node 22 everywhere. The rationale and the alternatives that were rejected are recorded in ADR-0001.
openrunic/
├── apps/
│ ├── web/ Next.js 16 staff EMR
│ └── api/ Hono service: FHIR R4 and internal surfaces
├── packages/
│ ├── types/ Shared TypeScript types, branded ids, result helpers
│ ├── fhir/ FHIR R4 types and domain mappers, round-trip tested
│ ├── ui/ React component library implementing the design system
│ └── database/ Prisma schema, migrations, tenant scoping, audit chain
├── docs/adr/ Architecture decision records
├── scripts/ci/ Dependency-free CI helper scripts
└── .github/ Workflows, templates, Dependabot config
Shared packages are consumed by path with workspace:*, not by publishing. There is no internal registry to operate, and a change to the schema, a mapper, the API, and the UI lands atomically in one pull request with one review.
flowchart TD
subgraph Clients
W["apps/web<br/>staff EMR, Next.js 16"]
P["apps/portal<br/>patient portal (branch)"]
T["Third-party apps"]
end
subgraph Service["apps/api (Hono)"]
BFF["/bff/v0<br/>internal, unstable"]
FHIR["/fhir<br/>FHIR R4, the public contract"]
MW["middleware chain<br/>request-id, authn, tenant-scope,<br/>policy, audit"]
REPO["repositories<br/>tenant-bound, no tenant parameter"]
end
subgraph Packages
UI["@openrunic/ui"]
FP["@openrunic/fhir<br/>mappers + round-trip tests"]
DB["@openrunic/database<br/>Prisma, tenancy, audit chain"]
TY["@openrunic/types"]
end
PG[("PostgreSQL 17")]
W --> BFF
P --> BFF
T --> FHIR
BFF --> MW
FHIR --> MW
MW --> REPO
REPO --> DB
FHIR --> FP
FP --> DB
DB --> PG
W --> UI
P --> UI
FP --> TY
DB --> TY
Dependencies run one way. The apps depend on the packages; the packages do not depend on the apps. @openrunic/fhir and @openrunic/database both depend on @openrunic/types and not on each other at runtime. @openrunic/ui depends on nothing in the repository.
The staff application talks to /bff/v0, an internal surface shaped for screens rather than for standards. It returns narrow projections with a uniform pagination envelope, and it is explicitly versioned v0 because it changes with the screens it serves.
Today the web app runs in mock mode by default, satisfying the same client interface from fixtures. See Web app.
There is deliberately no second proprietary public API. Third parties use /fhir.
Every handler reads and writes through repository interfaces in apps/api/src/repositories/. Those interfaces take no tenant parameter; the registry binds a tenant per request. Two implementations exist: an in-memory one for tests and a Prisma one for runtime. The test suite drives the real application through app.request(...) against the in-memory implementation, so most of the API suite runs with no database and no port binding.
The Prisma client is wrapped by the tenant scoping extension from @openrunic/database, which ANDs a tenant predicate onto every query and throws on the raw-SQL escape hatches. See Multi-tenancy and isolation.
FHIR lives at the edge and nowhere else. packages/fhir owns bidirectional mappers, and apps/api/src/fhir/ owns the routes that use them. The storage layer knows nothing about FHIR, and the FHIR layer knows nothing about Prisma. See FHIR boundary.
A request to a protected route passes through five middleware stages in a fixed order, declared as data so the order is asserted rather than assumed:
request-id -> authn -> tenant-scope -> policy -> audit -> handler
Each stage depends on the one before it and on nothing after it. Authentication before scope means a tenant can never come from an unverified request. Scope before policy means a role is always evaluated inside an organisation. Policy before audit means a denial has somewhere to be recorded. The full description is on API design.
| Layer | Choice | Why, in one line |
|---|---|---|
| Monorepo | pnpm 10 workspaces plus Turborepo | Cross-cutting changes land atomically (ADR-0001) |
| Runtime | Node.js 22 | One runtime version, enforced by engines and engine-strict
|
| Web | Next.js 16, React 19 | App Router, server components for metadata, client components for interaction |
| API | Hono | Small, fast, typed context, no framework ceremony |
| Database | PostgreSQL 17 via Prisma 7 | Typed relational access, migrations as reviewable diffs (ADR-0002) |
| Interop | FHIR R4 (4.0.1) | The version every current regulatory programme speaks |
| Components |
@openrunic/ui plus Storybook 9 |
One design system implementation, one place to review it |
| Tests | Vitest 4 | One runner across every workspace |
| Lint and format | ESLint 9 flat config plus Prettier | Prettier owns formatting; ESLint owns correctness |
Knowing what is not here is as useful as knowing what is.
No machine-learning runtime in the core deployment. Recorded in ADR-0004. The reasons are runtime shape, model-weight licence provenance, deployability on a stock Linux box, and the medical-device line. If it ever ships it arrives as an optional out-of-process adapter, never on the request path.
No FHIR-native document store and no external FHIR server. Both were considered and rejected in ADR-0002.
No vendored terminology content. ICD-10-CM, CPT, LOINC, SNOMED CT, and RxNorm all carry their own licences. TerminologyCode is a bring-your-own cache, and system is a string rather than an enum so a new system loads without a migration.
No rate limiting, CORS, CSRF, or timeout middleware in the API yet. Worth knowing before you deploy anything.
No row-level security in Postgres yet. Designed and written out in the schema header, deferred until the API wraps requests in a transaction that sets the tenant session variable.
Two long-lived branches. main is the release branch and moves only through promotion pull requests and hotfixes. dev is the integration branch, and every feature, fix, and docs pull request targets it. A single aggregate check, CI Required, is the only required status check on both. Recorded in ADR-0003 and described on Development workflow.
openrunic is an open-source operating system for human health. Pre-alpha: do not run it in production, and never put real patient data into it.
Repository · Licence (AGPL-3.0-only) · Security policy · Contributing · Code of conduct
Where this wiki and the repository disagree, the repository is right.