Deployment
Kuralle agents run anywhere that runs Node.js, Bun, or Cloudflare Workers. The runtime has no external process dependencies — serve it with whatever HTTP framework you prefer.
To compile instructions, skills, references, workspace seeds, and code capabilities from a folder, follow File-authored Agents.
If agent definitions live in an existing Postgres, Prisma, or Drizzle application—or the runtime is on Cloudflare while Hono and Neon remain your backend—see Agent Definitions in Your Database.
Hono (Node.js or Bun)
Section titled “Hono (Node.js or Bun)”@kuralle-agents/hono-server mounts a full set of endpoints onto a Hono app with one call.
npm install @kuralle-agents/hono-server hono @hono/node-server @hono/node-wsimport { Hono } from 'hono';import { serve } from '@hono/node-server';import { createNodeWebSocket } from '@hono/node-ws';import { createRuntime, defineAgent } from '@kuralle-agents/core';import { createKuralleChatRouter } from '@kuralle-agents/hono-server';import { openai } from '@ai-sdk/openai';
const agent = defineAgent({ id: 'support', instructions: 'You are a helpful support agent.', model: openai('gpt-4o-mini'),});
const runtime = createRuntime({ agents: [agent], defaultAgentId: 'support' });
const app = new Hono();const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app });app.route('/', createKuralleChatRouter({ runtime, upgradeWebSocket }));
const server = serve({ fetch: app.fetch, port: 3000 });injectWebSocket(server);Bun doesn't need @hono/node-server or createNodeWebSocket. Use Bun's built-in WebSocket upgrade instead:
import { upgradeWebSocket } from 'hono/bun';
const app = new Hono();app.route('/', createKuralleChatRouter({ runtime, upgradeWebSocket }));
export default app;WhatsApp & messaging webhooks
Section titled “WhatsApp & messaging webhooks”To deploy a bot on WhatsApp / Instagram (rather than the web chat endpoints), mount createMessagingRouter from engagement() instead — it exposes the Meta webhook at /{platform}/webhook. A self-hostable reference that boots on Bun or Node with a real WhatsApp Cloud API number (bring your own token, no Embedded Signup) and an optional Redis WindowStore:
# bring-your-own number/token — set WHATSAPP_* env, then:bun run packages/messaging-meta/examples/whatsapp-server/server.ts# webhook: https://<host>/messaging/whatsapp/webhookSee packages/messaging-meta/examples/whatsapp-server/README.md and the Engagement guide.
Endpoints
Section titled “Endpoints”createKuralleChatRouter mounts these routes:
| Method | Path | Description |
|---|---|---|
POST | /api/chat | Single-turn JSON response |
POST | /api/chat/sse | AI SDK UIMessageStream (default, useChat-compatible). ?format=raw for legacy StreamPart JSON-SSE |
POST | /api/chat/stream | Chunked text stream |
GET | /agents/chat/:sessionId | WebSocket widget endpoint |
GET | /ws/:sessionId | WebSocket turn endpoint |
GET | /api/session/:id | Fetch session |
DELETE | /api/session/:id | Delete session |
GET | /health | Health check |
Cloudflare Workers
Section titled “Cloudflare Workers”@kuralle-agents/cf-agent runs Kuralle agents on Cloudflare Workers with Durable Objects. Subclass KuralleAgent, implement two methods, and Cloudflare handles SQLite persistence, multi-client sync, and stream resumability.
npm install @kuralle-agents/cf-agent agents zodimport { KuralleAgent } from '@kuralle-agents/cf-agent';import { defineAgent } from '@kuralle-agents/core';import { createOpenAI } from '@ai-sdk/openai';import { routeAgentRequest } from 'agents';
interface Env { OPENAI_API_KEY: string; SupportAgent: DurableObjectNamespace;}
export class SupportAgent extends KuralleAgent<Env> { protected getAgents() { const openai = createOpenAI({ apiKey: this.env.OPENAI_API_KEY }); return [ defineAgent({ id: 'support', instructions: 'You are a helpful support agent.', model: openai('gpt-4o-mini'), }), ]; }
protected getDefaultAgentId() { return 'support'; }}
export default { async fetch(request: Request, env: Env) { return (await routeAgentRequest(request, env, { cors: true })) ?? new Response('Not found', { status: 404 }); },} satisfies ExportedHandler<Env>;Declare the Durable Object in wrangler.jsonc:
{ "main": "src/worker.ts", "compatibility_date": "2026-07-29", "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "SupportAgent", "class_name": "SupportAgent" }] }, "exports": { "SupportAgent": { "type": "durable-object", "storage": "sqlite" } }}Cloudflare recommends declarative exports for new Agent classes. If an existing Worker already has
a migrations history, preserve it and add sequential migrations instead of rewriting deployed
history.
The public native endpoint is /agents/support-agent/{instance}. One instance name resolves to one Durable Object and therefore one single-writer session boundary. Authenticate and authorize that instance name at your Worker boundary; do not accept an arbitrary tenant id merely because it matches the URL shape.
Flows, tools, derived routing, the Pi driver, and persistent SqlFileSystem work through this same KuralleAgent runtime.
Drive a deployed agent from the CLI
Section titled “Drive a deployed agent from the CLI”Kuralle's CLI can save either the completion-oriented HTTP endpoint or the native Cloudflare Agents transport:
# Next.js, Hono, or a Worker exposing POST /api/chatkuralle connect https://my-agent.example.com
# Native Cloudflare Agents WebSocket protocolkuralle connect https://my-agent.workers.dev \ --transport cloudflare \ --agent-name support-agent
kuralle chat --session customer-42The hosted runtime owns credentials, state, tools, and workspaces. The CLI stores only the non-secret server/transport selection. Provide a bearer token with KURALLE_TOKEN; see CLI & Devtools.
The deployment thread route
Section titled “The deployment thread route”POST /v1/agents/:agentEntityId/threads/:threadId/messages serves an AI SDK
UIMessageStream by default — the same wire every Kuralle runtime speaks — so
useChat consumes it with no bridge code:
const transport = new DefaultChatTransport({ api: `/v1/agents/${agentId}/threads/${threadId}/messages`, headers: () => ({ authorization: `Bearer ${token}`, 'idempotency-key': key.current }),});const { messages, sendMessage } = useChat<KuralleUIMessage>({ id: threadId, transport });Append ?format=raw for the named-event StreamPart SSE, the same negotiation
/api/chat/sse and /api/flow/sse already use. That form is for non-browser
consumers — CLIs, webhooks, custom transports.
HTTP streaming
Section titled “HTTP streaming”Web (useChat, default)
Section titled “Web (useChat, default)”POST /api/chat/sse returns a native AI SDK UIMessageStream. A React client uses useChat with no bridge:
import { useChat } from '@ai-sdk/react';import type { KuralleUIMessage } from '@kuralle-agents/core';
const { messages } = useChat<KuralleUIMessage>({ api: '/api/chat/sse' });Kuralle flow/safety/interactive events arrive as data-kuralle-* parts — read persistent parts from message.parts, transient telemetry from useChat({ onData }). See an earlier decision for the full mapping table.
Proxies and buffering
Section titled “Proxies and buffering”The runtime sets Cache-Control: no-cache, no-transform, Content-Encoding: identity and
X-Accel-Buffering: no on every streamed turn. Leave them alone — they are what stops an
intermediary collecting the whole turn and delivering it in one frame.
This is worth knowing about because the failure looks like a slow model rather than a broken proxy:
the server streams correctly, the client shows a spinner for the entire turn, and then everything
appears at once. It is also easy to mis-diagnose. A plain curl sends no Accept-Encoding, so it
measures a healthy stream while a browser — which always negotiates encoding — sees nothing until
the end. If you are checking whether streaming works, use curl --compressed, or measure in the
browser.
A Next.js rewrites() proxy in front of the server is the common case that trips this.
Raw JSON-SSE (?format=raw)
Section titled “Raw JSON-SSE (?format=raw)”Non-UI consumers that parsed StreamPart JSON from 0.4.x append ?format=raw:
curl -N -X POST 'http://localhost:3000/api/chat/sse?format=raw' \ -H 'Content-Type: application/json' \ -d '{"message":"hello"}'Or use createKuralleSseChatRouter for a router that always emits raw JSON-SSE.
Direct TurnHandle piping
Section titled “Direct TurnHandle piping”Without createKuralleChatRouter, return a native stream for web clients:
app.post('/chat', async (c) => { const { input, sessionId } = await c.req.json(); const handle = runtime.run({ input, sessionId }); return handle.toUIMessageStreamResponse({ sessionId });});For raw StreamPart JSON-SSE (curl, custom transports):
return new Response(handle.toResponseStream('sse'), { headers: { 'Content-Type': 'text/event-stream; charset=utf-8' },});createKuralleChatRouter wires the native default on /api/chat/sse and also mounts sessions, WebSocket, and the full endpoint set.
Production references
Section titled “Production references”- Pharmacy Workspace Agent — the same application on Next.js/Vercel and a Pi-powered
KuralleAgentDurable Object, with hosted CLI access. - Postgres Hacker Starter — Next.js/Hono, signed identity, Postgres sessions and memory, pgvector retrieval, and approvals.
- Examples — every runnable production system and deployment lab.