Skip to content

Next ​

WARNING

Experimental. @devframes/next's API is still settling — expect changes before a stable release.

@devframes/next hosts devframes from a Next.js App Router app via a route handler: one fetch handler serves each SPA and its __connection.json via serveStaticHandler.

It splits into @devframes/next/single and @devframes/next/hub; the bare import throws. single gives withDevframe(), createDevframeNextHandler(), and a React client (@devframes/next/single/client).

Config ​

ts
import { withDevframe } from '@devframes/next/single'

export default withDevframe({
  // ...your own Next config
})

withDevframe sets skipTrailingSlashRedirect: true, keeping the rest.

Hosting a single devframe ​

createDevframeNextHandler(definition) serves the SPA and starts a side-car RPC/WS server:

ts
import { createDevframeNextHandler } from '@devframes/next/single'
import myDevframe from '@/devframe'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

const handler = createDevframeNextHandler(myDevframe)
export const GET = handler.fetch

close() stops the side-car; ready resolves when listening.

OptionDefaultDescription
basedef.basePath ?? '/__<id>/'SPA mount path.
hostdef.cli?.host ?? 'localhost'Side-car bind host.
portfrom def.cli?.portSide-car port.
flags—Passed to def.setup(ctx, { flags }).
authfalsetrue for the OTP gate, or a handler.
key@devframes/next:<id>:<base>globalThis memoization key.

Hosting a hub ​

@devframes/hub's initHub mounts every frame under <base><id>/ behind one handler (memoize on globalThis; see examples/hub-next):

ts
import { DEVFRAMES_HUB_BASE, initHub } from '@devframes/hub/initiate'

// Next route handlers can't accept WS upgrades, so the socket asks for a
// side-car of its own; the browser discovers it via `__connection.json`.
const hub = initHub({
  base: DEVFRAMES_HUB_BASE,
  devframes: [myDevframe],
  ws: { sidecar: true },
  auth: false,
})

export const { handler } = hub // mount on a `[[...path]]` route handler
ts
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

export async function GET(request: Request): Promise<Response> {
  return handler(request) // serves every mounted SPA + connection meta
}

React client ​

tsx
'use client'
import { RpcProvider } from '@devframes/next/single/client'

export function Providers({ children }: { children: React.ReactNode }) {
  return <RpcProvider baseURL="/__my-tool/">{children}</RpcProvider>
}

useRpc() returns the DevframeRpcClient (null while connecting), useRpcStatus() the live { status, error }; both throw outside <RpcProvider>.

tsx
'use client'
import { useRpc, useRpcStatus } from '@devframes/next/single/client'

export function Panel() {
  const rpc = useRpc()?.scope('my-tool:')
  const { status, error } = useRpcStatus()
  if (!rpc)
    return <p>{error ? `connection failed — ${error.message}` : 'connecting…'}</p>
  // rpc.rpc.call('get-payload'), rpc.sharedState, …
}

Runtime ​

Handlers calling fetch pin runtime = 'nodejs' (Node side-car).

Mounting a hub ​

@devframes/next/hub's nextDevframeHub() is a globalThis-memoized handle; createNextDevframeHub() is the builder. UI defaults to @devframes/hub-ui; ui swaps it, ui: false gives a headless hub via @devframes/next/hub/client's useDevframeHubClient().

ts
import { nextDevframeHub } from '@devframes/next/hub'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

const hub = nextDevframeHub({ devframes: [] })
export const GET = (req: Request) => hub.handler(req)
export const POST = (req: Request) => hub.handler(req)
export const DELETE = (req: Request) => hub.handler(req)

No native hub viewer here, so this scope stays quiet; createDevframeNextHost() is the low-level DevframeHost.

See also ​

Released under the MIT License.