Dynamic Frontends

10 min read

Dynamic Frontends let a user generate a complete, custom web application from chat — and then run it safely inside the Unique AI platform. The user describes what they want; a Conduct harness skill scaffolds a full Next.js application from a template; an admin picks that bundle up and turns it into a Dynamic Frontend space. From then on, the generated app is served as an isolated, sandboxed application embedded in chat, talking back to the customer's own data through the Unique API and MCP servers.

It is the same philosophy as UniqueAI Conduct, applied to the frontend: arbitrary, AI-generated application code runs with full flexibility, but inside a hard isolation boundary — no ambient internet, no secrets, no ability to reach anything the customer did not explicitly allow, and no way to act as anyone other than the signed-in user.

This document captures what Dynamic Frontends are for (business view), the architecture, and — most importantly — why they are secure and how a generated app is prevented from escaping its box.


Why Dynamic Frontends?

UniqueAI Conduct can already produce deliverables — reports, spreadsheets, analyses. Dynamic Frontends close the last gap: instead of producing a static artefact, the agent can produce a living application that the whole organisation can open, click through, and interact with.

What it gives you

  • From prompt to product. A user describes a dashboard or a tool in chat, the harness generates a full Next.js app from a template + a generation skill, and the result is a real, deployable application — not a screenshot or a one-off chart.

  • Live, interactive, data-backed. Unlike a static report, a Dynamic Frontend reads and writes live data. It can pull from MCP servers (SharePoint, internal systems, your own MCP servers) and the public API, render it, and let users act on it — filter, drill down, submit, trigger workflows.

  • Dashboards today, generic apps tomorrow. The lead use case is dynamic dashboards built on MCP-server data, but the mechanism is general: any custom internal application a business wants — a request form, an approval queue, a data explorer — can be generated and hosted the same way.

  • Governed and isolated by construction. Each app runs in its own hardened, VM-isolated runtime with locked-down egress. The customer decides exactly which API endpoints and MCP servers the app may reach. Nothing else is reachable.

  • No deploy pipeline for the user. The user never touches Kubernetes, CI, or hosting. They generate, save, and an admin attaches the bundle to a space; the platform stands up everything needed to serve it.

The mental model: a generated app in a sealed box, wired to your data through one door

Think of a Dynamic Frontend as a small web application that the agent wrote for you, running in a sealed container that has exactly one door to the outside world — the Unique API — and that door is guarded. The app can be as rich as any web app, but it can only see the data the customer opened that door for, and it can only ever act as the user currently looking at it.


1. End-to-end flow

  1. Generate. In a Conduct space, a user invokes the build-dynamic-frontend skill. The harness scaffolds a complete Next.js application from a fixed template, customised to the user's request.

  2. Save. The generated application is packaged as a .zip bundle and saved into the Knowledge Base as content (a ZIP). It is identified by a contentId and a sha256 hash.

  3. Provision. An admin picks that bundle up and creates a Dynamic Frontend space from it (in the Admin app). The space is an Assistant with uiType = DYNAMIC_FRONTEND and no AI module — the entire runtime is delegated to the generated app.

  4. Instantiate. Behind the scenes the platform creates a ByocApp resource for the bundle. A dedicated controller reconciles it into a running, isolated Kubernetes workload that serves the app over HTTP.

  5. Run. When a user opens the space in chat, the generated app is loaded inside a sandboxed iframe, served from a dedicated per-tenant subdomain under a per-app path (/serve/<app>/). It talks to the Unique API through the two governed paths described in §3.

  6. Update. Pointing the space at a newer bundle (a new contentId / version) re-deploys the app; sharing and access are left untouched.


1.1 Bundle deployment flow

How a generated app's .zip bundle — identified in the Knowledge Base by its contentId and sha256 — becomes a running, served app. The populator Job materialises the bundle onto a shared volume (PVC); the serve Deployment is launched in parallel, but its copy-bundle init container blocks until the populator finishes, so the app only starts once the bundle is ready.

High-level flow

Diagram: Untitled Diagram-1780342403527

Detailed sequence — the populator runs first and the Deployment waits for it

Diagram: Untitled Diagram-1780342484900

2. Architecture

  • Generation (Conduct harness skill). A template-driven skill produces a self-contained Next.js bundle. Authoring happens inside the governed Conduct sandbox, so even the generation step never has ambient internet or secrets.

  • Bundle store (Knowledge Base). The .zip lives as Knowledge Base content, access-controlled like any other file. The runtime fetches it by contentId and verifies it against the expected sha256.

  • ByocApp runtime (BYOC = Bring Your Own Code). Each space maps to a ByocApp custom resource. A controller reconciles it into a Deployment, Service, HTTPRoute, NetworkPolicy / CiliumNetworkPolicy, and PodDisruptionBudget. node-chat is only in the management path (create / update / delete); it is never in the request path of the served app.

  • Dedicated subdomain. Generated apps for a tenant are served from a dedicated per-tenant subdomain (e.g. byoc.<tenant>.unique.app), deliberately separate from next.<tenant> and api.<tenant>, with each app under its own path (/serve/<app>/). This isolates generated-app browser state from the rest of the product. Sibling apps share that origin, so isolation between apps comes from per-app cookie scoping (name + Path) and the auth proxy's response-header policy, not from separate origins.

  • In-pod auth proxy. Every app pod runs a small trusted proxy in front of the bundle's Next.js (which listens only on loopback). The proxy authenticates the user, enforces access policy, pins the request Origin on writes, and rewrites response headers (CSP, COOP, cookie scoping) before the bundle is ever reached. The bundle cannot disable any of this — it runs in a separate process the bundle can't touch.

  • Embedding in chat. The chat shell loads the app in a sandboxed iframe (sandbox="allow-scripts allow-forms allow-same-origin") and polls the runtime status until the app's URL is ready (the “Preparing dashboard…” state).


3. How a generated app talks to Unique (the only two doors)

A Dynamic Frontend has no general-purpose network access. It can reach the Unique platform through exactly two governed channels, and nothing else.

3.1 Public API (locked-down, server-side egress)

The app's own server code can call the Unique public API (https://api.<tenant>.unique.app/...). All pod egress is funnelled through the egress gateway (sbx-gateway):

  • The pod has no ambient outbound network. A NetworkPolicy permits egress only to the gateway and cluster DNS — the app cannot open a raw TCP/TLS connection to the internet or to any other in-cluster service.

  • The gateway TLS-intercepts outbound calls and enforces a strict upstream allow-list. The app can only reach the destinations the customer has explicitly allowed. Everything else is dropped.

  • Identity is enforced via a hairpin through Kong: the user's Authorization bearer is forwarded, Kong re-validates the JWT against Zitadel and overwrites the identity headers from the verified claims. The app therefore acts strictly as the calling user and cannot impersonate another user or tenant.

3.2 Host bridge (iframe ↔ shell, for MCP calls)

For data that comes from MCP servers, the app talks to the chat shell over a postMessage host bridge rather than calling out itself:

  • The iframe posts a unique:callMcpTool message (MCP server id + tool name + args) to the parent window.

  • The chat shell validates the message origin and source, then forwards it to node-chat's mcpCallTool GraphQL mutation with the signed-in user's token, and posts the result back to the iframe.

  • The app never holds credentials for MCP servers and never connects to them directly — the shell brokers every call under the user's identity and access rights. If the user isn't connected to the required MCP server, the bridge surfaces a “Connect” prompt.

This is what makes “dashboards over MCP data” work: the generated app renders and interacts with MCP-backed data without ever being trusted with the connection itself.


4. Security model — why it cannot escape its box

Dynamic Frontends run untrusted, AI-generated code, so the isolation boundary is the whole point. Several independent layers each have to fail for an app to misbehave.

4.1 No ambient internet — “no SSL connection per se”

A generated app cannot open arbitrary outbound HTTPS/TLS connections. There is no ambient egress: the only outbound path is the egress gateway, and the gateway only forwards to an explicit allow-list. Even a bundle that ignores HTTP(S)_PROXY is stopped at the NetworkPolicy / Cilium layer (on Cilium, L7 DNS rules also prevent DNS-tunnel exfiltration). So the app has no general connection to the outside world — it can only reach the customer-approved Unique endpoints, and only through the guarded door.

4.2 Tenant isolation — enforcement points

  • L0 — Kong. Without a valid token, requests never reach the app's data plane; identity comes from the token, not the client.

  • L1 — HTTPRoute. The route matches on the app's path prefix (/serve/<app>/) on the dedicated host and rewrites it to /; its backendRef points at the currently active version's Service. (An earlier revision also matched x-company-id on the route, but Kong matches routes before its plugins run, so a plugin-stamped header cannot be used as a match key — tenant enforcement therefore lives in the in-pod auth proxy, below.)

  • L2 — in-pod auth proxy. Application-layer policy: the app is tied to a specific space/company, and the proxy verifies the caller's identity and company-id (BYOC_ACCESS_COMPANY_ID) plus USE access against node-chat on every inbound request (TTL-cached). The proxy — not the route — is the source of truth for authenticating /serve/* requests, reading the __Secure-byoc-session-<app> cookie set by POST /_launch.

4.3 The bundle cannot forge identity or reach other services

On the egress hairpin: the gateway strips every identity-shaped header (x-user-id, x-company-id, x-user-roles, x-service-id, cookies) and only whitelists Authorization for the public-API host; Kong then re-injects identity from the validated JWT. The bundle can only ever possess the calling user's own token. Direct connections to in-cluster services like node-chat are dropped by the NetworkPolicy. Net effect: an app can act as the current user against allow-listed endpoints — and nothing more.

4.4 No secrets in the app

The generated app never holds API keys or MCP credentials. Public-API calls reuse the user's forwarded JWT; MCP calls are brokered by the shell under the user's identity. There is nothing to steal inside the bundle.

4.5 Browser-origin hardening

Serving on a dedicated subdomain isolates generated-app browser state from the rest of the product. The in-pod auth proxy sets frame-ancestors to the allowed chat frontend origins (clickjacking / cross-app framing protection; 'none' when none configured), forces Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp and Cross-Origin-Resource-Policy: same-origin, a connect-src 'self' CSP (so the app's browser code can't fetch() an attacker host), strips Service-Worker-Allowed, and scopes session cookies per app (name + Path). The chat iframe is sandboxed and pins postMessage source/origin on the bridge.

4.6 Pod hardening

App pods run under VM-level isolation (kata-vm-isolation by default; the RuntimeClass is configurable per cluster), as non-root (65532), with allowPrivilegeEscalation=false, all capabilities dropped, a read-only root filesystem (writes go to emptyDir), and seccomp: RuntimeDefault.


5. Glossary

Term

Meaning

Dynamic Frontend

A generated web application (Next.js) run as an isolated, embedded app inside a Unique space.

Dynamic Frontend space

An Assistant with uiType = DYNAMIC_FRONTEND and no AI module; its runtime is the generated app.

build-dynamic-frontend skill

The Conduct harness skill that scaffolds a Next.js bundle from a template.

Bundle

The generated app packaged as a .zip, stored as Knowledge Base content, identified by contentId + sha256.

ByocApp

The Kubernetes custom resource that represents a deployed bundle; reconciled by the byoc-controller into the running workload.

Egress gateway (sbx-gateway)

The mandatory, TLS-intercepting, allow-listed egress path; the app's only route to the outside.

Auth proxy

The trusted in-pod process in front of the bundle that enforces auth, access policy, Origin pinning, and response-header hardening.

Host bridge

The postMessage channel between the app iframe and the chat shell used to broker MCP tool calls under the user's identity.

Hairpin

Routing the app's public-API call back out to the LoadBalancer and in through Kong, so the JWT is re-validated and identity headers are re-derived.

Last updated