HTTP applications
Mount authenticated Fetch routes and middleware around Fabric jobs, agents, channels, health, and identity.
Use the application surface when one deployed service needs Fabric routes plus product-specific HTTP
endpoints. Routes and middleware live in .fabricharness/config.ts, so fh dev and Node-derived
build artifacts run the same application.
Define routes
import { defineApplication, type FabricHarnessConfig } from '@fabric-harness/node';
const application = defineApplication({
routes: [
{
method: 'POST',
path: '/api/customers/:customerId/message',
permission: 'agent:invoke',
async handler(request, context) {
const body = await request.json() as {
message: string;
deliveryId?: string;
};
const receipt = await context.dispatch(
{
agent: 'support',
id: context.params.customerId!,
message: { kind: 'user', body: body.message },
},
body.deliveryId ? { idempotencyKey: body.deliveryId } : undefined,
);
return Response.json(receipt, { status: 202 });
},
},
{
method: 'POST',
path: '/api/reports',
permission: 'agent:invoke',
async handler(request, context) {
const input = await request.json();
return Response.json(
await context.invoke({ job: 'report', input }),
{ status: 202 },
);
},
},
],
});
export default {
application,
} satisfies FabricHarnessConfig;Route paths support named :parameters and a trailing * wildcard. The handler uses the standard
Web Request and returns a Web Response. Its context contains:
| Field | Purpose |
|---|---|
principal, actor, tenantId | Server-validated identity and tenant; request bodies cannot replace them. |
params | Decoded named path parameters. |
dispatch() | Durable admission to a persistent agent with optional idempotency key. |
invoke() | Ambient finite-job admission with tenant, actor, and parent correlation. |
stores | The configured session, submission, conversation, and attachment stores. |
signal | Aborts when the client aborts the request. |
requestId | Correlation id for application logs and response headers. |
Custom routes inherit the normal authentication, tenant binding, rate limit, and authorization
pipeline. Set permission to the closest built-in permission. It defaults to session:read for
GET and other non-Fabric paths. Application hooks do not accept identity from payload fields.
Add middleware
Authenticated middleware receives the complete request context and wraps both custom and built-in
routes. Calling next() once continues the chain. For a custom route, next() returns its
Response, so middleware can add headers or replace it. For a streaming or built-in Node route,
next() completes when the response finishes.
middleware: [async (request, context, next) => {
const started = performance.now();
const response = await next();
await audit.record({
requestId: context.requestId,
principalId: context.principal.id,
tenantId: context.tenantId,
method: request.method,
path: new URL(request.url).pathname,
durationMs: performance.now() - started,
});
if (!response) return;
const headers = new Headers(response.headers);
headers.set('x-fabric-request-id', context.requestId);
return new Response(response.body, { status: response.status, headers });
}],publicMiddleware wraps health, rate limiting, and authentication. It intentionally receives no
principal or stores. Use it for request timing, trusted ingress normalization, and global response
headers, not authorization decisions.
publicMiddleware: [async (request, context, next) => {
const response = await next();
console.log(context.requestId, request.method, new URL(request.url).pathname);
return response;
}],Middleware and handlers share one cached request body. Reading request.clone().json() in
middleware does not consume the bytes later used by job, channel, webhook-signature, or custom route
handling. The configured maxBodyBytes limit still applies.
Route ownership
Fabric reserves /health, /ready, /jobs, /agents, /channels, /sessions, /runs, /mcp,
/admin, /builds, /dispatch, and /openapi.json. A custom route beneath a reserved root or a
duplicate method/path fails before the server listens. This prevents an application update from
silently replacing approval, health, or agent behavior.
The complete runnable workspace is in
examples/application-routes.