Modular Deployment & Entitlements
medOS ships from one codebase but is sold and installed as a set of modules (country-agnostic features) layered with a single market pack (per-country overlay). A deployment is the cross-product of the two: pick the features a site purchased, pick the country it operates in, and let a dependency-resolving installer assemble the rest. The marginal site is cheap because it is configuration and data, not new code.
Two orthogonal axes
The deployment model separates what a hospital bought from where it runs.
MODULES (country-agnostic features — schema/logic/UI identical everywhere)
───────────────────────────────────────────────────────────────────────▶
MARKET PACKS │ read-model-core reporting scheduling telemetry biometric …
(per-country │ ────────────────────────────────────────────────────────────── ───
overlays: │ country A locale · currency · insurer rates · terminology
locale, │ country B locale · currency · insurer rates · terminology
seeds, │ country C locale · currency · insurer rates · terminology
insurers, │ … any future country = drop in a new market pack
terminology) ▼
- Modules are country-agnostic. The SQL, edge functions, and UI for a feature are the same in every country.
- Market packs are country overlays. They carry only what genuinely differs: locale, timezone, currency, insurer / scheme rate tables, terminology, and bilingual seed data.
- A deployment = chosen modules + one market pack. The application code never branches on country.
What is modular at each layer
| Layer | Modular unit | How it is selected |
|---|---|---|
| Frontend | Federated miniapp + an entitlement flag | Module registry + an enabled-modules allowlist |
| Backend | A microservice on the message bus | Run only the services a site needs |
| Read-model schema | A module package with a manifest | Dependency-resolved installer |
| Country config | A market pack | Selected by a region argument |
| Edge functions | A named function | Reported and deployed by the installer |
The two deploy contracts
Each modular unit declares itself with a small manifest so the installer can plan the work. Internal file paths and secrets are omitted here — these are illustrative shapes, not literal configs.
Module manifest — a country-agnostic feature
{
"id": "reporting",
"catalogCategory": "Reporting",
"requires": ["read-model-core", "scheduling", "telemetry"], // dependency edges
"migrations": ["..."], // schema — always applied
"seeds": ["..."], // demo rows — only with --with-seed
"edgeFunctions": ["..."], // deployed/reported by the installer
"miniapp": "reporting-dashboard",
"flags": { "ENABLED_FLAG": "true" } // the entitlement contract
}
Market-pack manifest — a country overlay
{
"name": "market-pack",
"locale": "xx", "timezone": "...", "currency": "XXX",
"country_code": "XX",
"terminology": { "...": "..." },
"facility_types": ["..."],
"seeds": ["..."], // SQL seeds applied for this region
"scripts": ["..."], // scripted seeds (e.g. reference data)
"complianceProfile": { "dataResidency": "...", "retentionDays": 0 }
}
A country that needs more than seeds (extra edge functions, dependencies) can also be expressed as a full module — the two contracts compose.
The installer
Installing a module set is a package-manager-style operation: resolve the dependency closure, order it deps-first, and apply each step idempotently.
Dependency resolver
Reads every module manifest, computes the dependency closure, topologically sorts it deps-first, and emits an ordered plan. Detects cycles and unknown modules before anything is applied.
install-module
Applies module schema in order, then the chosen country's market-pack seeds, and reports edge functions and flags to set.
One-click apply
A manual workflow runs the installer against a configured database — never auto-applied on push, since changing a live schema must be deliberate.
Re-runnable
Every migration and seed is guarded for existence, so applying any module set repeatedly never damages data.
Conceptually, one install command does the whole chain:
install-module reporting --region=<country> --with-seed
│
├─▶ resolve: read-model-core → scheduling → telemetry → reporting (deps first)
├─▶ apply each module's schema (idempotent) + optional demo seeds
├─▶ apply the country market-pack seeds (facility, rates, terminology)
└─▶ report: edge functions to deploy + entitlement flags + locale
Entitlement — catalog to flags to deploy
Entitlement is a two-layer gate. Layer 1 asks whether the deployment installed a feature; layer 2 asks whether the tenant's subscription tier includes it. Both default open so demos and dev never break.
featureAvailable(feature) =
moduleEnabled(moduleId) // Layer 1: deployment installed this module
&& deploymentFlag // and the feature is configured
&& isEntitled(feature) // Layer 2: tenant's tier includes the feature
| Layer | Question | Mechanism | Default |
|---|---|---|---|
| 1 — Module availability | Is the feature installed here? | An enabled-modules allowlist read by moduleEnabled(id) | Unset = all on |
| 2 — Subscription tier | Has the tenant paid for it? | A feature-to-tier matrix resolved by isEntitled(key) | Unset = all entitled |
Subscription tiers are ordered (for example free, basic, premium, enterprise). A feature lists the tiers that include it; a feature absent from the matrix is included everywhere for back-compatibility. When a tenant is not entitled, the UI shows an upgrade prompt instead of the feature, and the relevant backend endpoints reject the call as a trust anchor — so the gate holds even if the frontend is bypassed.
Editions and topologies
A small number of bundles map purchased modules and a tier onto ready-made configurations:
- LITE vs FULL — a trimmed module set for smaller sites versus the complete suite.
- Cloud / on-prem / air-gapped — the same modules and market pack, deployed to a managed cloud, a hospital's own servers, or a fully disconnected site.
Demo-safe by default
Every gate fails open until a deployment opts into enforcement. Nothing breaks during evaluation; production simply sets the allowlist and tier.
Three steps
Declare a module manifest, add a row to the tier matrix, and wrap the surface in the entitlement gate.
One pack
Drop a market-pack manifest plus bilingual idempotent seeds; the installer discovers it via the region argument with no code change.
Design philosophy
The system is built to be installed and re-installed safely against live data.
| Principle | What it means |
|---|---|
| Additive | New modules add tables, rows, and surfaces; they do not rewrite what exists. |
| Idempotent | Migrations and seeds are guarded, so any module set can be applied repeatedly. |
| Dependency-closed | A module always deploys with its full dependency closure, ordered deps-first. |
| Default-OFF / fail-open | Both entitlement layers default to enabled; enforcement is opt-in per deployment. |
| Country = data, not code | The application never branches on country — differences live in market-pack rows. |
| Residency-respecting | Each site keeps its read model in-region; only de-identified aggregates roll up to a group tier. |
| Bilingual seeds | Every seed carries the local language plus English. |
Edge and on-prem direction
The same modules and market pack drive edge and on-premise installs. The model supports a single all-in-one stack for a disconnected site, a trimmed LITE edition for resource-constrained hardware, and a cloud install for managed hosting — all from one codebase. Because a deployment is just module selection plus a market pack, moving a site between cloud, on-prem, and air-gapped is a re-deploy of the same artifacts rather than a rewrite.