@orion-ehr/app-bridge
v0.1.2
Published
Embedded-app SDK for the Orion EHR App Bridge. Hybrid distribution: thin npm shim (types, hooks, loader) plus CDN-served runtime (OrionBridge class, transport, FHIR helpers).
Readme
@orion-ehr/app-bridge
Embedded-app SDK for the Orion EHR App Bridge. React hooks and a postMessage protocol client for iframe apps to communicate with the Orion host.
Status: pre-1.0. The API is not yet stable; pin exactly (
"@orion-ehr/app-bridge": "0.1.0", not"^0.1.0"). Phase 1 of the App Bridge Parity Initiative.
Hybrid distribution model (0.1.0+)
Starting in 0.1.0, the SDK ships in two pieces:
- npm shim (
@orion-ehr/app-bridge, ~5 KB) — types, React hooks,OrionProvider, and a thin loader. This is whatnpm installbrings in. Appsimport { useOrion, OrionProvider } from '@orion-ehr/app-bridge'and get full type inference. - CDN runtime (
https://cdn.orionsoftware.io/v0.1.0/app-bridge.js, ~25 KB) — theOrionBridgeclass, postMessage transport, and FHIR helpers. Loaded automatically by the shim's loader on firstOrionProvidermount.
Apps don't fetch the runtime themselves — OrionProvider injects a <script> tag pointing at the CDN URL pinned by the shim version. Tenant pages need https://cdn.orionsoftware.io in their CSP script-src; the host platform handles that.
The version pinned by the npm shim and the CDN URL it loads are coupled 1:1 — bumping the shim version requires deploying the matching runtime first. CI handles both in lockstep on push to main.
Install
npm install @orion-ehr/app-bridgePeer dependencies: React 18+ or 19+.
Usage
Wrap your entry point in <OrionProvider>:
import { OrionProvider } from '@orion-ehr/app-bridge';
import App from './App';
export default function bootstrap(ctx: OrionAppContext) {
return (
<OrionProvider appId={ctx.appId} initialContext={ctx}>
<App />
</OrionProvider>
);
}Then use the typed hooks anywhere inside the subtree:
import {
useTheme,
usePatient,
useEncounter,
useToast,
useNavigate,
useAutoResize,
useHostInfo,
} from '@orion-ehr/app-bridge';
function MyApp() {
const theme = useTheme();
const patient = usePatient();
const encounter = useEncounter();
const showToast = useToast();
const navigate = useNavigate();
const hostInfo = useHostInfo();
useAutoResize();
return /* ... */;
}Surfaces
| Hook | Purpose |
|---|---|
| useOrion() | Live bridge instance + full app context |
| useOrionContext() | Live OrionAppContext only (re-renders on context change) |
| useTheme() | Current ThemeMessage (mode + tokens) |
| useUser() | Currently-signed-in user |
| usePatient() | Currently-active patient (or null) |
| useEncounter() | Currently-active encounter (or null) |
| useTokenResponse() | SMART access token + launch context |
| useToast() | Stable showToast(message, type, duration?) callback |
| useNavigate() | Stable navigate(url, options?) callback |
| useHostInfo() | Host origin, tenant domain, protocol version (cached) |
| useAutoResize() | Wires a ResizeObserver to post content height to host |
| useBridge() | Direct OrionBridge instance for advanced uses |
Theming
The host pushes resolved CSS custom properties (--background, --primary, etc.) on every theme/mode change. <OrionProvider> applies them to the iframe document's :root automatically. Apps using @orion-ehr/ui's Tailwind classes (bg-background, text-primary, etc.) inherit the host's theme without any per-app theming code.
For light/dark mode: the provider toggles dark on documentElement to match the host's resolved mode. Tailwind dark: variants and the UI library's mode-scoped tokens both pick this up.
Architecture
OrionBridge is the postMessage transport (request/response correlation, event subscription, timeouts), bundled into the CDN runtime. <OrionProvider> (in the npm shim) awaits loadOrionAppBridge(), calls window.OrionAppBridge.createBridge({...}) to obtain a single bridge instance per iframe, holds the live OrionAppContext, applies tokens, and subscribes to host events that update context. Hooks read the React context and return slices of it.
Loader semantics:
- Single resolution per page. Multiple
OrionProviders share one promise, one script tag, one global. - Idempotent on success. Repeat
loadOrionAppBridge()calls return the cached promise / global. - Retryable on failure. Three attempts at 0/200/600ms within one call; rejection clears the cache so the next call retries fresh.
- SSR-safe.
loadOrionAppBridge()rejects synchronously whenwindowis undefined;OrionProviderreturnsnulluntil the first client-sideuseEffectruns.
Wire protocol matches the host's app-bridge/message-handler.ts. See docs/marketplace.md for protocol-level details.
Local dev
The Orion CLI's orion dev command serves the runtime locally so you don't need outbound CDN reachability:
orion dev --serve-runtime(defaulttrue) hosts the local runtime athttp://localhost:5174/_orion/app-bridge.js.- The CLI is wired to override
LoaderOptions.cdnUrlfor the running session. ORION_SDK_RUNTIME_URLenv var globally overrides the default CDN URL — useful for tests and ngrok experiments.
Errors
Bridge requests that fail throw BridgeRequestError with a code field:
| Code | Meaning |
|---|---|
| UNSUPPORTED_ACTION | Host doesn't recognize the action (older host, capability not granted) |
| PERMISSION_DENIED | Host recognized the action but rejected it (revoked install, missing scope) |
| INTERNAL | Host hit an unexpected error |
| TIMEOUT | SDK timed out waiting for a host response |
Branch on error.code rather than parsing error.message. Free-form host error strings are mapped to a code (UNSUPPORTED_ACTION for legacy "Unknown action" / "does not support" wording, INTERNAL otherwise) with a single console.warn recommending the host upgrade to structured codes — that fallback will be removed once the host wire-shape ships { code, message } consistently.
BridgeRequestError is re-exported from the npm shim. The class itself lives in the CDN runtime — the shim's export is a thin proxy that delegates instanceof and construction to the real runtime constructor on window.OrionAppBridge.BridgeRequestError. instanceof checks return false until the runtime has loaded, but inside an OrionProvider subtree the runtime is always present.
FHIR REST helpers
bridge.fhirFetch(path, init?) issues authenticated FHIR REST calls against the host-minted SMART token + tenant FHIR base URL. Convenience helpers: getResource, searchResources, createResource, updateResource, deleteResource.
Security guarantees:
- The resolved request URL must share an origin with
fhirBase. Absolute URLs and protocol-relative paths (//evil.example) are rejected before the request is sent. redirect: 'error'is forced on every request so a host-side 3xx can't pull the bearer off-origin.- Error messages do not include the response body by default, since FHIR
OperationOutcomepayloads routinely contain PHI. Passdebug: truetoOrionProvider(dev only) to opt into body inclusion. bridge.destroy()clearsfhirAccessToken/fhirBaseso the bearer doesn't survive teardown in heap snapshots.
Installation context
GET $get-installation-context is a vendor FHIR operation that returns a Parameters resource describing the calling install: appId, appSlug, installedAt, status, granted permissionsGranted scopes, a resolved extensionUrlMap (one nested part per declared writableExtensions key, with valueUri omitted when the tenant admin chose Skip at install), and featureFlags (boolean values only). The route requires the user/AppInstallation.read scope, which the host grants automatically to any installed app.
Apps call it to discover the per-tenant StructureDefinition URL each writable extension was resolved to at install time, so writes target the resolved URL rather than the manifest-declared key:
const ctx = await bridge.fhirFetch('$get-installation-context');
const extensionMap = ctx.parameter?.find((p) => p.name === 'extensionUrlMap');
const pharmacyUrl = extensionMap?.part?.find((p) => p.name === 'preferred-pharmacy')?.valueUri ?? null;Migration from 0.0.x
0.0.x is npm-only (single bundle). 0.1.0 introduces the hybrid model. Mechanical migration:
npm install @orion-ehr/app-bridge@^0.1.0(or pin exactly).OrionProviderprops are unchanged.useOrion,useTheme, etc. signatures unchanged.- One conceptual shift: a brief async window now exists between provider mount and bridge readiness while the runtime fetches. Apps that previously did
bridge.something()synchronously inuseEffectneed to either:- await
bridge.tokenReady(already exists post-Phase 1), OR - check
useOrion().bridge !== nullbefore calling.
- await
- Tenant CSP must allow
https://cdn.orionsoftware.ioinscript-src. Host platform handles this; apps don't configure CSP themselves.
License
MIT — see LICENSE.
