Documentation Index

Fetch the complete documentation index at: https://docs.unqork.io/llms.txt

Use this file to discover all available pages before exploring further.

Embedded UI Architecture Reference

Prev Next

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["&lt;script src='embedded.js'&gt;"]
        API["window.unqork.runtimes.default"]
        TARGET["&lt;unqork-app&gt; 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 (not templateUrl) because the HTTP interceptor prefixes relative URLs with hostUrl, which would cause $templateCache key 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.