Skip to content

Deploy to Cloudflare Workers

Stoma has first-class Cloudflare Workers support. Deploy your gateway to the edge with full access to Service Bindings, KV, Durable Objects, and the Cache API.

Terminal window
npm install -g wrangler
wrangler login
  1. Install dependencies

    Terminal window
    npm install hono @homegrower-club/stoma
  2. Create your gateway

    For a minimal example, see the Basic Gateway partial. Here’s a typical production setup with the Cloudflare adapter:

    src/index.ts
    import {
    createGateway,
    cors,
    requestLog,
    rateLimit,
    } from "@homegrower-club/stoma";
    import { cloudflareAdapter } from "@homegrower-club/stoma/adapters/cloudflare";
    const gateway = createGateway(
    {
    name: "my-gateway",
    basePath: "/api",
    policies: [requestLog(), cors()],
    routes: [
    {
    path: "/users/*",
    pipeline: {
    policies: [rateLimit({ max: 100, window: 60 })],
    upstream: {
    type: "url",
    target: "https://api.example.com",
    rewritePath: (path) => path.replace("/api", ""),
    },
    },
    },
    ],
    },
    // Production adapter with KV rate limiting and Cache API support
    cloudflareAdapter({ env })
    );
    export default gateway.app;
  3. Configure wrangler

    wrangler.toml
    {
    "name": "my-gateway",
    "compatibility_date": "2025-01-01",
    "compatibility_flags": ["nodejs_compat"],
    "main": "src/index.ts"
    }
  4. Deploy

    Terminal window
    wrangler deploy
wrangler.toml
{
"name": "my-gateway",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"main": "src/index.ts",
"observability": {
"enabled": true
}
}
OptionDescription
nameWorker name (used in URL: <name>.workers.dev)
compatibility_dateUse a recent date for latest features
compatibility_flagsnodejs_compat enables Node.js compatibility
mainEntry point for your Worker

Configure multiple environments for staging and production:

wrangler.toml
{
"name": "my-gateway",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"env": {
"staging": {
"routes": [
{ "pattern": "staging.myapp.com/*", "zone_name": "myapp.com" }
]
},
"production": {
"routes": [
{ "pattern": "api.myapp.com/*", "zone_name": "myapp.com" }
]
}
}
}

Deploy to a specific environment:

Terminal window
wrangler deploy --env staging
wrangler deploy --env production

Store sensitive values securely:

Terminal window
# Interactive - will prompt for value
wrangler secret put JWT_SECRET
# Or pipe from stdin
echo "my-secret-value" | wrangler secret put API_KEY

Access secrets via the env parameter:

src/index.ts
interface Env {
JWT_SECRET: string;
API_KEY: string;
DATABASE_URL: string;
}
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const gateway = createGateway({
// Use env.JWT_SECRET here
});
return gateway.app.fetch(request, env, ctx);
},
};
Secret TypeUse Case
wrangler secret putRuntime secrets (API keys, tokens)
wrangler kv:namespace createKV namespace binding
wrangler d1 createD1 database binding
wrangler r2 bucket createR2 storage binding

Use the Cloudflare adapter for Service Bindings, KV rate limiting, and Durable Objects:

src/index.ts
import {
createGateway,
cors,
rateLimit,
cache,
circuitBreaker,
} from "@homegrower-club/stoma";
import { cloudflareAdapter } from "@homegrower-club/stoma/adapters/cloudflare";
interface Env {
JWT_SECRET: string;
RATE_LIMIT_KV: KVNamespace;
CACHE_KV: KVNamespace;
USERS_SERVICE: Fetcher;
}
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const adapter = cloudflareAdapter({
rateLimitKv: env.RATE_LIMIT_KV,
cacheKv: env.CACHE_KV,
executionCtx: ctx,
env,
});
const gateway = createGateway({
name: "my-gateway",
basePath: "/api",
adapter,
policies: [cors()],
routes: [
{
path: "/users/*",
pipeline: {
policies: [
rateLimit({
max: 100,
windowSeconds: 60,
store: adapter.rateLimitStore,
}),
],
upstream: {
type: "service-binding",
service: "USERS_SERVICE",
},
},
},
{
path: "/products/*",
pipeline: {
policies: [
cache({
ttlSeconds: 300,
store: adapter.cacheStore,
}),
],
upstream: {
type: "url",
target: "https://products.internal",
},
},
},
],
});
return gateway.app.fetch(request, env, ctx);
},
};

Automate deployments with GitHub Actions:

.github/workflows/deploy.yml
name: Deploy to Cloudflare Workers
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --env production
- name: Deploy to staging
if: github.event_name == 'pull_request'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --env staging

Add these secrets to your GitHub repository:

SecretHow to Get
CLOUDFLARE_API_TOKENCloudflare Dashboard → Profile → API Tokens → Create Token (use “Edit Cloudflare Workers” template)
CLOUDFLARE_ACCOUNT_IDCloudflare Dashboard → Overview → Account ID
Terminal window
# Add a custom domain
wrangler routes update --pattern "api.example.com" --zone-name "example.com"

Or in wrangler.toml:

{
"routes": [
{ "pattern": "api.example.com/*", "zone_name": "example.com" }
]
}

For complex frontends with a gateway, consider Cloudflare Pages:

Terminal window
# Create a Pages project
wrangler pages project create my-gateway
wrangler pages deploy dist/ --project-name my-gateway

“Module not found” errors

Ensure nodejs_compat is in your compatibility_flags:

{
"compatibility_flags": ["nodejs_compat"]
}

KV namespace not found

Bind the KV namespace in wrangler.toml:

{
"kv_namespaces": [
{ "binding": "RATE_LIMIT_KV", "id": "your-namespace-id" }
]
}

Service Binding not working

Ensure the target Worker is deployed and accessible:

{
"services": [
{ "binding": "USERS_SERVICE", "service": "users-worker" }
]
}