Skip to content

Architecture Overview

Ankit Upadhyay edited this page Aug 13, 2026 · 1 revision

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.

The monorepo

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.

How the pieces fit

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
Loading

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 three boundaries

Web to API

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.

API to database

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.

Domain to FHIR

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.

Request path

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.

Technology choices

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

Deliberate absences

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.

Branch model

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.

Related pages

Clone this wiki locally