Skip to Content
DocsCore concepts

Core concepts

This section explains how the core pieces of O3 fit together. By the end, you should understand how O3 starts up and how modules are loaded and configured. The diagram below shows the main building blocks and how they relate.

O3 architecture

Diagram notes (for clarity): spa-assemble-config.json is used during distro assembly to download frontend module packages and produce the two runtime artifacts the app shell reads: importmap.json and routes.registry.json. Modules do not load directly from npm at runtime. They load from bundle URLs in the import map, or from local dev server URLs when import map and route overrides are active.

The major pieces in the O3 frontend architecture are:

  • The app shell - the base layer that coordinates startup and runtime behavior.
  • Frontend modules - composable UI building blocks.
  • The import map - a JSON file that maps module names to module bundle URLs.
  • The routes registry - a JSON file that tells the app shell which pages, extensions, modals, workspaces, and feature flags modules provide.
  • The core framework - shared libraries and APIs used by frontend modules.

Where this lives in code

Two repositories are especially useful when you want to trace these concepts in real code. The openmrs-esm-core monorepo contains the runtime and tooling:

The openmrs-distro-referenceapplication repository shows how a real distro uses that tooling. Its frontend/spa-assemble-config.json chooses frontend modules and versions, while frontend/spa-build-config.json points the built app shell at the generated import map and routes registry.

App shell

The app shell is the host application that boots O3 and coordinates loading and runtime services. Introduced in RFC-26 , it handles:

  • Startup: renders index.html, loads the import map and routes registry, and wires module loading/routing.
  • Runtime services: defines breakpoints, subscriptions (modals, toasts, inline notifications), and offline support.
  • Platform wiring: initializes global state and sets up the configuration and extension systems.

At startup, the app shell does not eagerly execute every frontend module. It sets up core UI services, reads <script type="systemjs-importmap"> and <script type="openmrs-routes"> tags from index.html, registers the metadata declared in the routes registry, loads configuration, and then starts single-spa. Module code is fetched later, when a route, extension, modal, or workspace actually needs one of that module’s exported lifecycle functions.

Learn more: App shell deep dive

Frontend modules

Frontend modules are the units of functionality that make up the O3 UI. Think of each module as a mini‑app that owns a slice of the interface (for example, the patient chart or the primary navigation).

O3 treats these modules as microfrontends (popularized by single-spa ). The app shell reads the routes registry generated from each selected module’s route metadata, registers the declared pages, extensions, modals, workspaces, and feature flags, then loads the module’s exported lifecycle functions from the import map when UI code is needed. If a module exports startupApp(), the app shell calls it once before loading that module’s component lifecycle functions. Modules export lifecycle functions shaped for single-spa, following the interface defined in RFC-26 . In most distros, the import map and routes registry are generated during distro assembly from spa-assemble-config.json and the selected modules’ packaged route metadata. Extensions are plug‑in UI blocks that modules define for named slots.

Anatomy essentials (files you’ll touch most often):

  • package.json - module metadata, scripts, and dependencies
  • src/index.ts - entry point that exports lifecycle functions and setup code
  • src/routes.json - static route metadata for the app shell
  • src/config-schema.ts - where the module’s configuration properties live

Frontend module code is loaded on demand by the app shell when it’s needed, which improves initial load performance.

Frontend modules can live in standalone repositories or in domain-focused monorepos when multiple modules share a domain. For example, patient-management modules (registration, queues, appointments, bed management, etc.) are grouped in the openmrs-esm-patient-management monorepo.

You control which modules are included in a distro by editing the distro’s spa-assemble-config.json file. To add your own module, scaffold it with npm create @openmrs/o3-app@latest, publish it to npm under your own namespace, then add an entry for it in spa-assemble-config.json. The template app repo  remains useful as a reference for the generated module structure.

Next:

Import map

An import map is a browser specification for mapping module names to the URLs they should load from. Introduced in RFC-4 , it tells the app shell exactly where to fetch each frontend module. The app shell preloads the import map on startup and then uses it to resolve and load module bundles.

In practice, import maps are generated and served by a distro (not the app shell). For example, the import map for the O3 community reference application  is served at /openmrs/spa/importmap.json, and the module/version list that generates it lives in frontend/spa-assemble-config.json. Most distros follow the same approach.

To view your distro’s import map in the browser, navigate to /openmrs/spa/importmap.json (or your distro’s SPA base path), where you’ll see a simplified excerpt like:

{ "imports": { "@openmrs/esm-home-app": "./openmrs-esm-home-app-4.1.1-pre.211/openmrs-esm-home-app.js", "@openmrs/esm-login-app": "./openmrs-esm-login-app-4.3.2-pre.671/openmrs-esm-login-app.js", "@openmrs/esm-primary-navigation-app": "./openmrs-esm-primary-navigation-app-4.3.2-pre.671/openmrs-esm-primary-navigation-app.js", "@openmrs/esm-patient-chart-app": "./openmrs-esm-patient-chart-app-4.3.1-pre.1352/openmrs-esm-patient-chart-app.js" } }

The keys in this object are module names (unique identifiers), and the values are bundle URLs. These URLs are often relative paths in assembled distros, but local development overrides can point at absolute URLs such as a local dev server. The app shell reads the import map at runtime when it needs to load a module’s code. Current O3 frontend modules are loaded as Module Federation containers: the app shell appends a script tag for the bundle URL, then calls the container’s init and get methods to load exported lifecycle functions. See the App shell deep dive for the loading mechanics.

Learn more: Module loading

Routes registry

The routes registry is a JSON object that maps module names to the static metadata from those modules’ routes.json files. The app shell reads it at startup to register:

  • Pages and their routes
  • Extensions and the slots they attach to
  • Modals, workspaces, workspace groups, and feature flags

During distro assembly, openmrs assemble extracts each selected frontend module, reads its packaged routes.json, and writes the combined registry to routes.registry.json. In local development, openmrs develop reads src/routes.json directly and serves a generated registry for the running dev app.

This is intentionally separate from the import map. The import map answers “where is this module’s JavaScript bundle?” The routes registry answers “what pages, extensions, and other UI entry points does this module provide?” A module usually needs entries in both places before its UI can render.

How O3 loads modules (quick mental model)

  1. The app shell boots and preloads the import map and routes registry.
  2. It reads the routes registry and registers the declared pages, extensions, modals, workspaces, and feature flags.
  3. When a route, extension, modal, or workspace needs code, it resolves that module’s URL from the import map and loads its exported lifecycle functions.
  4. The router mounts pages as users navigate, and extension slots mount extensions when their host UI renders.

If you need to change…

  • Which modules and versions are included in a distro → edit the distro’s spa-assemble-config.json.
  • Which pages or extensions a module provides → edit the module’s src/routes.json.
  • How modules load globally → app shell and dynamic-loading behavior.
  • What a module does after it loads → the module’s own repo (UI, lifecycle exports, config, and business logic).

Core framework

The O3 framework is the shared runtime library that frontend modules build on. It is published as @openmrs/esm-framework, but in the source repo it is made of many focused packages under packages/framework/*. The aggregate package re-exports the public APIs from those packages so modules can usually import from one place:

import { openmrsFetch, useConfig, ExtensionSlot, showToast } from "@openmrs/esm-framework";

Think of the framework as the contract between the app shell and frontend modules. The app shell decides which modules are available and when their code should load. The framework gives those modules the APIs they use after they are loaded.

The most important pieces are:

  • Backend and session APIs: openmrsFetch, REST base URLs, backend dependency checks, current user/session helpers, patient and visit utilities, and event subscriptions.
  • Runtime metadata systems: configuration schemas, extension registration, extension slots, feature flags, global stores, app context, and expression evaluation.
  • Rendering and lifecycle helpers: React hooks, Extension and ExtensionSlot components, lifecycle helpers such as getSyncLifecycle and getAsyncLifecycle, and utilities that bridge React components into single-spa lifecycles.
  • Shared UX services: styleguide components, navigation and breadcrumbs, modals, workspaces, toasts, snackbars, notifications, translations, and offline support.

For module authors, the rule of thumb is: use the framework for cross-cutting O3 behavior instead of inventing local versions of fetch wrappers, global state, extension wiring, config loading, or notification UI. If you need to change how those shared systems behave globally, that usually belongs in openmrs-esm-core, not in an individual frontend module.

Learn more: Framework API reference

Next steps

Ready to build? Continue to Frontend modules to see how modules are structured and loaded.

Last updated on