FabricFabricHarness
Building Agents

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.

Rendering diagram...

Define routes

.fabricharness/config.ts
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:

FieldPurpose
principal, actor, tenantIdServer-validated identity and tenant; request bodies cannot replace them.
paramsDecoded 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.
storesThe configured session, submission, conversation, and attachment stores.
signalAborts when the client aborts the request.
requestIdCorrelation 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.