Deploying Cloudflare Workers with the cf CLI
hostc's server runs on Cloudflare Workers and Durable Objects. We deploy it with cf, Cloudflare's newer command line tool, and describe it in a TypeScript config instead of wrangler.toml. cf is still in beta, so pin its version. These are our notes on the setup and the problems we hit.
Writing the config in TypeScript
cloudflare.config.ts is TypeScript, so a Worker's types come from its config, and a mistake fails at build time:
import { bindings, defineConfig, exports } from "cf/config";
import * as entrypoint from "./src/index.ts" with { type: "cf-worker" };
export default defineConfig({
worker: {
name: "my-worker",
entrypoint,
compatibilityDate: "2026-10-30",
workersDev: false,
exports: { Room: exports.durableObject({ storage: "sqlite" }) },
env: { ERRORS: bindings.analyticsEngineDataset({ name: "my_errors" }) },
},
});cf workers types generates the Env type from this file, so a binding you declare is typed everywhere.
Check option names in the type definitions. We wrote dataset: for that binding out of habit from Wrangler, and the right key is name.
Configuration that depends on the mode
The config can be a function of the mode, which keeps domains out of the code:
export default defineConfig(({ mode }) => {
const production = mode === "production";
// Throw here if a production setting is missing.
return { worker: {/* ... */} };
});Our deploy command is cf deploy --mode production. If a domain is missing it throws, so we never deploy a Worker with no routes.
Writing out the switches
An option you leave out gets Cloudflare's default, and logging, for example, is on by default. So we write the observability switches in the config. Before moving the compatibility date forward, check what it turns on. For example: after 2026-03, WebSocket binaryType defaults to "blob".
Problems we hit
cf dev --hostis not the listen address. It changes the host your Worker sees and drops the port fromrequest.url, which broke the URLs we generate.cf devtakes only--mode,--hostand--port, so pass everything else through the environment.cfstill hands some work to Wrangler, and secrets are set withwrangler secret put.pnpm deployis a built-in pnpm command, so a package script calleddeployhas to run aspnpm run deploy.- Workers Builds doesn't read
packageManager, so setPNPM_VERSIONandNODE_VERSIONas build variables, or it uses older ones.
Discovering commands
cf has a built-in command search:
cf cli search "update a build trigger"We change Cloudflare settings from the command line rather than the dashboard.
Give localhost a public URL, free and with no account
npx hostc@latest 3000