---
title: "Next"
description: "@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."
canonical_url: "https://devfra.me/frameworks/next"
---
# Next

> @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.

> [!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`](https://devfra.me/adapters/dev).

It splits into `@devframes/next/single` and [`@devframes/next/hub`](#mounting-a-hub); the bare import throws. `single` gives **`withDevframe()`**, **`createDevframeNextHandler()`**, and a React RPC-client helper (`@devframes/next/single/client`).

## Config

```ts [next.config.mjs]
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 [app/__my-tool/[[...path\\]\\]/route.ts]
import { createDevframeNextHandler } from '@devframes/next/single'
import myDevframe from '@/my-tool'

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.

| Option           | Default                        | Description                                                                                                                                                                                                  |
| ---------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `base`           | `def.basePath ?? '/__<id>/'`   | SPA mount path.                                                                                                                                                                                              |
| `host`           | `def.cli?.host ?? 'localhost'` | Side-car bind host.                                                                                                                                                                                          |
| `port`           | from `def.cli?.port`           | Side-car port.                                                                                                                                                                                               |
| `flags`          | none                           | Passed to `def.setup(ctx, { flags })`.                                                                                                                                                                       |
| `auth`           | `false`                        | `true` for the OTP gate, or a handler.                                                                                                                                                                       |
| `mcp`            | `'auto'`                       | Expose the MCP route. `'auto'` mounts once agent tools exist; `true` forces the origin-only route on (trusts same-machine callers); `McpRouteOptions` can add an `authorization` identity check.             |
| `allowedOrigins` | loopback-only                  | Widen the side-car WS origin check for a remotely-accessed dev server (container / Codespace / tunnel): extra origins, a `WsOriginRegistry`, or `false` to disable (the auth gate stays the trust boundary). |
| `key`            | `@devframes/next:<id>:<base>`  | `globalThis` memoization key.                                                                                                                                                                                |

## Hosting a hub

[`@devframes/hub`](https://devfra.me/guide/hub)'s `initHub` mounts every devframe under `<base><id>/` behind one `handler` (memoize on `globalThis`; see `examples/custom-hub-next`):

```ts [devframe/host.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 [app/__devframes/[[...path\\]\\]/route.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 RPC client

```tsx [app/providers.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 [app/panel.tsx]
'use client'
import { useRpc, useRpcStatus } from '@devframes/next/single/client'

export function Panel() {
  const rpc = useRpc()?.scope('my-tool:')
  const { 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 [app/__devframes/[[...path\\]\\]/route.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)
```

The aggregate MCP route mounts by default once any mounted devframe exposes agent tools (the `'auto'` setting). Force it on with `mcp: true` (origin-only, trusting same-machine callers), off with `mcp: false`, or add `mcp: { authorization }` for an identity check when the app is reachable beyond localhost.

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

## See also

- [Vite](https://devfra.me/frameworks/vite)
- [Hub](https://devfra.me/guide/hub)
- [custom-hub-next](https://github.com/devframes/devframe/tree/main/examples/custom-hub-next)


## Sitemap

See the full [sitemap](https://devfra.me/sitemap.md) for all pages.
