Skip to content
LogoLogo

Cloudflare Workers

A Worker is a module whose fetch handler answers each request. Write the handler in TypeScript and the program in VibeLang: the handler runs the program with an Effect runner. esbuild bundles both through vibelang-unplugin, and Wrangler runs the bundle locally in workerd, Cloudflare's runtime.

Install

npm install effect vibelang-runtime
npm install -D vibelang vibelang-unplugin esbuild wrangler

Configure

Keep a normal tsconfig.json that includes the .vibe sources: the plugin compiles with it, and without one the build stops with TS5058.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2022", "DOM"],
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "rewriteRelativeImportExtensions": true
  },
  "include": ["src/**/*.ts", "src/**/*.vibe"]
}

build.mjs bundles the Worker into one ES module, with the export conditions workerd resolves:

// build.mjs
import * as esbuild from "esbuild"
import vibelang from "vibelang-unplugin/esbuild"
 
await esbuild.build({
  entryPoints: ["src/worker.ts"],
  bundle: true,
  format: "esm",
  platform: "browser",
  conditions: ["workerd", "worker"],
  outfile: "dist/worker.js",
  plugins: [vibelang()]
})

wrangler.toml runs that build and serves its output unbundled:

name = "greeter"
main = "dist/worker.js"
compatibility_date = "2026-10-01"
no_bundle = true
 
[build]
command = "node build.mjs"

Write the Worker

// src/greeter.vibe
export error EmptyName
 
service Greeter {
  greet(name: string): string!EmptyName
}
 
const GreeterLive = Layer.succeed(Greeter, {
  greet(name) {
    if (name.length === 0) throw new EmptyName()
    return `Hello, ${name}!`
  }
})
 
export function greeting(name: string) {
  provide GreeterLive {
    return Greeter.greet(name) catch "Hello, stranger!"
  }
}
// src/worker.ts
import { Effect } from "effect"
import { greeting } from "./greeter.vibe"
 
export default {
  async fetch(request: Request): Promise<Response> {
    const name = new URL(request.url).searchParams.get("name") ?? "Worker"
    return new Response(await Effect.runPromise(greeting(name)))
  }
}

A Worker has no main, so the entry's platform import, import "vibelang/platform/node", has nothing to run. Provide each program's layers where it runs, as greeting does; HttpClient takes FetchHttpClient.layer, which calls the Worker's own fetch.

Run it locally

npx wrangler dev
curl "http://localhost:8787/?name=Ada"
# Hello, Ada!
curl "http://localhost:8787/?name="
# Hello, stranger!

npx wrangler deploy runs the same build and uploads it to your Cloudflare account.