Understanding the Router Package in Cloudflare OS: Architecture and Routing Logic

The router package in Cloudflare OS functions as the public entry point for external traffic, operating as a single Cloudflare Worker that dynamically routes API requests, gatekeeper traffic, and frontend assets to appropriate backend services based on environment bindings.

This package acts as the central traffic director for the cloudflare/cloudflare-os repository, handling all incoming HTTP requests and email messages before dispatching them to the Workshop Backend, specific gatekeeper services, or static asset handlers. By leveraging Cloudflare Workers service bindings, the router maintains a zero-downtime, configuration-driven architecture that requires no code changes when adding or removing gatekeeper services.

Core Responsibilities of the Router Package

The router package aggregates four distinct traffic handling responsibilities within a single Worker instance. Each responsibility targets a specific request pattern or protocol.

API and Screenshot Request Forwarding

All paths beginning with /api or /blueprint-screenshot are proxied directly to the Workshop Backend via the WORKSHOP_BACKEND service binding. This ensures that REST API calls and screenshot generation requests reach the core application logic without additional routing overhead.

Gatekeeper Traffic Dispatching

The router implements dynamic service discovery for gatekeeper workers by scanning its environment for bindings prefixed with GATEKEEPER_. When a request arrives at /gatekeeper/<short-name>/*, the router:

  1. Iterates through environment keys matching GATEKEEPER_*
  2. Transforms the binding suffix (e.g., GATEKEEPER_MY_SERVICE → my-service)
  3. Matches the transformed name against the URL path segment
  4. Forwards the request to the corresponding Fetcher binding

This convention-based routing allows new gatekeepers to register themselves purely through wrangler.jsonc configuration changes rather than router code modifications.

Frontend Asset Serving

Static single-page application (SPA) assets are served through an optional ASSETS binding. When present (typically in production deployments), the router delegates all remaining requests to this binding. In development environments where ASSETS is omitted, the router falls back to proxying requests to the Workshop Backend, enabling Vite dev server integration for hot module replacement.

Email Gatekeeper Handling

For email-triggered workflows, the router checks for a GATEKEEPER_EMAIL binding. If bound, incoming email messages are forwarded to this specialized gatekeeper; otherwise, the router rejects the mail with a descriptive error message explaining that no email gatekeeper is installed.

Dynamic Gatekeeper Discovery Implementation

The gatekeeper routing logic resides in src/index.ts and relies on runtime environment introspection. The implementation converts binding names to URL-friendly path segments using consistent normalization rules:

// From src/index.ts - Gatekeeper routing logic
for (const key of Object.keys(env)) {
  if (!key.startsWith('GATEKEEPER_')) continue;
  const suffix = key.slice('GATEKEEPER_'.length).toLowerCase().replaceAll('_', '-');
  const prefix = `/gatekeeper/${suffix}`;
  if (url.pathname === prefix || url.pathname.startsWith(prefix + '/')) {
    return (env[key] as Fetcher).fetch(request);
  }
}

This loop executes on every request, ensuring that gatekeeper additions take effect immediately upon Worker deployment without requiring router redeployment or version bumps.

Environment-Aware Request Handling

The router distinguishes between production and development environments through the presence of the ASSETS binding. The request handling flow in src/index.ts implements a cascading priority system:

// From src/index.ts - Complete fetch handler implementation
export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);

    // 1. Gatekeeper routing (highest priority)
    for (const key of Object.keys(env)) {
      if (!key.startsWith('GATEKEEPER_')) continue;
      const suffix = key.slice('GATEKEEPER_'.length).toLowerCase().replaceAll('_', '-');
      const prefix = `/gatekeeper/${suffix}`;
      if (url.pathname === prefix || url.pathname.startsWith(prefix + '/')) {
        return (env[key] as Fetcher).fetch(request);
      }
    }

    // 2. API and screenshot routes
    if (
      url.pathname === '/api' || url.pathname.startsWith('/api/') ||
      url.pathname === '/blueprint-screenshot' || url.pathname.startsWith('/blueprint-screenshot/')
    ) {
      return env.WORKSHOP_BACKEND.fetch(request);
    }

    // 3. Static assets (production only)
    if (env.ASSETS) {
      return env.ASSETS.fetch(request);
    }

    // 4. Development fallback
    return env.WORKSHOP_BACKEND.fetch(request);
  },

  async email(message, env) {
    if (!env.GATEKEEPER_EMAIL) {
      message.setReject('No email gatekeeper is installed on this instance.');
      return;
    }
    await env.GATEKEEPER_EMAIL.email(message);
  },
} satisfies ExportedHandler<Env>;

Configuration and Deployment

Service bindings are defined in wrangler.jsonc at the package root. A typical configuration includes:

  • WORKSHOP_BACKEND: The primary application service binding
  • ASSETS: Optional static asset namespace (production)
  • GATEKEEPER_*: Dynamic service bindings for individual gatekeepers
  • GATEKEEPER_EMAIL: Optional email processing service

The test suite in __tests__/router.test.ts validates this configuration-driven behavior through stubbed Fetcher objects:

// From __tests__/router.test.ts
it('routes /gatekeeper/<short> by scanning GATEKEEPER_* bindings', async () => {
  const env = makeEnv({
    GATEKEEPER_GOOGLE: stubFetcher('google'),
    GATEKEEPER_HOMEASSISTANT: stubFetcher('homeassistant'),
  });
  const req = new Request('https://example.com/gatekeeper/google');
  const res = await router.fetch!(req, env, {} as ExecutionContext);
  expect(await res.text()).toBe('google');
});

Summary

  • The router package serves as the sole public hostname entry point for Cloudflare OS instances, consolidating all external traffic through a single Cloudflare Worker.
  • Gatekeeper routing requires zero code changes—the router discovers services by scanning GATEKEEPER_* environment bindings and automatically generates URL path mappings.
  • Environment detection determines asset handling—the presence of an ASSETS binding triggers static file serving, while its absence enables development mode proxying to the Workshop Backend.
  • Email processing is optional and explicit—the router validates GATEKEEPER_EMAIL binding existence before accepting mail traffic.
  • All routing logic resides in src/index.ts with comprehensive test coverage in __tests__/router.test.ts, while deployment configuration lives in wrangler.jsonc.

Frequently Asked Questions

How does the router package handle gatekeeper routing without requiring code changes?

The router implements runtime service discovery by iterating over its environment bindings and filtering for keys prefixed with GATEKEEPER_. It dynamically constructs routing paths by lower-casing the binding suffix and converting underscores to dashes (e.g., GATEKEEPER_MY_SERVICE becomes /gatekeeper/my-service). Adding a new gatekeeper only requires adding a binding entry in wrangler.jsonc and redeploying the router Worker.

What happens when the ASSETS binding is missing in development environments?

When the ASSETS binding is undefined, the router falls through all specific routing rules and executes env.WORKSHOP_BACKEND.fetch(request) as a final catch-all. This proxies all unmatched requests to the Workshop Backend, allowing the Vite development server to handle frontend asset requests and enabling hot module replacement during local development.

How does the router process incoming email messages?

The router exposes an email handler that checks for the GATEKEEPER_EMAIL service binding. If present, it forwards the email message object to that service's email method. If the binding is absent, it immediately rejects the message with the response "No email gatekeeper is installed on this instance," preventing unprocessed email accumulation.

Where is the core routing logic defined in the Cloudflare OS codebase?

The primary routing implementation is located in packages/router/src/index.ts, which exports the Worker's fetch and email handlers. The unit tests validating this behavior reside in packages/router/__tests__/router.test.ts, and the service binding definitions are configured in packages/router/wrangler.jsonc.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →