The Standard Handler

initDevframe() turns a DevframeDefinition into a running devframe whose .handler (a Web Standard (request: Request) => Promise<Response>) carries everything a devframe serves (SPA, __connection.json discovery, RPC socket, auth gate, MCP route) under one mount base. Every other serving path (adapters,…

initDevframe() turns a DevframeDefinition into a running devframe whose .handler (a Web Standard (request: Request) => Promise<Response>) carries everything a devframe serves (SPA, __connection.json discovery, RPC socket, auth gate, MCP route) under one mount base. Every other serving path (adapters, framework kits, hub) is assembled from it. Mount it with a catch-all route.

import { initDevframe } from 'devframe/initiate'
import myDevframe from './my-tool'

const devtools = initDevframe(myDevframe, { base: '/__my-tool/' })
// devtools.base, devtools.handler, devtools.nodeMiddleware, devtools.attach,
// devtools.handleUpgrade, devtools.ready, devtools.context,
// devtools.connectionMeta(), devtools.close()

base is required: pass resolveBasePath(def, 'hosted') (def.basePath ?? /__<id>/) to default it, and the running devframe echoes it back as devtools.base. handler/nodeMiddleware await readiness internally. The running devframe binds no port, so the WebSocket binding is the host framework's call.

Mount the handler

// vite.config.ts
// connect-style middleware + Vite's own server for the socket
import { initDevframe } from 'devframe/initiate'
import { defineConfig } from 'vite'
import myDevframe from './my-tool'

export default defineConfig({
  plugins: [{
    name: 'my-tool',
    apply: 'serve',
    configureServer(server) {
      const devtools = initDevframe(myDevframe, {
        base: '/__my-tool/',
        server: server.httpServer ?? undefined,
      })
      server.middlewares.use(devtools.nodeMiddleware)
    },
  }],
})

Host frameworks with dev-time module reloading (Next, Nitro, SvelteKit) re-evaluate the calling module, so memoize the running devframe on globalThis to avoid leaking a socket per reload. @devframes/next's createDevframeNextHandler handles this.

The WebSocket binding

Fetch handlers only hand over Requests, so the host framework binds the RPC socket. The local binding resolves in this order:

  1. ws.port: a side-car server on that exact port.
  2. server: share the host framework's node:http server; the upgrade binds at <base>__ws. No extra ports.
  3. ws: { sidecar: true }: a side-car server on a free port, for host frameworks whose handlers never see upgrades (Next.js route handlers, Nitro, Rsbuild).
  4. The host framework's own upgrades. With none set, the socket waits: devtools.attach(server) routes a server's upgrade events (returning a detach fn); devtools.handleUpgrade(req, socket, head) completes a single one from a listener you own.

ws.url controls the advertisement instead, so the browser dials it verbatim. Alone, an external WebSocket server owns the transport and its auth (wire the running devframe's context via createContextRpcServer + a WS transport); alongside a local binding it overrides only the advertisement (the tunnel pattern).

__connection.json describes the active combination. Asking a configured running devframe to take over the host framework's upgrades reports DF0055 (a local binding owns the socket) or DF0056 (ws.url handed it off).

Auth

The running devframe gates by default. The interactive OTP handler wires automatically, printing its code/magic-link banner when an untrusted browser client asks for a code (rpc.requestAuthCode()); an already-authorized page triggers no print. The magic link's origin comes from the origin option, or is derived from a request whose own origin is loopback or exactly matches an allowedOrigins entry. A non-loopback deployment (behind a proxy, on a LAN, on a public host) sets origin explicitly so the magic link resolves to the intended address; a raw inbound Host header and forwarded headers are never trusted. Pass auth: false for single-user localhost, or a DevframeAuthHandler for a custom scheme.

Relation to the other adapters

createDevServer, devframeViteBridge (@devframes/vite), and @devframes/next are assembled from it internally. To host many devframes, use initHub.