Skip to content

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/).

ts
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

PathServes
/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.jsthe ui.embedded bootstrap (404 without one)
__connection.jsonconnection meta for the shared RPC socket
__wsthe WebSocket upgrade route (shared-server and Bun tiers)
__index.jsonthe machine-readable index: frames, endpoints
__client-imports.jsthe dock client-script import map for external viewers
__mcpthe 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:

ts
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 seesSingular (/__git/)Hub (/__devframes/git/)
Runtime base/__git//__devframes/git/ (transparent to the SPA)
__connection.jsonown meta, own socketper-frame meta pointing at the shared hub socket
RPC registrythis frame's functionsmerged: all frames + hub built-ins, callable cross-frame
Shared stateown context's slotsall frames' slots + hub slots
Authown gate, own tokenthe single hub Auth
Hub subsystemsdocks, terminals, messages, commands; the frame is also an iframe dock
MCP<base>__mcp, this frame's toolsthe aggregate at hub level
Isolationhard (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:

ts
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).

Released under the MIT License.