Skip to content

Embed F8 Studio

There are two ways to put F8 Studio in front of your users. The first needs no code at all: deploy the standalone container at its own origin and link to it, with a runtime config.js pointing it at the right instance. The second is this page: your application (a host portal, an internal tool, an admin console) renders Studio inside its own shell - its routing, its auth, its chrome - through a library artifact and one config object. Everything here is opt-in: every StudioConfig field has a default that reproduces the standalone app exactly. (This page is the embed CONTRACT; the staged journey - an in-browser WASM engine, the canvas over it, then the full Studio - is walked end to end in Embed scenarios.)

The library build lives in the fallen-8-web-ui package:

npm run build:lib # in fallen-8-web-ui/

It produces dist-lib/: one ES module (the export surface of src/embed/index.ts), one stylesheet, and TypeScript declarations, wired through the package’s exports map. The package declares react and react-dom (19+) as peer dependencies: your application brings its own React and bundles the artifact like any dependency (a bundler is required; the module is not served raw). Consume it as a file:/workspace dependency or as a packed tarball (npm pack after build:lib); the package is deliberately not published to a registry.

Two imports, one call:

import { mountStudio } from "fallen-8-web-ui";
import "fallen-8-web-ui/styles.css"; // the stylesheet is NOT injected by the module
const studio = mountStudio(document.getElementById("studio")!, {
instances: [{
id: "tenant-graph",
name: "Tenant graph",
baseUrl: "https://f8.example.internal",
auth: { kind: "bearer", getToken: () => myAuth.freshToken() },
}],
lockInstances: true,
namespace: "default",
lockNamespace: true,
history: "memory",
storageNamespace: "tenant-42.",
nlAssist: "instance-only",
});
// later: studio.unmount()

React hosts render <F8Studio config={...} /> instead; same contract, no imperative handle.

Every field is optional; omitting all of them is exactly the standalone app.

Field What it does Default
instances The instances Studio offers, supplied by the host. Host-supplied instances are managed: never persisted, re-created on every mount the same-origin instance
activeInstanceId Which instance starts active the first
lockInstances Hides register/edit/remove and the activation radios; the shell shows a static label false
namespace Seeds the active namespace when nothing is remembered for the instance default
lockNamespace Hides the namespace switcher and management; forces the pin over a remembered choice false
basepath Router prefix when Studio lives under a host route ""
history "memory" keeps Studio’s navigation out of the host’s address bar "browser"
storageNamespace Prefix for every localStorage key, so embeds and the standalone app never share state ""
theme Token overrides (surfaces, accents, the mono font stack); anything omitted keeps Studio’s dark defaults Studio’s palette
queryClient Reuse the host’s TanStack QueryClient. Source-level embedding only: the packaged artifact bundles its own @tanstack/react-query copy, so a host client from another copy only half-works (focus/online managers diverge) - leave it unset when consuming the artifact Studio’s own
nlAssist "disabled" removes the NL-assist panels; "instance-only" locks model calls to the instance’s POST /chat. Enforced structurally: the browser-direct transports refuse under any policy, and the NL store is policy-resolved at rehydrate, so an instance-only embed neither holds nor re-persists a custom endpoint config or its third-party key standalone behavior

One live mount per page. A second simultaneous mount fails loudly (the second tree’s mount errors rather than silently rebinding the first to its config): two embeds would share one instance registry and one set of persisted keys. Reconfiguring means unmount, then mount with the new config - each mount starts from storage plus config alone. The config is read once per mount, so swapping the config prop of a live <F8Studio> in place does nothing, and remounting it with a different config inside the same React commit is unsupported; unmount, let React commit the removal, then mount.

The bearer auth arm is how a host hands Studio a per-user, per-instance credential without ever exposing a long-lived secret:

auth: { kind: "bearer", getToken: () => Promise<string> }

The token is resolved per request (the change-feed stream included), refresh stays the host’s job, and a rejecting provider fails the request rather than retrying forever. Bearer instances are never persisted, because a callback cannot be. The standalone arms (none, apiKey) are unchanged.

The embed calls the REST API cross-origin exactly like the standalone container, so the data plane’s AllowedCorsOrigins must include the host’s origin, and the usual security rules apply unchanged.

lockInstances and lockNamespace are UI affordances, not an authorization boundary. Fallen-8 authenticates per instance, not per namespace: a credential that reaches one namespace reaches them all over plain REST. Under browser history the namespace is in the URL and user-editable, so pair lockNamespace with history: "memory" if the pin should hold in the UI - and put anything stronger on the server.

Everything Studio styles lives under one .f8-studio scope root, and the library stylesheet ships with its reset scoped the same way, so the artifact neither styles the host page (no bare html, body or :root rule survives the build; a check fails the build otherwise) nor depends on the host loading a reset. theme overrides land as inline custom properties on the scope root and win over the stylesheet defaults.

One boundary to know: Studio’s styles sit in CSS cascade layers, and unlayered host CSS beats layered CSS regardless of specificity. Scoping stops Studio leaking out; it cannot stop an aggressive unlayered host reset (say, a global button { all: unset }) leaking in. Keep global resets layered, or away from the region that hosts the embed.

Hosts that want an interactive graph without all of Studio import the canvas as a component:

import { F8GraphCanvas } from "fallen-8-web-ui";
import "fallen-8-web-ui/styles.css";
<F8GraphCanvas
nodes={{ 1: { id: 1, label: "turbine" }, 2: { id: 2, label: "site" } }}
edges={{ 10: { id: 10, source: 1, target: 2, edgePropertyId: "locatedAt", label: null } }}
onSelect={(ref) => console.log(ref)}
theme={{ accent: "#e2001a" }}
/>

It renders in its own .f8-studio scope (no app shell required), takes the same style config the Studio canvas uses, and reports selections through onSelect. Size it via its parent; it fills what it is given.

  • A bundler is required. React is external; the module expects the host’s build to resolve peers and asset imports.
  • The Samples gallery reads its datasets from this repository’s public mirror in an embed (the host origin does not serve /samples); everything else talks only to the configured instance.
  • The code editor’s worker is inlined in the artifact, so no worker file needs hosting and no extra CSP entry for a worker URL is needed beyond blob: workers.
  • Verified end to end in CI: a bare host application consumes the built package (exports map, peer resolution, scoped styles, editor, canvas, unmount) on every push.

How the embed fits the topology is on the architecture page; the Studio feature set itself is documented at F8 Studio and applies unchanged inside an embed.