Host adapters
Your team already runs a web framework, with its middleware, health checks, and deployment story. You do not want a second server for Arc. A host adapter mounts the Arc application into the framework you have and leaves every route Arc does not own to that framework.
Before you start
Section titled “Before you start”- An ES module package (
"type": "module"). Applications using the adapters and Core need Node.js 22 or later; building the workspace needs 22.19 or later, and Node.js 24 LTS is recommended. - A built Arc application. Keep it in its own module so every host imports the same instance:
import { ArcApplication } from '@cratis/arc.core';import { Tasks } from './Features/Tasks/Tasks.js';import { metadata } from './Features/generatedMetadata.js';
const builder = ArcApplication.createBuilder();builder.useGeneratedMetadata(metadata);builder.services.addSingleton(Tasks);await builder.discover(new URL('./Features/', import.meta.url));export const arc = await builder.build();This is the Tasks sample’s bootstrap without app.run(), because the framework owns the listener. metadata is the module that arc-proxygenerator --metadata writes; see Generate artifact metadata. Register your own services the way the sample registers Tasks. build() checks the artifacts and options and throws when something is wrong, so a mistake shows up when the process starts rather than on the first request.
| Package | Exports | Framework peer range |
|---|---|---|
@cratis/arc.express | cratisArc(arc) middleware and .injectWebSocket(listener) | express ^5.0.0 |
@cratis/arc.fastify | app.register(cratisArc, { arc }) (WebSockets on by default) | fastify ^5.0.0 |
@cratis/arc.hono | app.use(cratisArc(arc)); Node: serveCratisArc | hono ^4.0.0; optional @hono/node-server ^1.19.11 for Node hosting |
@cratis/arc.core/hosting | attachNodeWebSockets and adapter hosting primitives | None |
Each adapter accepts a built ArcApplication or low-level ArcServer. The @cratis/arc.core/hosting subpath also exposes integration-only command helpers. Applications must not call these helpers; inlineCommitClientResponse, for example, lets a trusted integration mark a command whose handler commits inline.
Pick your framework
Section titled “Pick your framework”- Express: one middleware, mounted before body parsers.
- Fastify: an encapsulated plugin with its own raw-body parser.
- Hono: middleware on a Fetch API framework, with raw-path checks on Node.
- Fetch API runtimes: explicitly registered artifacts over
app.fetchfor a checked Next.js Node.js production-server route handler and bounded Bun/Deno checks. The package is an unpublished source preview; read the verification boundary before deployment.
Express needs a listener attach step for WebSockets; Fastify’s plugin and Hono’s Node helper attach them in the setup call, described in WebSockets. To use a principal your framework already verified, see Native principal.
What every adapter shares
Section titled “What every adapter shares”The adapters add no Arc behavior of their own. Each one:
- dispatches only requests whose path exactly matches an Arc route, on a fixed internal origin, so a crafted
Hostheader cannot select a different operation (Express and Fastify compare the raw path; Hono checks the raw request-target only on@hono/node-server); - hands Arc the unparsed body and lets Arc enforce
hosting.maxBodyBytes; - passes a cancellation signal that becomes
context.signal; pass it to anything that accepts one, such asfetchor a database driver; - serves the same routes: commands, validation routes, queries, observable queries, and the
/.cratisand/openapi.jsonendpoints listed in the HTTP contract reference.
Where adapters differ
Section titled “Where adapters differ”| Area | Express | Fastify | Hono |
|---|---|---|---|
| Registration | One middleware, before body parsers | Encapsulated plugin; routes exist once the app is ready | One middleware |
| Request bodies | Raw request stream | Raw buffer from a scoped catch-all parser | The Fetch API request body |
| Body size | Arc’s hosting.maxBodyBytes | Fastify’s bodyLimit first, then hosting.maxBodyBytes | Arc’s hosting.maxBodyBytes |
| Unknown path | Falls through to your routes; Express’s own 404 carries no Arc correlation header | Fastify’s 404 | Falls through to your routes |
context.signal aborts | When the client disconnects before the response finishes | When the client disconnects before the response finishes | When the signal of the request Hono received aborts; depends on the server running Hono |
| Application type | Express | FastifyInstance | Hono<E> for any Env type |
Ownership and shutdown
Section titled “Ownership and shutdown”The framework owns its listener. Mounting an application does not make the adapter own shutdown: call shutdownArcHost(arc.server, () => framework.close()) from @cratis/arc.core/hosting to start participant shutdown before closing a listener with live observables. Express also provides middleware.shutdown(listener). If your host supplied Arc’s registry, pass it explicitly as the third argument to shutdownArcHost (or the second argument to Express shutdown); it is never disposed implicitly. Without participants the helper closes the framework first, then disposes Arc, as before. framework.close() or middleware.close(listener) alone remains listener-only.
Related
Section titled “Related”- Hosting overview
- Arc.Core and the standalone host for hosting without a framework
- Calling commands from code