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
{
"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": []
}
}| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Display name. |
slug | string | yes | URL-safe identifier. Lowercase alphanumeric with hyphens, 3-64 chars. Locked at first publish. |
version | semver | yes | Bumped by you between publishes. The marketplace rejects re-uploads of the same version. |
sdkVersion | semver | yes | The @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. |
scope | string[] | yes | SMART-on-FHIR scopes the app needs (e.g. user/Patient.read, launch/patient, openid). The reviewer must approve any new scope. |
entryPoint | string | yes | Relative 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. |
description | string | no | Marketplace listing copy. |
author | string | no | Display name shown alongside the listing. |
homepage | string | no | URL surfaced in the marketplace listing. |
support | string | no | Support contact URL or mailto. |
icon | string | no | Icon path or URL. |
thirdPartyServices | string[] | no | Outbound origins your app talks to, surfaced to reviewers. |
extensions | object | no | Container 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.
{
"extensions": {
"pages": [
{
"id": "reports",
"path": "/reports",
"title": "Reports",
"component": "ReportsPage",
"layout": "app"
}
]
}
}| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Unique within the manifest. Used in routing. |
path | string | yes | Path the page mounts at. Must start with /. |
title | string | yes | Display title in the host's chrome. |
component | string | yes | Named React component your entry file exports / imports. |
layout | app|fullscreen|modal | no | app keeps the host nav/header. fullscreen hides chrome. modal opens as an overlay. |
renderMode | inline|iframe | no | Override the default render mode for this page. |
requiredScopes | string[] | no | Hide the page when the user lacks these scopes. |
requiredRoles | string[] | no | Hide 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.
{
"extensions": {
"blocks": [
{
"id": "status-bar",
"target": "encounter-detail/status-bar",
"action": "after",
"component": "StatusBarBlock"
}
]
}
}| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Unique within the manifest. |
target | string | yes | Injection 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. |
action | before|after | yes | Placement relative to the target. |
component | string | yes | Named React component your entry file exports / imports. |
renderMode | inline|iframe | no | Override the default render mode. |
minHeight / maxHeight | number | no | Height bounds the host honours when sizing the block. |
requiredScopes | string[] | no | Hide 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.
{
"extensions": {
"widgets": [
{
"id": "patient-vitals",
"title": "Patient Vitals",
"mountPoint": "patient-detail-sidebar",
"component": "PatientVitalsWidget"
}
]
}
}| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Unique within the manifest. |
title | string | yes | Display title shown in the widget chrome. |
mountPoint | string | yes | A widget slot name from the host catalog (e.g. patient-detail-sidebar, dashboard-widgets). |
component | string | yes | Named React component your entry file exports / imports. |
defaultCollapsed | boolean | no | Start collapsed in widget shells that support collapse. |
minHeight / maxHeight | number | no | Height bounds for the widget. |
requiredScopes | string[] | no | Hide 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).
{
"extensions": {
"actions": [
{
"id": "send-summary",
"title": "Send Summary",
"trigger": "patient-context-menu",
"handler": "sendSummary"
}
]
}
}| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Unique within the manifest. |
title | string | yes | Display label for the menu/button. |
trigger | string | yes | Trigger name from the host catalog (e.g. patient-context-menu, appointment-context-menu, global-command-palette, toolbar). |
handler | string | yes | Named export your entry file imports — invoked when the action fires. |
icon | string | no | Lucide icon name. |
shortcut | string | no | Keyboard shortcut hint. |
requiredScopes | string[] | no | Hide the action when the user lacks these scopes. |
Generate via orion generate action <id> --trigger <name>.
Navigation
A nav entry adds a link to one of the host EMR's navigation areas.
{
"extensions": {
"navigation": [
{
"id": "reports",
"title": "Reports",
"icon": "FileText",
"placement": "sidebar",
"target": "/reports"
}
]
}
}| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Unique within the manifest. |
title | string | yes | Display text. |
icon | string | yes | Lucide icon name (e.g. FileText, Activity). |
placement | sidebar|header|settings|patient-actions | yes | Which nav area to insert into. |
target | string | conditionally | Where the nav entry navigates to. Required unless children is provided. |
order | number | no | Sort key within the placement. |
badge | string | number | no | Badge text shown next to the entry. |
requiredScopes | string[] | no | Hide the entry when the user lacks these scopes. |
children | object[] | no | Sub-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.
{
"extensions": {
"writableExtensions": [
{
"key": "preferred-pharmacy",
"title": "Preferred Pharmacy",
"description": "Patient's preferred pharmacy for prescription routing.",
"valueType": "string",
"appliesTo": ["Patient"]
}
]
}
}| Field | Type | Notes |
|---|---|---|
key | string | Stable 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. |
title | string | Display label shown in the install dialog and the developer portal. |
description | string (optional) | Free-text explanation for the tenant admin reviewing the install. |
valueType | enum | The FHIR primitive type for Extension.value[x]. One of: string, boolean, integer, decimal, code, date, dateTime, Coding, CodeableConcept. |
appliesTo | string[] | 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
{
"scope": [
"launch/patient",
"openid",
"fhirUser",
"user/Patient.read",
"user/ActivityDefinition.read"
]
}scope is a SMART-on-FHIR scope list. Two shapes are recognised:
- Launch scopes —
launch,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 ofread,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.