Skip to content

Cloudflare Workers

Cloudflare is built on the Web adapter base (the same Web Request/Response model as Bun), so unlike Vercel and Netlify it streams liveres.stream() and SSE emit chunks as they are produced rather than buffering the whole response.

Node / BunVercel / NetlifyCloudflare
ConfigroutesDir string / ServerlessConfigServerlessConfigServerlessConfig
Route discoveryscan / manifestPre-built manifestPre-built manifest
Streaminglivebufferedlive
req.platform{}{}{ env, ctx, cf }
my-app/
├── routes/
│ ├── index.ts
│ └── users/
│ └── [id]/
│ └── index.ts
├── worker.ts
├── env.d.ts
└── wrangler.toml
// worker.ts
import { cloudflareAdapter } from 'lacis/adapters'
import { routes } from './routes/_manifest.js'
export default cloudflareAdapter.createHandler({ routes })

On Cloudflare, runtime context lives under req.platform:

  • req.platform.env — your Worker bindings (KV, D1, R2, Queues, secrets…)
  • req.platform.ctx — the ExecutionContext (e.g. ctx.waitUntil)
  • req.platform.cf — the incoming request’s IncomingRequestCfProperties
// routes/users/[id]/index.ts
import type { Request, Response } from 'lacis'
export async function GET(req: Request, res: Response) {
const user = await req.platform.env.MY_KV.get(req.params!.id)
res.json({ user, country: req.platform.cf.country })
}

req.platform is empty by default and augmented via declaration merging. The Cloudflare template generates an env.d.ts so you access bindings without as any:

// env.d.ts
/// <reference types="@cloudflare/workers-types" />
interface Env {
MY_KV: KVNamespace
MY_DB: D1Database
// …your bindings
}
declare module 'lacis' {
interface PlatformContext {
env: Env
ctx: ExecutionContext
cf: IncomingRequestCfProperties
}
}
name = "my-app"
main = "worker.ts"
compatibility_date = "2024-09-23"
# [[kv_namespaces]]
# binding = "MY_KV"
# id = "..."
  1. Add lacis build to your build script:

    {
    "scripts": {
    "build": "lacis build",
    "deploy": "wrangler deploy"
    }
    }
  2. Build the routes manifest and deploy:

    Terminal window
    npm run build
    wrangler deploy

lacis dev detects wrangler.toml and runs wrangler dev for you after generating the manifest.

  • SSE / initSSE(): like Bun, Cloudflare must decide streaming synchronously — call res.initSSE() before the handler’s first await.
  • Client IP: req.connection.remoteAddress is populated from the cf-connecting-ip header.
  • defaultHeaders are supported (pass them in the ServerlessConfig).
export default cloudflareAdapter.createHandler({
routes,
maxBodySize: 5_000_000,
defaultHeaders: { 'X-Powered-By': 'Lacis' },
cors: { origin: 'https://myapp.com', credentials: true },
})