Serve a Hub Anywhere
initHub() from @devframes/hub/initiate puts a whole multi-devframe devtools installation behind one web-standard handler: mount it on a single catch-all route and every frame, the shared RPC socket, the single auth gate, discovery, and the optional UI are live under one namespace (default /__devframes/).
import { createUi } from '@devframes/hub-ui'
import { initHub } from '@devframes/hub/initiate'
import { createInspectDevframe } from '@devframes/plugin-inspect'
import { createTerminalsDevframe } from '@devframes/plugin-terminals'
export const hub = initHub({
key: 'devtools',
devframes: [createInspectDevframe(), createTerminalsDevframe()],
ui: createUi(),
configure(ctx) {
ctx.commands.register({ id: 'app:hello', title: 'Hello', handler: () => 'hi' })
},
})Every mounted devframe runs its setup() against the shared hub context: one merged RPC registry (frames can call each other's functions), one shared-state store, one WebSocket transport, one Auth. The instance mirrors initDevframe's surface — handler, nodeMiddleware, websocket (Bun), ready, context, connectionMeta(), close() — and the same mount snippets apply with the base swapped to /__devframes/; see the initiate adapter.
The namespace
| Path | Serves |
|---|---|
/ | the ui.viewer SPA — or the index document when the hub runs headless |
<id>/ | each mounted devframe's SPA, with its own __connection.json pointing at the shared socket |
embedded.js | the ui.embedded bootstrap (404 without one) |
__connection.json | connection meta for the shared RPC socket |
__ws | the WebSocket upgrade route (shared-server and Bun tiers) |
__index.json | the machine-readable index: frames, endpoints |
__client-imports.js | the dock client-script import map for external viewers |
__mcp | the aggregate MCP endpoint over the whole tool registry (opt-in via mcp) |
Frame ids become URL segments, so they are validated: reserved names throw DF8000, and ids must be route-safe (DF8004).
The ui slot
The hub is headless — DevframeHubUi is pure data, and whoever fills it decides what a viewer looks like:
interface DevframeHubUi {
viewer?: { distDir: string } // a standalone SPA served at the namespace root
embedded?: { entry: string } // a prebuilt bootstrap served at <base>embedded.js
}@devframes/hub-ui's createUi() is the reference implementation: a standalone viewer plus the floating dock — one <script type="module" src="/__devframes/embedded.js"> tag in the host page and the dock mounts itself, always visible. A viewer product supplies a different object to the same slot and reuses all the infrastructure; visibility policy (keyboard summon, passive modes) belongs entirely to the entry's author.
One Auth for the hub
The hub has a single Auth: one gate at the one shared transport covers every frame, the hub built-ins, and the MCP route. Mounted frames have no gates of their own — trust established once (OTP exchange, magic link, or a pre-shared token) unlocks the namespace. The gate is on by default; auth: false opts a single-user localhost setup out.
Singular vs hub mounting
A devframe's SPA and RPC client code are byte-identical in both cases — that is devframe's portability promise. The differences are environmental:
| What the SPA / RPC client sees | Singular (/__git/) | Hub (/__devframes/git/) |
|---|---|---|
| Runtime base | /__git/ | /__devframes/git/ (transparent to the SPA) |
__connection.json | own meta, own socket | per-frame meta pointing at the shared hub socket |
| RPC registry | this frame's functions | merged: all frames + hub built-ins, callable cross-frame |
| Shared state | own context's slots | all frames' slots + hub slots |
| Auth | own gate, own token | the single hub Auth |
| Hub subsystems | — | docks, terminals, messages, commands; the frame is also an iframe dock |
| MCP | <base>__mcp, this frame's tools | the aggregate at hub level |
| Isolation | hard (own context, own transport) | cooperative (shared context — tools compose) |
Bring your own context
Hosts that assemble createHubContext + mountDevframe themselves (with their own DevframeHost serving the frames) pass the finished context instead of a devframes list:
const hub = initHub({ context: ctx })The instance then serves the hub-level endpoints and transport only; serve each frame's meta from hub.connectionMeta() yourself. The two reference examples — examples/vite-devframe-hub and examples/next-devframe-hub — use the declarative mode with their own hand-built viewer UIs, and examples/nitro-devframe-hub / examples/hono-devframe-hub show the minimal createUi() mounts (the Hono one on Node and Bun).