Fetch API runtimes
You can handle Arc requests without an Arc-owned listener. Import the source-preview @cratis/arc.core/fetch entry, register artifacts explicitly, and hand the resulting app.fetch to your host. The package is not published to npm; install a tarball packed from a built clone, as Create an application shows, or use it inside the clone’s workspace. Arc targets Node-compatible servers: Chronicle uses gRPC, MongoDB uses TCP, and live queries rely on long-lived SSE/WebSocket connections. Next.js App Router on its Node.js runtime is verified locally. Bun has a smoke check, not a supported deployment guarantee. Cloudflare Workers and Next.js Edge are not supported hosts for the full framework or these integrations.
Build the application once
Section titled “Build the application once”The Fetch entry has the same command/query pipelines as Node, but does not load configuration files or discover files. Use builder.add(...) or builder.useGeneratedMetadata(metadata) followed by add(...). With no generated metadata, bind method parameters explicitly using the model-bound APIs.
import { command } from '@cratis/arc.core/fetch';
/** A minimal command served by the Fetch host. */@command()export class Ping { handle(): string { return 'pong'; }}import { query, readModel } from '@cratis/arc.core/fetch';
/** A read model with one snapshot query. */@readModel()export class Status { @query() static all(): string[] { return ['ready']; }}import { ArcApplication } from '@cratis/arc.core/fetch';import { Ping } from './Ping.js';import { Status } from './Status.js';
const builder = ArcApplication.createBuilder();builder.add(Ping, Status);declare global { var __cratisArcFetchApp: ReturnType<typeof builder.build> | undefined;}export const app = await (globalThis.__cratisArcFetchApp ??= builder.build());POST /api/ping with a JSON {} body returns response: "pong"; GET /api/all returns data: ["ready"]. The Fetch scenario exercises commands, validation, authorization, GET and QUERY, tenant and correlation headers, observable snapshots, direct and hub SSE, server-side direct SSE subscription release after client abort, and fall-through. Use your host’s build tool to compile decorators and bundle dependencies. A neutral esbuild bundle externalizes only node:async_hooks (for AsyncLocalStorage); it imports no node:fs, node:crypto, ws, or other Node modules from the Fetch entry. The HTTP checks use built workspace dist, not a published tarball.
import { app } from './arc.js';
Bun.serve({ port: 3000, fetch: (request: Request) => app.fetch(request) });Stop the Bun server before calling await app.dispose() in your shutdown hook. yarn check:fetch:bun runs the shared scenario through Bun.serve on Bun 1.3.10. This is an optional smoke check only: Chronicle gRPC, MongoDB, production deployment, and lifecycle under load are unverified.
import { app } from './arc.js';
Deno.serve({ hostname: '127.0.0.1', port: 3000 }, (request: Request) => app.fetch(request));This hosting call requires Deno network permission. Deno 2.9.7 previously passed the smaller command, query, direct and hub SSE scenario; the expanded scenario and Deno.serve listener have not been rerun there. This is not a full Arc deployment target verification.
Next.js App Router on Node.js
Section titled “Next.js App Router on Node.js”Pin the Node.js runtime. Use two catch-all route handlers: app/api/[...arc]/route.ts serves commands, queries, and direct SSE; app/.cratis/[...arc]/route.ts serves the SSE hub and its control requests. Both handlers import the same application. Next.js may bundle separate route entries independently, so cache the build promise on the process global rather than building an app for every request or route.
The arc.ts module above caches its build promise for this Node.js process; separate instances still need their own applications and their own stores.
import { app } from '../../../arc.js';
export const runtime = 'nodejs';export const dynamic = 'force-dynamic';export const GET = (request: Request): Promise<Response> => app.fetch(request);export const POST = (request: Request): Promise<Response> => app.fetch(request);import { app } from '../../../arc.js';
export const runtime = 'nodejs';export const dynamic = 'force-dynamic';export const GET = (request: Request): Promise<Response> => app.fetch(request);export const POST = (request: Request): Promise<Response> => app.fetch(request);For model-bound artifacts on Next.js 15.5.14, disable server minification: it changes class names used by Arc’s convention-based routes (Submit became tb in the production check). Keep your other Next configuration alongside this setting:
export default { experimental: { serverMinification: false } };yarn check:fetch:next builds and starts these routes with next build && next start on Node.js 26.8.1. It checks GET, POST commands, validation, authorization, first direct and hub SSE frames, direct SSE subscription release after client abort, tenant and correlation headers. The unmatched route returns Next.js’s own 404, not an Arc fall-through response. Next.js rejects the nonstandard HTTP QUERY method with 400 before the route handler; use GET instead. This local production-server check does not verify deployed serverless streaming persistence, proxy buffering, WebSocket upgrades, or Chronicle/MongoDB integrations. Next.js Edge is not supported: node:async_hooks and the full framework’s gRPC/TCP and long-lived streams are outside the Edge target.
Boundaries and verification
Section titled “Boundaries and verification”The Fetch entry cannot perform file discovery or load configuration files, start a Node listener, serve static files, or upgrade WebSockets. Calling discover() on its builder fails rather than importing files. Use @cratis/arc.core unchanged on Node for those features. Both entries share the core pipelines; Chronicle and storage integrations require their own host-specific verification. The application does not own the host listener: close that listener before await app.dispose(). A Fetch host is responsible for trusted principal metadata, ingress limits, and canceling long-lived streams when clients disconnect.
yarn check:fetch bundles the compiled package export on esbuild’s neutral platform, allows only node:async_hooks, and runs the shared scenario inside a Node VM without process or Buffer. yarn check:fetch:deno runs the same scenario in Deno. yarn check:fetch:bun and yarn check:fetch:next bundle the same built entry and send real HTTP requests through Bun.serve or Next.js App Router respectively. The Next.js QUERY check pins the host’s rejection instead of claiming Arc dispatch; all other scenario assertions are shared. Next.js is a root development dependency for the production check; set NEXT_BIN to override it. Install optional Bun outside the workspace and set BUN_BIN and ARC_RUNTIME_SCRATCH as needed. Missing executables return exit code 2 (not run); broken installations and failed checks return exit code 1. The runtime scripts use <root>/.ai-work/scratch/runtimes inside the repository checkout for temporary host files and remove generated files afterward. If you have an older Bun installation under the previous ../../scratch/runtimes location, reinstall it in the new scratch location or set ARC_RUNTIME_SCRATCH to that directory.
The Fetch entry’s node:async_hooks requirement is satisfied by Node.js, Bun, and Deno. It does not bring in Node filesystem, Node crypto, or Node streams; Fetch globals supply crypto and streams. Cloudflare Workers (even with nodejs_compat) are not supported for the framework: Chronicle gRPC, MongoDB TCP, and long-lived live-query connections are not verified or promised there. These local checks do not establish full compatibility for every optional integration.