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 live — res.stream() and SSE emit chunks as they are produced rather than buffering the whole response.
How it differs from the other adapters
Section titled “How it differs from the other adapters”| Node / Bun | Vercel / Netlify | Cloudflare | |
|---|---|---|---|
| Config | routesDir string / ServerlessConfig | ServerlessConfig | ServerlessConfig |
| Route discovery | scan / manifest | Pre-built manifest | Pre-built manifest |
| Streaming | live | buffered | live |
req.platform | {} | {} | { env, ctx, cf } |
Project structure
Section titled “Project structure”my-app/├── routes/│ ├── index.ts│ └── users/│ └── [id]/│ └── index.ts├── worker.ts├── env.d.ts└── wrangler.tomlWorker entry point
Section titled “Worker entry point”// worker.tsimport { cloudflareAdapter } from 'lacis/adapters'import { routes } from './routes/_manifest.js'
export default cloudflareAdapter.createHandler({ routes })Accessing bindings: req.platform
Section titled “Accessing bindings: req.platform”On Cloudflare, runtime context lives under req.platform:
req.platform.env— your Worker bindings (KV, D1, R2, Queues, secrets…)req.platform.ctx— theExecutionContext(e.g.ctx.waitUntil)req.platform.cf— the incoming request’sIncomingRequestCfProperties
// routes/users/[id]/index.tsimport 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 })}Typing req.platform with env.d.ts
Section titled “Typing req.platform with env.d.ts”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 }}wrangler.toml
Section titled “wrangler.toml”name = "my-app"main = "worker.ts"compatibility_date = "2024-09-23"
# [[kv_namespaces]]# binding = "MY_KV"# id = "..."Deploy flow
Section titled “Deploy flow”-
Add
lacis buildto your build script:{"scripts": {"build": "lacis build","deploy": "wrangler deploy"}} -
Build the routes manifest and deploy:
Terminal window npm run buildwrangler 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 — callres.initSSE()before the handler’s firstawait. - Client IP:
req.connection.remoteAddressis populated from thecf-connecting-ipheader. defaultHeadersare supported (pass them in theServerlessConfig).
Optional config
Section titled “Optional config”export default cloudflareAdapter.createHandler({ routes, maxBodySize: 5_000_000, defaultHeaders: { 'X-Powered-By': 'Lacis' }, cors: { origin: 'https://myapp.com', credentials: true },})