KB LabsDocs

Architecture

Last updated April 7, 2026


Internal components, bootstrap sequence, and request lifecycle.

The gateway is a Fastify 5 application with a handful of internal components that handle routing, auth, host tracking, and WebSocket dispatch. This page documents the internal structure — the code you'd touch if you were modifying the gateway itself.

Source: infra/kb-labs-gateway/apps/gateway-app/src/.

Component layout

apps/gateway-app/src/
├── bootstrap.ts          ← startup sequence
├── config.ts             ← loadGatewayConfig() — reads kb.config.json
├── server.ts             ← Fastify setup, route registration
├── manifest.ts           ← ServiceManifest for kb-dev

├── auth/
│   ├── middleware.ts     ← Bearer token validation
│   ├── routes.ts         ← /auth/token, /auth/refresh
│   └── tokens.ts         ← JWT minting, verification, token store

├── hosts/
│   ├── registry.ts       ← HostRegistry — in-cache + persistent store
│   ├── dispatcher.ts     ← Dispatch capability calls to connected hosts
│   └── ws-handler.ts     ← WebSocket handler for host agent connections

├── execute/
│   ├── routes.ts         ← /internal/dispatch endpoint
│   ├── execution-registry.ts
│   ├── retry-executor.ts
│   └── errors.ts

├── ws/
│   └── gateway-ws.ts     ← WebSocket server wiring

├── llm/                  ← (optional) LLM gateway routes
├── platform/             ← Platform introspection
├── clients/              ← Typed upstream clients
├── telemetry/            ← Correlation + metrics
├── observability/        ← Diagnostic events
└── docs/                 ← /openapi.json registration

Bootstrap sequence

From bootstrap.ts:

  1. Init platform. createServiceBootstrap({ appId: 'gateway', repoRoot }) loads .env, initializes the platform singleton (logger, cache, storage, optionally a SQL adapter).
  2. Build a correlated logger with serviceId: 'gateway' bindings.
  3. Load gateway config via loadGatewayConfig(repoRoot) — this parses the gateway section of kb.config.json through GatewayConfigSchema. Missing config or missing gateway key means the gateway starts with defaults (port: 4000, empty upstreams).
  4. Create the host store. If platform.sqlDatabase is registered, construct a SqliteHostStore for persistence. Otherwise log a warning and fall back to cache-only — hosts will be lost on restart.
  5. Create the HostRegistry, combining platform.cache (for fast lookup) with the optional SQL store (for durability).
  6. Restore persisted hosts from the store into the cache. If restore fails, log a diagnostic event and abort bootstrap.
  7. Seed static tokens. For each entry in gateway.staticTokens, write host:token:<token>{ hostId, namespaceId } into the cache so the auth middleware recognizes them.
  8. Build JWT config. Read GATEWAY_JWT_SECRET from env. Required in production; warns and falls back to an insecure default if missing.
  9. Create the Fastify server with the injected registry, JWT config, and upstreams.
  10. Start listening on the configured port.
  11. Wire graceful shutdown on SIGTERM / SIGINT.

Persistent host store

The gateway uses SQLite for persistence when available:

TypeScript
const db = platform.getAdapter<ISQLDatabase>('sqlDatabase');
if (db) {
  hostStore = new SqliteHostStore(db);
  logger.info('Host store: SQLite (persistent)');
} else {
  logger.warn('Host store: none (cache-only, hosts will be lost on restart)');
}

The choice is automatic — the gateway inspects the platform's adapter registry at bootstrap time. Configure a SQL adapter (see Configuration → kb.config.json) to enable persistence; leave it out and every gateway restart starts with an empty host registry.

This isn't just about host metadata — it's about whether in-flight WebSocket sessions survive a restart. Without persistence, every host agent has to reconnect after a gateway restart. With persistence, the registry comes back with its previous state and existing connections can be resumed or rejected cleanly.

Fastify setup

The gateway uses Fastify 5 with @fastify/cors for cross-origin handling and @fastify/websocket (via @kb-labs/shared-http) for the WebSocket tunnel.

Body limit: the gateway sets a moderate body limit — large enough for typical API requests (several MB) but bounded to avoid request-smuggling attacks.

Logging: like every other service, Fastify's own logger is disabled and all logging flows through platform.logger.

Host registry

HostRegistry is the data structure that tracks which hosts are currently connected. Two-layer design:

  • Cache layer (platform.cache) — fast lookups and TTL-based expiration.
  • Store layer (SqliteHostStore or none) — durable records that survive restart.

On every write, the registry updates both layers. On reads, it hits the cache first and falls back to the store for misses (e.g. after a restart, before the cache is warm).

Keys:

  • host:connected:<hostId> — "this host is currently connected" marker with TTL.
  • host:token:<token> — token → { hostId, namespaceId } lookup for auth.
  • host:by-namespace:<namespaceId> — reverse lookup for dispatch.

The TTL-based approach means stale entries expire automatically even if a host disconnects ungracefully.

Host dispatcher

dispatcher.ts is what the /internal/dispatch endpoint calls when it needs to execute something on a host agent. Given a namespaceId (or a specific hostId), the dispatcher:

  1. Looks up a connected host in the registry.
  2. Sends a capability call message over the host's WebSocket.
  3. Correlates the response by requestId.
  4. Streams intermediate events (chunk) and the final result back to the caller.

This is the mechanism behind the workspace-agent adapter: when a workflow asks for a workspace, the adapter calls /internal/dispatch, which routes to a connected host agent, which performs the filesystem operation locally and sends the result back.

WebSocket handlers

Two distinct WebSocket surfaces:

  • /hosts/connect — for host agents connecting inbound. Authenticated via Bearer JWT. Managed by ws-handler.ts.
  • /api/v1 WebSocket upgrades — proxied through to the REST API upstream if websocket: true is set on the upstream config. The gateway acts as a transparent WebSocket proxy in this case.

The host agent WS is the more interesting one — it's bidirectional, survives for the lifetime of the connection, and is the channel for dispatching capability calls back to the agent.

Internal dispatch endpoint

/internal/dispatch is a gateway-internal endpoint used by the container execution backend. When platform.execution.mode === 'container', the REST API (or workflow daemon) forwards execution requests through this endpoint to a container environment.

It's not exposed publicly — auth is via a separate shared secret (GATEWAY_INTERNAL_SECRET), not via the public token system. The endpoint accepts a handler reference plus inputs and returns the execution result.

This is how container-mode plugin execution works end-to-end:

  1. CLI invokes pnpm kb commit:commit.
  2. REST API (or CLI's gateway-executor) looks up the plugin's handler.
  3. Execution mode is container → forward to POST /internal/dispatch on the gateway.
  4. Gateway mints a short-lived JWT for the container to call back with.
  5. Gateway tells the environment adapter to spin up a container with the JWT and the handler reference.
  6. Container starts a runtime server, imports the handler, executes it, and streams results back.
  7. Gateway forwards the results to the original caller.

See Concepts → Execution Model for the full pipeline.

Request lifecycle

For a typical proxied request (say, POST /api/v1/plugins/commit/generate from Studio):

Studio


Gateway :4000
  │ 1. CORS preflight (handled by @fastify/cors)
  │ 2. Auth middleware: verify Bearer token → { hostId, namespaceId }
  │ 3. Correlation middleware: attach traceId/requestId to request
  │ 4. Match upstream: prefix '/api/v1' → rest @ http://localhost:5050
  │ 5. Apply rewritePrefix: (none) → forward path unchanged
  │ 6. Forward headers + body to REST API
  │ 7. Stream response back to Studio

REST API :5050
  │ handles the request, calls plugin handler, returns result

Gateway → Studio (response)

For a host-dispatched request (workflow wants to read a file on a developer's laptop):

Workflow daemon


Gateway /internal/dispatch
  │ 1. Verify internal secret header
  │ 2. Parse request { namespaceId, capability, method, args }
  │ 3. Look up connected host in HostRegistry for namespaceId
  │ 4. Send WS message to host agent: { requestId, capability, method, args }
  │ 5. Wait for response (with timeout)

Host agent (on laptop) ← WSS tunnel
  │ executes local filesystem call

Gateway ← WS message: { requestId, result }

Workflow daemon (response)

Graceful shutdown

On SIGTERM / SIGINT:

  1. Stop accepting new HTTP and WS connections.
  2. Send disconnect frames to connected host agents (they'll reconnect later with backoff).
  3. Drain in-flight HTTP requests.
  4. Close the Fastify server.
  5. Close the host store (commit any pending writes to SQLite).
  6. Call platform.shutdown() to flush the logger and close adapters.
  7. Exit.
Architecture — KB Labs Docs