Embedded UI lets external web applications display Unqork modules on their own pages. To use it, a host page adds a single <script> tag from the Unqork server. That script registers a JavaScript API on window.unqork, creates a custom HTML element to hold the module, and starts the appropriate runtime (Centauri or Vega) inside a Shadow DOM boundary that keeps the module isolated from the host page's styles and scripts.
This reference covers the system architecture, file responsibilities, initialization sequence, and internal class hierarchy for Embedded UI.
Design Principles
These principles shape every technical decision in Embedded UI. Understanding them helps you predict system behavior and choose the right embedding approach for your use case.
| Principle | Implementation |
|---|---|
| Isolation | Shadow DOM prevents CSS bleed; navigation guards prevent URL hijacking. |
| Runtime Transparency | Host developers call the same API regardless of which module runtime is used. |
| Lazy Loading | Runtime scripts are loaded only when the first module of that type is mounted, reducing initial page load. |
| Authentication Flexibility | Supports anonymous, SAML, OIDC, login form, and refer-string authentication methods. |
| Minimal Host Impact | Requires a single <script> tag; no build-tool integration is needed in the host page. |
Terminology
The following terms appear throughout this reference. Review these definitions before reading the technical sections below.
| Term | Meaning |
|---|---|
| Host Page | The external HTML page that embeds the Unqork module. |
| Centauri | Unqork's Angular-based runtime, used for v1.x modules. |
| Vega | Unqork's React-based runtime, used for v2.x modules. |
| Module | A single Unqork form or UI unit, identified by a MongoDB ObjectId. |
| Workflow | An ordered sequence of modules with navigation controls (Next, Previous, Save and Exit). |
| Shadow DOM | A browser API that encapsulates DOM and CSS inside a private boundary, preventing styles from leaking in or out. |
High-Level Component Diagram
This diagram displays the main components and how they connect at a high level. The host page loads a single script that sets up the runtime and manages all communication between the host and the embedded module.
graph TB
subgraph "Host Page (External)"
SCRIPT["<script src='embedded.js'>"]
API["window.unqork.runtimes.default"]
TARGET["<unqork-app> custom element"]
end
subgraph "embedded.js (Server-rendered Nunjucks)"
RUNNER["IndexRunner.execute()"]
UQR["UnqorkRuntime"]
CR["CentauriRuntime"]
VR["VegaRuntime"]
end
subgraph "Centauri Stack"
CEMBED["centauri-embed.js"]
LOADER["embed-loader.js"]
SPA["spa-embed.js"]
SHADOW["Shadow DOM"]
ANG["Angular bootstrap"]
end
subgraph "Vega Stack"
VAPI["runtimeApi.js"]
VMOUNT["Vega mountModule()"]
end
SCRIPT --> RUNNER
RUNNER --> UQR
UQR -->|"v1.x module"| CR
UQR -->|"v2.x module"| VR
CR --> CEMBED
CEMBED --> LOADER
LOADER --> SPA
SPA --> SHADOW
SHADOW --> ANG
VR --> VAPI
VAPI --> VMOUNT
API --> UQR
TARGET --> SHADOW
Request Flow — Centauri Module
This sequence diagram traces the full loading and mounting flow for a Centauri module, from the host page's initial script request to the module rendering inside the Shadow DOM.
sequenceDiagram
participant Host as Host Page
participant EmbJS as embedded.js
participant Server as Unqork Server
participant CE as centauri-embed.js
participant Shadow as Shadow DOM
Host->>Server: GET /embedded.js?runtimeId=default
Server-->>Host: Rendered index-js-express.njk (JS)
Host->>EmbJS: IndexRunner.execute()
Note over EmbJS: Creates UnqorkRuntime, sets window.unqork
Host->>EmbJS: runtime.initialize()
Host->>EmbJS: runtime.mountModule(moduleId, target)
EmbJS->>Server: GET /fbu/form/:moduleId (detect runtime)
Server-->>EmbJS: Module JSON (settings.runtimeVersion)
Note over EmbJS: runtimeVersion=1.x → Centauri
EmbJS->>Server: GET /centauri-embed.js
Server-->>EmbJS: spa-embed.js + embed-loader.js
Note over EmbJS: window.unqorkCentauri ready
EmbJS->>Server: GET /auth/me
Server-->>EmbJS: authUser object
EmbJS->>CE: unqorkCentauri.mount(tagName, moduleId, config)
CE->>Shadow: Create custom element + Shadow DOM
CE->>Shadow: Load CSS into shadow
CE->>Shadow: angular.bootstrap() inside shadow
CE->>Shadow: $state.go('embedDisplay', { formId })
Shadow->>Server: GET /fbu/roles, /fbu/form/{id}, /fbu/styles
Shadow-->>Host: Module rendered inside unqork-app element
Core File Map
The following tables list every file involved in Embedded UI, grouped by role. Use this map to find the source of a specific behavior or to identify which file to modify for a given change.
| File | Location | Purpose |
|---|---|---|
index-js-express.njk |
packages/unqork-server/views/ |
Server-side template that injects WINDOW_VARS (environment variables and feature flags), then includes scripts/embedded.js. |
embedded.js |
packages/unqork-server/views/scripts/ |
Entry-point script: defines UnqorkRuntime, CentauriRuntime, and VegaRuntime; registers all three on window.unqork. |
spa-embed.js |
packages/unqork-express/src/app/ |
Core Centauri embed engine: creates the Shadow DOM, bootstraps Angular, enforces navigation guards, manages the template cache, and configures router states. |
embed-loader.js |
packages/unqork-express/src/app/ |
Defines the UnqorkCentauriElement custom HTML element and wires up the window.unqorkCentauri API. |
embed-utils.js |
packages/unqork-express/src/app/ |
Shared helpers: isEmbeddedMode(), setEmbeddedMode(), and surfaceEmbedError(). |
index.route.js |
packages/unqork-express/src/app/ |
Defines Angular UI-Router states for embed mode: embedDisplay and embedWorkflow*. |
Workflow-Specific Files
| File | Purpose |
|---|---|
controllers/displayWorkflow.js |
The DisplayWorkflow controller handles workflow API calls, step navigation, and submission persistence. |
services/WorkflowResponseReceivedHandler.js |
Processes workflow API responses and handles redirects and navigation state changes. |
services/WorkflowUriResolver.js |
Resolves the URI for each workflow step. |
services/WorkflowPreviousStateResolver.js |
A UI-Router resolve function that captures the previous state for back-navigation support. |
Iframe Embed Files
| File | Location | Purpose |
|---|---|---|
EmbedDisplayController.js |
src/app/controllers/ |
Orchestrates the iframe-side embed: initializes security, messaging, API handlers, resize management, and event broadcasting. |
embed-security.js |
src/app/controllers/embed/ |
Enforces security: rate limiting, HMAC signing, input sanitization, and origin validation. |
embed-messaging.js |
src/app/controllers/embed/ |
Manages the postMessage protocol helpers and the initial handshake between host and iframe. |
embed-api-handlers.js |
src/app/controllers/embed/ |
Handles API requests from the host: get and set data, validation, and form submission. |
embed-resize.js |
src/app/controllers/embed/ |
Automatically adjusts the iframe height using ResizeObserver, MutationObserver, and a polling fallback. |
embed-events.js |
src/app/controllers/embed/ |
Watches for module events and broadcasts them to the host page. |
Test Files
Unit and Integration Tests
These files live in /src/app/__tests__/ and run with Jest.
| File | Type | Count | What it covers |
|---|---|---|---|
spa-embed.test.js |
Unit | ~30 | Angular mounting and unmounting, navigation guard URL interception, template cache decorator, UI-Router state definitions (embedDisplay, embedWorkflow*), navigateToModule() and navigateToWorkflow() routing, workflow priority when both workflowPath and moduleId are provided, Angular bootstrap error handling. |
embed-loader.test.js |
Unit | ~20 | window.unqorkCentauri API surface (mount(), destroy()), UnqorkCentauriElement custom element registration, config and module data binding (_config, _moduleId, _moduleData), workflow mounting with workflowPath and submissionId, destroy and remount lifecycle. |
embed.integration.test.js |
Integration | ~26 | Full mount flow from initialize() to Shadow DOM creation, runtime detection for Centauri (v1.x) and Vega (v2.x), fallback to Centauri on missing or failed version fetch, authentication state detection (401 handling), navigation guard event dispatch, workflow-always-Centauri enforcement, unqork-workflow-mounted event, error handling for network and 404 failures. |
Initialization and Boot Sequence
This section displays the step-by-step sequence that runs when Embedded UI loads. The flowchart covers everything from the initial script tag to the module mounting inside the host page.
flowchart TD
A["Host page loads script tag"] --> B["Server renders index-js-express.njk"]
B --> C["Injects: RUNTIME_VARS, __UQENV__, FEATURE_TOGGLES"]
C --> D["IndexRunner.execute() runs"]
D --> E["Creates UnqorkRuntime instance"]
E --> F["Registers on window.unqork.runtimes[runtimeId]"]
F --> G["Host calls runtime.initialize()"]
G --> H["Host calls runtime.mountModule()"]
H --> I{"Detect runtimeVersion"}
I -->|"1.x"| J["CentauriRuntime.initialize()"]
I -->|"2.x"| K["VegaRuntime.initialize()"]
J --> L["Load centauri-embed.js"]
K --> M["Load runtimeApi.js"]
L --> N["unqorkCentauri.mount()"]
M --> O["vegaApi.mountModule()"]
Server-Side Rendering
The Nunjucks template index-js-express.njk injects server-known values before the client-side JavaScript runs. The following variables configure the runtime to connect to the correct environment:
| Variable | Source | Example |
|---|---|---|
RUNTIME_VARS.runtimeId |
query param or 'default' |
'default' |
RUNTIME_VARS.hostUrl |
Computed from the incoming request | 'https://env.unqork.io' |
__UQENV__.REMOTE_ROOT |
Server configuration | 'https://d-env.unqork.io/' |
__UQENV__.CUSTOMER |
Server configuration | 'acme-corp' |
FEATURE_TOGGLES |
LaunchDarkly or environment configuration | { 'enable-unq-styles': true, ... } |
Class Hierarchy
window.unqork.runtimes.default → UnqorkRuntime
├── ._centauriRuntime → CentauriRuntime (created lazily)
│ └── ._centauriApi → window.unqorkCentauri (set by embed-loader.js)
└── ._vegaRuntime → VegaRuntime (created lazily)
└── ._runtimeApi → window.makeUnqorkApi() (Vega SDK)
Runtime Detection
When mountModule({ moduleId }) is called, UnqorkRuntime fetches the module definition from /fbu/form/{moduleId} and inspects settings.runtimeVersion to decide which runtime to use:
settings.runtimeVersion |
Selected Runtime |
|---|---|
"1.0.0" or starts with "1." |
Centauri |
"2.0.0" or starts with "2." |
Vega |
| Missing or undefined | Centauri (default) |
| API error or 404 | Centauri (default) |
Results are cached in UnqorkRuntime._moduleRuntimeCache (a Map), so repeat mounts of the same module skip the API call. The module definition fetched for detection is also passed to the internal runtime's mountModule(), avoiding a duplicate API request.
flowchart LR
A["mountModule(moduleId)"] --> B["fetch /fbu/form/:moduleId"]
B --> C{"runtimeVersion?"}
C -->|"1.x"| D["CentauriRuntime.mountModule(moduleId, moduleData)"]
C -->|"2.x"| E["VegaRuntime.mountModule(moduleId, moduleData)"]
C -->|"missing"| D
Centauri Embedded — Three-Layer Architecture
The Centauri embedded mode uses three JavaScript files, each with a distinct role. The table below displays how they stack and each layer's responsibilities.
| File | Responsibilities |
|---|---|
embed-loader.js |
Defines UnqorkCentauriElement (an HTMLElement subclass), sets window.unqorkCentauri, and manages the custom element lifecycle. |
spa-embed.js |
Core embed engine: bootstrap() sets up and bootstraps the Angular module; mount() runs the full mount flow (styles, bootstrap, state navigation); unmount() cleans up; enforces navigation guards and manages the template system. |
index.route.js |
Defines UI-Router states (embedDisplay, embedWorkflow*) and provides inline templates for workflow states. |
Angular States for Embed Mode
Embed mode registers its own set of UI-Router states inside the Shadow DOM so the embedded module can navigate between forms and workflow steps without modifying the host page's URL or routing tree. The table below lists each state, its URL pattern, the controller it uses, and how its template is provided.
| State Name | URL Pattern | Controller | Template |
|---|---|---|---|
home |
/ |
HomeController |
views/home.html (empty in embed) |
embedDisplay |
/embed/display/:formId/:submissionId?/:submissionFormId? |
EmbedDisplayController |
Inline: <display-form-main ng-controller="displayForm"> |
embedWorkflowStart |
/embed/workflow/:workflowPath |
DisplayWorkflow |
Inline workflow template |
embedWorkflowPath |
/embed/workflow/:workflowPath/:stepPath |
DisplayWorkflow |
Inline workflow template (with params) |
embedWorkflowStepSubmission |
/embed/workflow/:workflowPath/:stepPath/submission/:submissionId |
DisplayWorkflow |
Inline workflow template |
embedWorkflowSubmission |
/embed/workflow/:workflowPath/submission/:submissionId |
DisplayWorkflow |
Inline workflow template |
Note: Workflow states use inline
template(nottemplateUrl) because the HTTP interceptor prefixes relative URLs withhostUrl, which would cause$templateCachekey mismatches.
Angular Services Decorated in Embed Mode
In embed mode, Angular services are decorated and HTTP interceptors are registered to prevent the embedded module from interfering with the host page. The table below lists the key modifications and their purpose.
| Service | Modification | Purpose |
|---|---|---|
$browser |
url() no-op on SET |
Prevents Angular from changing the host page's URL when the embedded module navigates internally. |
$templateRequest |
Return from EMBED_TEMPLATES; surface errors on failure |
Prevents the module from making HTTP requests for Angular templates; surfaces template errors to the host instead. |
$exceptionHandler |
Delegate + surfaceError() |
Catches Angular runtime exceptions and forwards them to the host page as embed error events. |
$httpProvider (interceptor) |
Prefix relative URLs with hostUrl; surfaceError() on rejection |
Prefixes relative URLs with hostUrl to redirect all relative API calls to the Unqork server; surfaces HTTP errors to the host. |
Vega Embedded — Overview
The Vega embedded path is simpler because the Vega runtime API (runtimeApi.js) handles its own mounting and isolation. The host-facing API calls are identical to the Centauri path; the difference is internal.
flowchart LR
A["VegaRuntime.initialize()"] --> B["appendScript(runtimeApi.js)"]
B --> C["window.makeUnqorkApi(options)"]
C --> D["VegaRuntime._runtimeApi ready"]
D --> E["mountModule({ moduleId, target })"]
E --> F["runtimeApi.mountModule()"]
| Aspect | Centauri | Vega |
|---|---|---|
| Shadow DOM | Created by embed-loader.js |
Managed internally by runtimeApi |
| Angular | Bootstrapped manually by spa-embed.js |
Not used |
| Template system | Custom $templateCache with HTTP interception |
React virtual DOM |
| Navigation guards | $browser decorator suppresses URL changes |
Not needed (no URL routing) |
| Script loaded | centauri-embed.js |
runtimeApi.js (loaded as a module type) |
File Dependency Graph
This diagram traces how each file depends on or loads another file. Use it to understand the relationship between the entry point script and the runtime-specific code.
graph TD
NJK["index-js-express.njk (server)"] -->|renders| EMBJS["embedded.js"]
EMBJS -->|creates| UQR["UnqorkRuntime"]
UQR -->|creates| CR["CentauriRuntime"]
UQR -->|creates| VR["VegaRuntime"]
CR -->|loads| CEMBED["centauri-embed.js"]
CEMBED -->|bundles| LOADER["embed-loader.js"]
CEMBED -->|bundles| SPA["spa-embed.js"]
SPA -->|imports| UTILS["embed-utils.js"]
SPA -->|uses| ROUTES["index.route.js"]
ROUTES -->|references| DW["displayWorkflow.js"]
ROUTES -->|references| EDC["EmbedDisplayController.js"]
DW -->|uses| WRRH["WorkflowResponseReceivedHandler.js"]
VR -->|loads| VAPI["runtimeApi.js (external)"]
Changelog
| Date | Change |
|---|---|
| 2026-06-09 | Editorial pass: rewrote introduction for accessibility, removed HR dividers between sections, fixed heading em dashes (colon substitution), fixed British spelling (sanitisation → sanitization), added why-descriptions to all sections lacking prose intros, expanded table descriptions for non-experts, fixed "through" → "using" in Iframe Embed Files, added See Also section with links to published paths. |
| 2026-06-15 | Three-layer architecture table replaced with ASCII diagram; embedWorkflowSubmission URL pattern corrected; $httpProvider row updated; headings updated to em dash format. |
| 2026-04-21 | Initial publication. |