Concepts
Start here if you've never built an Orion app before. This page explains what an Orion app is, where it runs, and how the developer-side tooling lines up against the host EMR at runtime.
What an Orion app is
An Orion app is a JavaScript bundle that runs inside the Orion EMR's browser shell. The host EMR loads your bundle into an iframe, mounts your component at one of the published extension points (page, block, widget, action, or nav item), and hands it a context — current patient, current encounter, current user — along with an authenticated way to call FHIR.
Apps ship in two flavors. A single-tenant app is installed only on the tenant that built it. A marketplace app is published to the Orion marketplace and any tenant can install it. The bundle is the same either way; only the distribution path differs.
You declare extension points, scopes, and metadata in orion-app.json. The host EMR reads that manifest at install time and only mounts your app where the manifest says it can be mounted.
Architecture
Two things to notice. First, the CLI talks to the Portal, not to the host EMR your users are on. Second, your shipped bundle never talks back to your machine — once published, it's served from the marketplace CDN directly to each tenant.
Two auth systems
The host EMR runs two distinct token systems. They share one scope vocabulary (SMART on FHIR), but they're issued by different endpoints, signed differently, and live for different durations. You'll touch both as an app developer.
1. SMART on FHIR (OAuth2 bearer tokens) — for clinical data access via the FHIR REST API. Issued by the host's OAuth2 server (Laravel Passport) using the standard SMART launch flow. Tokens carry SMART scopes — launch-context scopes (launch, launch/patient, launch/encounter, openid, fhirUser, offline_access) and resource scopes of the form {patient,user,system}/{Resource}.{read,write} (e.g. user/Patient.read, patient/Observation.read). You declare the scopes you need in orion-app.json's scope[]; the host issues a token scoped to that intersection and you send it as Authorization: Bearer <token> to FHIR REST endpoints. Token lifetimes are set per-deployment (Passport default in this codebase is 30 days for developer/CLI tokens; FHIR access tokens minted to apps follow the host's per-tenant Passport configuration — don't hardcode an expiry, refresh on 401).
2. App Bridge session token (custom JWT) — for cross-origin iframe communication back to the host page. Issued by POST /api/app-bridge/token (AppBridgeTokenController), which the bridge calls automatically the first time your iframe invokes getSessionToken(). The host mints an HS256-signed JWT carrying sub (the signed-in user's id), app_id, tenant_id, the granted permissions[], and iat/exp. TTL is APP_BRIDGE_TOKEN_TTL (default 3600 seconds). The bridge auto-refreshes the cached token when fewer than APP_BRIDGE_TOKEN_REFRESH_THRESHOLD seconds (default 300) remain — your app code doesn't manage the lifecycle.
One scope vocabulary
Both tokens are gated against the same SMART scope set. When the host's bridge message-handler receives an iframe action like getCurrentPatient, setFormField, fetchData, or updateData, it checks the corresponding SMART scope on the bridge JWT — user/Patient.read, user/DocumentReference.write, user/Encounter.read, fhirUser, etc. — and rejects with Permission denied: <scope> if the app doesn't have it. PHI-bearing context broadcasts (patient:opened, encounter:status-changed, contextUpdate) are filtered or redacted for any iframe missing user/Patient.read. There is no second permission catalog — the host previously had an 11-string bridge vocabulary (read:patients, write:documents, …) but those now exist only as legacy aliases for SMART scopes (see AppPermissionRegistry::LEGACY_ALIASES) and remain only to keep the deprecated GraphQL @appPermission directive working until that surface is removed.
Practical takeaway
| You want to… | Token | How you get it |
|---|---|---|
| Read or write FHIR resources | SMART OAuth2 bearer | Standard SMART launch; refresh on 401 |
| Toast, navigate, read context, fill a form field | App Bridge JWT | bridge.getSessionToken() — automatic |
You declare a single scope[] array in orion-app.json. The host applies that array to both tokens at install time. You don't get a separate "bridge permissions" knob in the manifest, and the CLI doesn't surface one — see Manifest reference for the field.
What the CLI does
Your entry point as an app developer. The CLI scaffolds projects, runs the local dev loop, validates the manifest, publishes bundles, and reads back marketplace state.
The CLI is a development tool — it's not part of what ships. After orion publish, your machine is out of the picture; the host EMR pulls the bundle from the marketplace.
Lifecycle
orion init— scaffold a new app project withorion-app.jsonand a starter component.orion dev— run a local Vite server, open a Cloudflare tunnel, and live-reload your code into a sandbox tenant.orion validate— check your manifest, scopes, and bundle against marketplace requirements before submitting.orion publish— upload the bundle from your home tenant to the marketplace for review.- An Orion reviewer approves the submission, and your bundle goes live in the marketplace.
- Tenants install it from their admin console, and users see your app at its declared extension points.
Where things run
- Your machine — CLI, Vite dev server, Cloudflare tunnel. Used during development and at publish time. Not used in production.
- Sandbox tenant — a scratch Orion instance you point
orion devat. Loads your local bundle through the tunnel for live-reload. - Home tenant — your production-like tenant. Where your developer account lives and where
orion publishuploads from. - Marketplace — hosts approved bundles and serves them over CDN to any tenant that installs the app.
- Each consuming tenant — a separate Orion instance running for an end customer. Loads your bundle into iframes when users navigate to your app's mount points. You don't see these tenants directly; the marketplace fans your bundle out to all of them.
Where to go next
- Tutorial — build and publish your first app end-to-end.
- Setup commands —
initand theconfigfamily. - Manifest reference — every field in
orion-app.json. - Host vs sandbox — when to use which tenant, and why.
- Glossary — terms used across these docs.