Skip to content

Manifest schema

orion-app.json declares your app's metadata and extension surfaces. orion validate enforces this schema before you publish, and orion generate writes well-formed entries automatically — so most of the time you'll only edit the manifest by hand for app-level fields like name, description, and scope.

Top-level fields

json
{
    "name": "My App",
    "slug": "my-app",
    "version": "1.0.0",
    "sdkVersion": "1.1.0",
    "description": "Short marketplace listing copy.",
    "scope": ["user/Patient.read", "openid", "fhirUser"],
    "entryPoint": "./src/main.tsx",
    "extensions": {
        "navigation": [],
        "pages": [],
        "blocks": [],
        "widgets": [],
        "actions": [],
        "writableExtensions": []
    }
}
FieldTypeRequiredNotes
namestringyesDisplay name.
slugstringyesURL-safe identifier. Lowercase alphanumeric with hyphens, 3-64 chars. Locked at first publish.
versionsemveryesBumped by you between publishes. The marketplace rejects re-uploads of the same version.
sdkVersionsemveryesThe @orion-ehr/app-bridge SDK version your bundle was authored against. Recognised values: 1.0.0, 1.0.1, 1.1.0. Other values produce a warning, not an error.
scopestring[]yesSMART-on-FHIR scopes the app needs (e.g. user/Patient.read, launch/patient, openid). The reviewer must approve any new scope.
entryPointstringyesRelative path to the source entry the bundler should compile (e.g. ./src/main.tsx). Used by orion generate to locate the file it appends imports to.
descriptionstringnoMarketplace listing copy.
authorstringnoDisplay name shown alongside the listing.
homepagestringnoURL surfaced in the marketplace listing.
supportstringnoSupport contact URL or mailto.
iconstringnoIcon path or URL.
thirdPartyServicesstring[]noOutbound origins your app talks to, surfaced to reviewers.
extensionsobjectnoContainer for every extension array. See sections below.

The validator also tolerates a deprecated top-level permissions field (a permissions: ... value emits a warning telling you to use scope instead).

Extensions

All extension arrays live under the top-level extensions object: extensions.navigation, extensions.pages, extensions.blocks, extensions.widgets, extensions.actions, extensions.writableExtensions. Each section is optional — declare only the surfaces you mount.


Pages

A full-page extension. Mounts inside the host EMR at the path you declare and renders inside the standard app shell.

json
{
    "extensions": {
        "pages": [
            {
                "id": "reports",
                "path": "/reports",
                "title": "Reports",
                "component": "ReportsPage",
                "layout": "app"
            }
        ]
    }
}
FieldTypeRequiredNotes
idstringyesUnique within the manifest. Used in routing.
pathstringyesPath the page mounts at. Must start with /.
titlestringyesDisplay title in the host's chrome.
componentstringyesNamed React component your entry file exports / imports.
layoutapp|fullscreen|modalnoapp keeps the host nav/header. fullscreen hides chrome. modal opens as an overlay.
renderModeinline|iframenoOverride the default render mode for this page.
requiredScopesstring[]noHide the page when the user lacks these scopes.
requiredRolesstring[]noHide the page when the user lacks these roles.

Generate via orion generate page <id> [--path <path>] [--layout <layout>].


Blocks

A block extension renders inline inside a host UI surface, before or after a named target slot.

json
{
    "extensions": {
        "blocks": [
            {
                "id": "status-bar",
                "target": "encounter-detail/status-bar",
                "action": "after",
                "component": "StatusBarBlock"
            }
        ]
    }
}
FieldTypeRequiredNotes
idstringyesUnique within the manifest.
targetstringyesInjection target — must match one of the host EMR's published slot names (e.g. encounter-detail/status-bar). See your developer portal for the current list.
actionbefore|afteryesPlacement relative to the target.
componentstringyesNamed React component your entry file exports / imports.
renderModeinline|iframenoOverride the default render mode.
minHeight / maxHeightnumbernoHeight bounds the host honours when sizing the block.
requiredScopesstring[]noHide the block when the user lacks these scopes.

Generate via orion generate block <id> --target <name> [--action before\|after].


Widgets

Widgets render in named mount points (sidebars, headers, etc.) — anywhere the host EMR exposes a widget slot.

json
{
    "extensions": {
        "widgets": [
            {
                "id": "patient-vitals",
                "title": "Patient Vitals",
                "mountPoint": "patient-detail-sidebar",
                "component": "PatientVitalsWidget"
            }
        ]
    }
}
FieldTypeRequiredNotes
idstringyesUnique within the manifest.
titlestringyesDisplay title shown in the widget chrome.
mountPointstringyesA widget slot name from the host catalog (e.g. patient-detail-sidebar, dashboard-widgets).
componentstringyesNamed React component your entry file exports / imports.
defaultCollapsedbooleannoStart collapsed in widget shells that support collapse.
minHeight / maxHeightnumbernoHeight bounds for the widget.
requiredScopesstring[]noHide the widget when the user lacks these scopes.

Generate via orion generate widget <id> --mount-point <slot>.


Actions

An action extension contributes a button/menu item to a host trigger (e.g. patient context menu).

json
{
    "extensions": {
        "actions": [
            {
                "id": "send-summary",
                "title": "Send Summary",
                "trigger": "patient-context-menu",
                "handler": "sendSummary"
            }
        ]
    }
}
FieldTypeRequiredNotes
idstringyesUnique within the manifest.
titlestringyesDisplay label for the menu/button.
triggerstringyesTrigger name from the host catalog (e.g. patient-context-menu, appointment-context-menu, global-command-palette, toolbar).
handlerstringyesNamed export your entry file imports — invoked when the action fires.
iconstringnoLucide icon name.
shortcutstringnoKeyboard shortcut hint.
requiredScopesstring[]noHide the action when the user lacks these scopes.

Generate via orion generate action <id> --trigger <name>.


A nav entry adds a link to one of the host EMR's navigation areas.

json
{
    "extensions": {
        "navigation": [
            {
                "id": "reports",
                "title": "Reports",
                "icon": "FileText",
                "placement": "sidebar",
                "target": "/reports"
            }
        ]
    }
}
FieldTypeRequiredNotes
idstringyesUnique within the manifest.
titlestringyesDisplay text.
iconstringyesLucide icon name (e.g. FileText, Activity).
placementsidebar|header|settings|patient-actionsyesWhich nav area to insert into.
targetstringconditionallyWhere the nav entry navigates to. Required unless children is provided.
ordernumbernoSort key within the placement.
badgestring | numbernoBadge text shown next to the entry.
requiredScopesstring[]noHide the entry when the user lacks these scopes.
childrenobject[]noSub-entries. Each child requires id, title, and target.

Generate via orion generate nav <id> --placement <name> --path <path> [--icon <name>]. The generator normalises its --path argument into the manifest's target field.


Writable extensions

Declare custom FHIR Extension fields your app needs to write — without baking tenant-specific StructureDefinition URLs into the manifest. You declare structural intent (a key, a value type, the resource types it applies to); the host resolves a per-tenant URL at install time. The host gates writes against the resolved allow-list, so apps cannot write extensions they didn't declare.

json
{
    "extensions": {
        "writableExtensions": [
            {
                "key": "preferred-pharmacy",
                "title": "Preferred Pharmacy",
                "description": "Patient's preferred pharmacy for prescription routing.",
                "valueType": "string",
                "appliesTo": ["Patient"]
            }
        ]
    }
}
FieldTypeNotes
keystringStable identifier for this extension across tenants. Lowercase kebab-case, 1-64 chars (a-z, 0-9, hyphen-separated, no leading/trailing/double hyphens). Must be unique within the manifest.
titlestringDisplay label shown in the install dialog and the developer portal.
descriptionstring (optional)Free-text explanation for the tenant admin reviewing the install.
valueTypeenumThe FHIR primitive type for Extension.value[x]. One of: string, boolean, integer, decimal, code, date, dateTime, Coding, CodeableConcept.
appliesTostring[]FHIR resource types this extension can attach to (e.g. ["Patient"] or ["Patient", "Encounter"]). Non-empty.

Caps: at most 50 entries in writableExtensions[] per manifest, and at most 50 resource types per appliesTo. Apps that need more are doing something the format isn't designed for — file a spec.

orion init scaffolds one example entry. orion validate enforces this schema before publish. There's no orion generate writable-extension today; hand-edit the manifest.

For the install-time URL resolution flow and runtime URL discovery from inside your app, see the Use writable extensions recipe.


Scopes

json
{
    "scope": [
        "launch/patient",
        "openid",
        "fhirUser",
        "user/Patient.read",
        "user/ActivityDefinition.read"
    ]
}

scope is a SMART-on-FHIR scope list. Two shapes are recognised:

  • Launch scopeslaunch, launch/patient, launch/encounter, openid, fhirUser, profile, offline_access.
  • Resource scopes{patient|user|system}/{Resource}.{permission}, with optional SMART v2 query suffixes (e.g. patient/Observation.rs?category=laboratory). Permission is one of read, write, *, or a SMART v2 letter combination (c, r, u, d, s).

orion validate checks each scope against these patterns; the host's reviewer flow checks for approved values. The reviewer must approve any new SMART scope before it goes live in production.

Documents @orion-ehr/cli v0.0.15 — released under the MIT License.