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, 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)
},
}],
})// routes/__my-tool/[...path].ts
// routes/__my-tool/index.ts
// for the namespace root, since a catch-all doesn't match its own empty path.
import { defineHandler } from 'nitro'
import { devtools } from '../../devtools'
export default defineHandler(event => devtools.handler(event.req))// server.ts
// `serve()` hands back the node server the socket rides on
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { devtools } from './devtools'
const app = new Hono()
app.all('/__my-tool/*', c => devtools.handler(c.req.raw))
devtools.attach(serve({ fetch: app.fetch, port: 3000 }))// app/%5F_my-tool/[[...path]]/route.ts
// Next reserves `_`-prefixed folders, so the segment is URL-encoded (`%5F_` decodes to `__`).
import { initDevframe } from 'devframe/initiate'
import myDevframe from '@/my-tool'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
// Route handlers never see upgrades, so the socket asks for a side-car; the
// globalThis memo keeps a dev-time reload from starting a second one.
const g = globalThis as { devtools?: ReturnType<typeof initDevframe> }
const devtools = g.devtools ??= initDevframe(myDevframe, {
base: '/__my-tool/',
ws: { sidecar: true },
})
export const GET = devtools.handler// server/middleware/devtools.ts
import { devtools } from '../devtools'
export default defineEventHandler((event) => {
const { pathname } = new URL(toWebRequest(event).url)
// `devtools.base` is the normalized mount base; no repeated string.
if (pathname.startsWith(devtools.base) || pathname === devtools.base.slice(0, -1))
return devtools.handler(toWebRequest(event))
})// src/routes/%5F_my-tool/[...path]/+server.ts
import myDevframe from '$lib/my-tool'
import { initDevframe } from 'devframe/initiate'
const g = globalThis as { devtools?: ReturnType<typeof initDevframe> }
const devtools = g.devtools ??= initDevframe(myDevframe, {
base: '/__my-tool/',
ws: { sidecar: true },
})
export const GET = ({ request }) => devtools.handler(request)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:
ws.port: a side-car server on that exact port.server: share the host framework'snode:httpserver; the upgrade binds at<base>__ws. No extra ports.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).- The host framework's own upgrades. With none set, the socket waits:
devtools.attach(server)routes a server'supgradeevents (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.
Adapters
The lowest-level path is the standard handler, initDevframe(def, { base }): a Web Standard (request: Request) => Promise<Response> for any catch-all route. Every path below builds on it.
CLI (cac)
A cac CLI around a DevframeDefinition with dev, build, and mcp commands. cac is an optional peer: