Cloudflare Workers example

Try it
This example runs inside the Setu-TS repository. To start a similar project:
npm create @setu-ts my-app -- --runtime cloudflare-workers
cd my-app && npm install
npx wrangler dev
The commands further down run inside a clone of the repository.

An injected Workers env reaches the Workers-only runtime composition and CloudflarePlugin; the Worker reads and writes the EXAMPLE_KV binding and exports a cron handler. The runtime composition uses the public Workers adapters directly so Wrangler’s bundled Worker never evaluates Node, Deno, or Bun adapters.

Wrangler is not a dependency of this example; install it, then run the Worker locally from this directory:

cd apps/cloudflare
wrangler dev --local

GET /diag/platform answers with the detected runtime. pnpm --dir apps/cloudflare run smoke runs the checks CI runs.

Create a local KV namespace and replace the placeholder id before using this in a real account. The smoke task starts local Wrangler, reads and writes KV, and invokes the scheduled handler; it skips only when Wrangler is unavailable.

Read the example deep dive for the concept and source walkthrough.

Read, in order

worker.ts

import {
  createMessagingHandler,
  createScheduledHandler,
  ReplyInboxObjectCore,
  WorkersCron,
} from '@setu-ts/cloudflare-plugin';
import type {
  CloudflareWorkerEnv,
  IDurableObjectState,
  IDurableObjectWebSocket,
  IQueueMessageBatch,
  IScheduledController,
} from '@setu-ts/cloudflare-plugin';
import type { IKernelApplication } from '@setu-ts/kernel';
import { createApp } from './setu.config.ts';

interface WorkerEnvironment extends CloudflareWorkerEnv {
  readonly EXAMPLE_KV: { put(key: string, value: string): Promise<void> };
}

let scheduledRuns = 0;
const cron = new WorkersCron().on('*/5 * * * *', () => {
  scheduledRuns += 1;
  return Promise.resolve();
});

/** The Worker's `fetch` path, plus the started application its other exports reuse. */
export interface WorkerHandler {
  fetch(request: Request, env: WorkerEnvironment): Promise<Response>;
  /**
   * The application `fetch` started.
   *
   * The `queue` export must dispatch into this same application: a second one
   * would hold its own broker, and the subscriptions registered on one would be
   * invisible to the other.
   */
  application(env: WorkerEnvironment): Promise<IKernelApplication>;
}

/**
 * Starts the application once per isolate and shares it between invocations.
 *
 * Concurrent callers share one start. A failed start is forgotten, so the next
 * request retries it instead of the isolate failing for its whole life.
 */
export function createWorkerHandler(
  build: (env: WorkerEnvironment) => IKernelApplication = createApp,
): WorkerHandler {
  let started: Promise<IKernelApplication> | undefined;

  const application = (env: WorkerEnvironment): Promise<IKernelApplication> => {
    if (started === undefined) {
      const attempt = (async () => {
        const app = build(env);
        await app.start();
        return app;
      })();
      started = attempt;
      attempt.catch(() => {
        started = undefined;
      });
    }
    return started;
  };

  return {
    fetch: (request, env) => application(env).then((app) => app.fetch(request)),
    application,
  };
}

const workerHandler = createWorkerHandler();

/**
 * The Durable Object that holds request/reply inboxes.
 *
 * Not `extends DurableObject`: that class lives in `cloudflare:workers`, which
 * only a Worker toolchain resolves. workerd accepts a plain `(ctx, env)` class.
 */
export class ReplyInboxObject {
  readonly #core: ReplyInboxObjectCore;

  constructor(ctx: IDurableObjectState, _env: CloudflareWorkerEnv) {
    this.#core = new ReplyInboxObjectCore(ctx);
  }

  fetch(request: Request): Promise<Response> {
    return this.#core.fetch(request);
  }

  webSocketClose(ws: IDurableObjectWebSocket, code: number, reason: string): void {
    this.#core.webSocketClose(ws, code, reason);
  }

  webSocketError(ws: IDurableObjectWebSocket): void {
    this.#core.webSocketError(ws);
  }
}

export default {
  fetch(request: Request, env: WorkerEnvironment): Promise<Response> {
    return workerHandler.fetch(request, env);
  },

  /** Cloudflare delivers queue messages here, in a separate invocation from the publish. */
  async queue(batch: IQueueMessageBatch, env: WorkerEnvironment): Promise<void> {
    await createMessagingHandler(await workerHandler.application(env))(batch);
  },

  async scheduled(controller: IScheduledController, env: WorkerEnvironment): Promise<void> {
    await createScheduledHandler(cron)(controller);
    await env.EXAMPLE_KV.put('scheduled-runs', String(scheduledRuns));
  },
};

wrangler.toml

name = "setu-ts-cloudflare-example"
main = "worker.ts"
compatibility_date = "2025-09-01"
compatibility_flags = ["nodejs_compat"]

[[kv_namespaces]]
binding = "EXAMPLE_KV"
id = "00000000000000000000000000000000"

[triggers]
crons = ["*/5 * * * *"]

[[queues.producers]]
binding = "MESSAGES"
queue = "example-messages"

# `max_batch_timeout = 0` is what makes request/reply usable: the platform
# default of 5s alone exhausts the default reply budget.
[[queues.consumers]]
queue = "example-messages"
max_batch_size = 1
max_batch_timeout = 0

[[durable_objects.bindings]]
name = "REPLY_INBOX"
class_name = "ReplyInboxObject"

[[migrations]]
tag = "v1"
new_classes = ["ReplyInboxObject"]

smoke.ts

import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { run } from '../../scripts/lib/run.ts';
import { delay, freePort, skip, startHost, stopHost, waitForHttp } from '../smoke-helpers.ts';
import type { Host } from '../smoke-helpers.ts';

const SCRATCH = '../../.tmp/cloudflare-example';
const BUNDLE = `${SCRATCH}/worker.mjs`;

/** Whether the `wrangler` binary is on PATH. */
async function wranglerAvailable(): Promise<boolean> {
  try {
    return (await run('wrangler', ['--version'])).code === 0;
  } catch (error) {
    if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return false;
    throw error;
  }
}

/** Bundles the Worker so workerd never evaluates the Node, Deno or Bun adapters. */
async function bundleWorker(): Promise<void> {
  const { code } = await run(
    'pnpm',
    [
      'exec',
      'esbuild',
      'worker.ts',
      '--bundle',
      '--format=esm',
      '--platform=browser',
      '--tsconfig=../../tsconfig.apps.json',
      '--external:node:*',
      `--outfile=${BUNDLE}`,
    ],
    { inherit: true },
  );
  if (code !== 0) throw new Error('esbuild could not bundle the Worker.');
}

function startWrangler(port: number): Host {
  return startHost('wrangler', [
    'dev',
    BUNDLE,
    '--config',
    'wrangler.toml',
    '--local',
    '--test-scheduled',
    '--compatibility-date',
    '2025-09-01',
    '--compatibility-flag',
    'nodejs_compat',
    '--persist-to',
    `${SCRATCH}/state`,
    '--port',
    String(port),
    '--log-level',
    'error',
  ]);
}

if (!(await wranglerAvailable())) skip('Wrangler is not installed; the Worker was not run.');

describe('cloudflare example on workerd', () => {
  let worker: Host;
  let baseUrl: string;

  const readValue = async (key: string): Promise<string | undefined> => {
    const response = await fetch(`${baseUrl}/value/${key}`);
    return response.ok ? (await response.json()).value : undefined;
  };

  before(async () => {
    await bundleWorker();
    const port = await freePort();
    baseUrl = `http://127.0.0.1:${port}`;
    worker = startWrangler(port);
    // `/value/<unset key>` answers 404 once the Worker is serving.
    await waitForHttp(`${baseUrl}/value/ready`, {
      label: 'Wrangler',
      isReady: (response) => response.status === 404,
    });
  });

  after(async () => {
    await stopHost(worker);
  });

  it('reads back a KV value it wrote', async () => {
    const written = await fetch(`${baseUrl}/value/smoke`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ value: 'written-by-wrangler' }),
    });

    assert.equal(written.status, 204);
    assert.equal(await readValue('smoke'), 'written-by-wrangler');
  });

  it('runs the scheduled handler', async () => {
    const triggered = await fetch(`${baseUrl}/__scheduled?cron=*/5+*+*+*+*`);

    assert.ok(triggered.ok, `scheduled trigger returned ${triggered.status}`);
    assert.equal(await readValue('scheduled-runs'), '1');
  });

  it('detects the Cloudflare Workers runtime', async () => {
    const { detected } = await (await fetch(`${baseUrl}/diag/platform`)).json();

    assert.equal(detected, 'cloudflare-workers');
  });

  it('delivers a published message to the subscriber in another invocation', async () => {
    // Unique per run: `--persist-to` keeps KV between runs, so a fixed value
    // would be satisfied by an earlier run's delivery.
    const note = `audited-${crypto.randomUUID()}`;
    const published = await fetch(`${baseUrl}/publish/${note}`, { method: 'POST' });
    assert.equal(published.status, 202);

    let seen: string | undefined;
    for (let attempt = 0; attempt < 60 && seen !== note; attempt += 1) {
      seen = await readValue('last-audit');
      if (seen !== note) await delay(250);
    }
    assert.equal(seen, note);
  });

  it('answers a request through a Durable Object reply inbox', async () => {
    const response = await fetch(`${baseUrl}/double/21`);

    assert.ok(response.ok, `request/reply returned ${response.status}`);
    assert.deepEqual(await response.json(), { answer: 42 });
  });
});