showcase/shell-docs/src/content/docs/runtime-server-adapter.mdx
The CopilotKit runtime is framework-agnostic. At its core, it's a pure Fetch handler — a function that takes a Request and returns a Response. This means it runs natively on any platform that supports the Fetch API, and thin adapters are provided for Node.js-based frameworks like Express and Hono.
| Runtime | Import Path | Key Function |
|---|---|---|
| Fetch-native (Bun, Deno, CF Workers, Next.js App Router) | @copilotkit/runtime/v2 | createCopilotRuntimeHandler |
| Node.js HTTP | @copilotkit/runtime/v2/node | createCopilotNodeHandler / createCopilotNodeListener |
| Express | @copilotkit/runtime/v2/express | createCopilotExpressHandler |
| Hono | @copilotkit/runtime/v2/hono | createCopilotHonoHandler |
The runtime supports two endpoint modes:
Multi-route (default) — exposes individual URL endpoints:
GET {basePath}/infoPOST {basePath}/agent/:agentId/runPOST {basePath}/agent/:agentId/connectPOST {basePath}/agent/:agentId/stop/:threadIdPOST {basePath}/transcribeSingle-route — a single POST {basePath} endpoint that accepts a JSON envelope:
{ "method": "agent/run", "params": { "agentId": "default" }, "body": { ... } }
Single-route mode is useful when your platform only allows a single route handler (e.g. a single serverless function), or when you prefer a simpler URL structure.
If your platform supports the Fetch API natively (Bun, Deno, Cloudflare Workers, etc.), you can use createCopilotRuntimeHandler directly — no adapter needed.
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
// 1. Create the runtime with your agent(s)
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
// 2. Create a Fetch handler — takes Request, returns Response
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
});
// 3. Serve it (Bun example — works the same with Deno.serve, CF Workers, etc.)
Bun.serve({ fetch: handler, port: 4000 });
For framework-specific examples, see the sections below.
<Tabs groupId="runtime-mode" items={["Multi-route", "Single-route", "Existing server"]}> <Tab value="Multi-route">
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
// Configure the runtime with your agent(s)
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
// Create a Fetch handler with multi-route endpoints (GET /info, POST /agent/:id/run, etc.)
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
});
// Bun natively supports Fetch handlers
Bun.serve({ fetch: handler, port: 4000 });
const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }), }, });
// Single-route mode: one POST endpoint that dispatches via JSON envelope const handler = createCopilotRuntimeHandler({ runtime, basePath: "/api/copilotkit", mode: "single-route", cors: true, });
Bun.serve({ fetch: handler, port: 4000 });
</Tab>
<Tab value="Existing server">
```ts title="server.ts"
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
const copilotHandler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
});
Bun.serve({
port: 4000,
fetch(request) {
const url = new URL(request.url);
// Route CopilotKit requests to the handler
if (url.pathname.startsWith("/api/copilotkit")) {
return copilotHandler(request);
}
// Your other routes
if (url.pathname === "/health") {
return Response.json({ status: "healthy" });
}
return Response.json({ status: "ok" });
},
});
<Tabs groupId="runtime-mode" items={["Multi-route", "Single-route", "Existing server"]}> <Tab value="Multi-route">
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
});
// Deno.serve natively accepts a Fetch handler
Deno.serve({ port: 4000 }, handler);
const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }), }, });
// Single-route mode: one POST endpoint that dispatches via JSON envelope const handler = createCopilotRuntimeHandler({ runtime, basePath: "/api/copilotkit", mode: "single-route", cors: true, });
Deno.serve({ port: 4000 }, handler);
</Tab>
<Tab value="Existing server">
```ts title="server.ts"
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
const copilotHandler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
});
Deno.serve({ port: 4000 }, (request) => {
const url = new URL(request.url);
// Route CopilotKit requests to the handler
if (url.pathname.startsWith("/api/copilotkit")) {
return copilotHandler(request);
}
// Your other routes
if (url.pathname === "/health") {
return Response.json({ status: "healthy" });
}
return Response.json({ status: "ok" });
});
<Tabs groupId="runtime-mode" items={["Multi-route", "Single-route", "Existing worker"]}> <Tab value="Multi-route">
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
export interface Env {
OPENAI_API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
// Workers use the Fetch API natively — handler plugs in directly
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
});
return handler(request);
},
};
export interface Env { OPENAI_API_KEY: string; }
export default { async fetch(request: Request, env: Env): Promise<Response> { const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }), }, });
// Single-route: one POST endpoint with JSON envelope dispatch
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
mode: "single-route",
cors: true,
});
return handler(request);
}, };
</Tab>
<Tab value="Existing worker">
```ts title="src/index.ts"
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
export interface Env {
OPENAI_API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Route CopilotKit requests to the handler
if (url.pathname.startsWith("/api/copilotkit")) {
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
});
return handler(request);
}
// Your other routes
if (url.pathname === "/health") {
return Response.json({ status: "healthy" });
}
return Response.json({ status: "ok" });
},
};
<Tabs groupId="runtime-mode" items={["Multi-route", "Single-route"]}> <Tab value="Multi-route">
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
});
// Export as both GET and POST to handle all CopilotKit routes
export { handler as GET, handler as POST };
const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }), }, });
// Single-route: only POST needed, no catch-all [...path] required const handler = createCopilotRuntimeHandler({ runtime, basePath: "/api/copilotkit", mode: "single-route", });
export { handler as POST };
<Callout type="info">
In single-route mode, no `[...path]` catch-all is needed — everything goes through a single `POST /api/copilotkit`.
</Callout>
</Tab>
</Tabs>
<Callout type="info">
No `cors: true` needed for Next.js — same-origin requests don't require CORS headers.
</Callout>
## React Router (Framework Mode)
React Router v7 in framework mode uses file-based routing with Fetch API handlers — the CopilotKit handler works directly as a resource route.
<Tabs groupId="runtime-mode" items={["Multi-route", "Single-route"]}>
<Tab value="Multi-route">
```ts title="app/routes/api.copilotkit.$.ts"
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
import type { Route } from "./+types/api.copilotkit.$";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
});
// loader handles GET requests (e.g. /info)
export function loader({ request }: Route.LoaderArgs) {
return handler(request);
}
// action handles POST requests (e.g. /agent/:id/run)
export function action({ request }: Route.ActionArgs) {
return handler(request);
}
const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }), }, });
// Single-route: only action (POST) needed, no splat segment required const handler = createCopilotRuntimeHandler({ runtime, basePath: "/api/copilotkit", mode: "single-route", });
export function action({ request }: Route.ActionArgs) { return handler(request); }
<Callout type="info">
In single-route mode, no `$` splat is needed — the filename is just `api.copilotkit.ts` and all dispatch happens via the JSON envelope.
</Callout>
</Tab>
</Tabs>
## TanStack Start
TanStack Start uses API routes that receive standard `Request` objects and return `Response` objects.
<Tabs groupId="runtime-mode" items={["Multi-route", "Single-route"]}>
<Tab value="Multi-route">
```ts title="app/routes/api/copilotkit/$.ts"
import { createAPIFileRoute } from "@tanstack/react-start/api";
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
});
// The $ splat matches all subpaths — GET for /info, POST for /agent/:id/run, etc.
export const APIRoute = createAPIFileRoute("/api/copilotkit/$")({
GET: ({ request }) => handler(request),
POST: ({ request }) => handler(request),
});
const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }), }, });
// Single-route: only POST needed, no $ splat required const handler = createCopilotRuntimeHandler({ runtime, basePath: "/api/copilotkit", mode: "single-route", });
export const APIRoute = createAPIFileRoute("/api/copilotkit")({ POST: ({ request }) => handler(request), });
<Callout type="info">
In single-route mode, no `$` splat is needed — the file is `copilotkit.ts` (not `$.ts`) and only `POST` is required.
</Callout>
</Tab>
</Tabs>
## Node.js HTTP
For vanilla Node.js HTTP servers, use the `/node` subpath which bridges Fetch to Node's `IncomingMessage`/`ServerResponse`.
<Tabs groupId="runtime-mode" items={["Multi-route", "Single-route", "Existing server"]}>
<Tab value="Multi-route">
```ts title="server.ts"
import { createServer } from "node:http";
import { CopilotRuntime, BuiltInAgent } from "@copilotkit/runtime/v2";
import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
// createCopilotNodeListener returns a Node request listener (req, res) => void
// that bridges to the Fetch handler internally
const listener = createCopilotNodeListener({
runtime,
basePath: "/api/copilotkit",
cors: true,
});
createServer(listener).listen(4000, () => {
console.log("Listening at http://localhost:4000/api/copilotkit");
});
const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }), }, });
const listener = createCopilotNodeListener({ runtime, basePath: "/api/copilotkit", mode: "single-route", cors: true, });
createServer(listener).listen(4000, () => { console.log("Listening at http://localhost:4000/api/copilotkit"); });
</Tab>
<Tab value="Existing server">
```ts title="server.ts"
import { createServer } from "node:http";
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
import { createCopilotNodeHandler } from "@copilotkit/runtime/v2/node";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
// 1. Create the Fetch handler
const copilotHandler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
});
// 2. Wrap it as a Node (req, res) handler
const copilotNodeHandler = createCopilotNodeHandler(copilotHandler);
// 3. Use it in your existing server with manual routing
const server = createServer(async (req, res) => {
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
// Route CopilotKit requests to the handler
if (url.pathname.startsWith("/api/copilotkit")) {
return copilotNodeHandler(req, res);
}
// Your other routes
if (url.pathname === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "healthy" }));
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
});
server.listen(4000, () => {
console.log("Listening at http://localhost:4000");
});
The Express adapter returns an Express Router that you mount with app.use().
npm install express cors
<Tabs groupId="runtime-mode" items={["Multi-route", "Single-route", "Existing server"]}> <Tab value="Multi-route">
import express from "express";
import { CopilotRuntime, BuiltInAgent } from "@copilotkit/runtime/v2";
import { createCopilotExpressHandler } from "@copilotkit/runtime/v2/express";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
const app = express();
// Mount the CopilotKit router — creates Express routes under /api/copilotkit
app.use(
createCopilotExpressHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
}),
);
app.listen(4000, () => {
console.log("Listening at http://localhost:4000/api/copilotkit");
});
const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }), }, });
const app = express();
// Single-route: one POST endpoint that dispatches via JSON envelope app.use( createCopilotExpressHandler({ runtime, basePath: "/api/copilotkit", mode: "single-route", cors: true, }), );
app.listen(4000, () => { console.log("Listening at http://localhost:4000/api/copilotkit"); });
</Tab>
<Tab value="Existing server">
```ts title="server.ts"
import express from "express";
import { CopilotRuntime, BuiltInAgent } from "@copilotkit/runtime/v2";
import { createCopilotExpressHandler } from "@copilotkit/runtime/v2/express";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
const app = express();
// Your existing routes
app.get("/", (req, res) => {
res.json({ status: "ok" });
});
app.get("/health", (req, res) => {
res.json({ status: "healthy" });
});
// Mount CopilotKit alongside your existing routes
app.use(
createCopilotExpressHandler({
runtime,
basePath: "/api/copilotkit",
cors: true,
}),
);
app.listen(4000, () => {
console.log("Listening at http://localhost:4000");
});
| Option | Type | Default | Description |
|---|---|---|---|
runtime | CopilotRuntime | required | The runtime instance |
basePath | string | required | URL path prefix (e.g. "/api/copilotkit") |
mode | "multi-route" | "single-route" | "multi-route" | Multi-route exposes individual endpoints; single-route uses a JSON envelope |
cors | boolean | CorsOptions | false | CORS configuration. true for permissive defaults, or pass a cors options object |
hooks | CopilotRuntimeHooks | — | Lifecycle hooks |
The Hono adapter returns a Hono app that you mount with app.route().
npm install hono
<Tabs groupId="runtime-mode" items={["Multi-route", "Single-route", "Existing server"]}> <Tab value="Multi-route">
import { serve } from "@hono/node-server";
import { CopilotRuntime, BuiltInAgent } from "@copilotkit/runtime/v2";
import { createCopilotHonoHandler } from "@copilotkit/runtime/v2/hono";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
// Returns a Hono app with CopilotKit routes mounted at basePath
const endpoint = createCopilotHonoHandler({
runtime,
basePath: "/api/copilotkit",
});
// Serve the Hono app directly
serve({ fetch: endpoint.fetch, port: 4000 }, () => {
console.log("Listening at http://localhost:4000/api/copilotkit");
});
const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }), }, });
// Single-route: one POST endpoint that dispatches via JSON envelope const endpoint = createCopilotHonoHandler({ runtime, basePath: "/api/copilotkit", mode: "single-route", });
serve({ fetch: endpoint.fetch, port: 4000 }, () => { console.log("Listening at http://localhost:4000/api/copilotkit"); });
</Tab>
<Tab value="Existing server">
```ts title="server.ts"
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { CopilotRuntime, BuiltInAgent } from "@copilotkit/runtime/v2";
import { createCopilotHonoHandler } from "@copilotkit/runtime/v2/hono";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
const app = new Hono();
// Your existing routes
app.get("/", (c) => c.json({ status: "ok" }));
app.get("/health", (c) => c.json({ status: "healthy" }));
// Mount CopilotKit as a sub-app via app.route()
app.route("/", createCopilotHonoHandler({ runtime, basePath: "/api/copilotkit" }));
serve({ fetch: app.fetch, port: 4000 }, () => {
console.log("Listening at http://localhost:4000");
});
Elysia runs on Bun and supports the Fetch API natively, so you use createCopilotRuntimeHandler directly.
bun add elysia
<Tabs groupId="runtime-mode" items={["Multi-route", "Single-route", "Existing server"]}> <Tab value="Multi-route">
import { Elysia } from "elysia";
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
// Create a Fetch handler — Elysia passes the raw Request through
const handler = createCopilotRuntimeHandler({ runtime, cors: true });
new Elysia()
.all("/api/copilotkit/*", ({ request }) => handler(request))
.listen(4000);
const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }), }, });
// Single-route: one POST endpoint, no wildcard needed const handler = createCopilotRuntimeHandler({ runtime, basePath: "/api/copilotkit", mode: "single-route", cors: true, });
new Elysia() .post("/api/copilotkit", ({ request }) => handler(request)) .listen(4000);
</Tab>
<Tab value="Existing server">
```ts title="server.ts"
import { Elysia } from "elysia";
import { CopilotRuntime, createCopilotRuntimeHandler, BuiltInAgent } from "@copilotkit/runtime/v2";
const runtime = new CopilotRuntime({
agents: {
default: new BuiltInAgent({ model: "openai/gpt-4o-mini" }),
},
});
const handler = createCopilotRuntimeHandler({ runtime, cors: true });
new Elysia()
// Your existing routes
.get("/", () => ({ status: "ok" }))
.get("/health", () => ({ status: "healthy" }))
// Mount CopilotKit
.all("/api/copilotkit/*", ({ request }) => handler(request))
.listen(4000);
All adapters support lifecycle hooks for cross-cutting concerns like authentication, logging, and response modification.
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
hooks: {
// Before routing — auth, correlation IDs
onRequest: async ({ request, path, runtime }) => {
const token = request.headers.get("authorization");
if (!token) {
throw new Response("Unauthorized", { status: 401 });
}
},
// After routing — route-specific authorization
onBeforeHandler: async ({ request, route }) => {
console.log(`Handling ${route.method} for agent ${route.agentId}`);
},
// After handler — modify response headers
onResponse: async ({ response, request }) => {
const headers = new Headers(response.headers);
headers.set("x-request-id", crypto.randomUUID());
return new Response(response.body, {
status: response.status,
headers,
});
},
// On error — custom error responses
onError: async ({ error, request }) => {
console.error("Handler error:", error);
},
},
});
| Hook | When | Can modify |
|---|---|---|
onRequest | Before routing | Throw Response to short-circuit |
onBeforeHandler | After routing, before handler | Access route info (method, agentId, threadId) |
onResponse | After handler | Return a new Response to replace it |
onError | On unhandled error | Log or produce a custom error response |
Pass cors: true for permissive defaults (Access-Control-Allow-Origin: *), or provide a config object:
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
cors: {
origin: "https://myapp.com",
credentials: true,
},
});
Pass cors: true for permissive defaults, or pass a cors options object:
createCopilotExpressHandler({
runtime,
basePath: "/api/copilotkit",
cors: {
origin: "https://myapp.com",
credentials: true,
},
});
Pass a cors config with explicit origin:
createCopilotHonoHandler({
runtime,
basePath: "/api/copilotkit",
cors: {
origin: "https://myapp.com",
credentials: true,
},
});
Once your runtime is running, point your frontend at it:
<CopilotKit runtimeUrl="http://localhost:4000/api/copilotkit">
<YourApp />
</CopilotKit>
For same-origin deployments (e.g. Next.js, React Router, TanStack Start), use a relative path:
<CopilotKit runtimeUrl="/api/copilotkit">
<YourApp />
</CopilotKit>