This note explains how to wire Cloudflare Pages to a Worker with service bindings, how the relevant files fit together, how to handle multiple environments, and what
workers_devactually does.
The short version
If your stack looks like this:
Pages serves the frontend
Workers serves the API and
/auth/*Pages calls the Worker through a service binding
then these three conditions must hold:
The Pages project must declare
[[services]]If you define
[env.production], you must also declare[[env.production.services]]The Worker
namemust match theservicevalue used by Pages
If any of those is missing, the Pages Function can end up with an empty context.env, which means your binding is absent at runtime.
What each file is responsible for
apps/web/wrangler.toml
This is the source of truth for the Pages project. It defines:
the Pages project name
the static output directory
service bindings
environment variables
apps/web/functions/[[path]].ts
This is the Pages Functions entrypoint. It:
intercepts
/api/*intercepts
/auth/*forwards the request to the Worker via a service binding
apps/api/wrangler.toml
This is the Worker project configuration. It defines:
the Worker name
development and production environment variables
D1 / KV / R2 bindings
whether the Worker keeps a
workers.devdefault domain
Suggested project layout
A clean split usually looks like this:
apps/
web/
wrangler.toml
functions/[[path]].ts
src/
api/
wrangler.toml
src/In this setup:
webowns Pages and request forwardingapiowns business logic and authentication
Pages-side configuration
1. Top-level config
apps/web/wrangler.toml
name = "react-hono-starter"
pages_build_output_dir = "dist"
compatibility_date = "2024-09-25"
# Bind to the API worker - proxies /api/* and /auth/* without a network hop
[[services]]
binding = "WORKER"
service = "react-hono-starter-api"
[vars]
# Public vars accessible in the browserWhat this means:
nameis the Pages project namepages_build_output_diris the build output directory[[services]]creates the service binding available to Pages Functionsbinding = "WORKER"meanscontext.env.WORKERservice = "react-hono-starter-api"must match the actual Worker script name
2. Production config
If you define [env.production], you must treat it as its own environment block.
Some keys are inheritable, some are not.services belongs to the set that should be declared explicitly in the target environment.
So production should also include:
[env.production]
vars = {}
[[env.production.services]]
binding = "WORKER"
service = "react-hono-starter-api"This is the piece that matters most when a project works locally but the production Pages deployment has no binding at runtime.
Pages Functions entrypoint
apps/web/functions/[[path]].ts
interface Env {
WORKER: Fetcher;
}
export const onRequest: PagesFunction<Env> = (ctx) => {
const { pathname } = new URL(ctx.request.url);
if (pathname.startsWith('/api/') || pathname.startsWith('/auth/')) {
if (!ctx.env.WORKER) {
return new Response('Missing worker service binding', { status: 500 });
}
return ctx.env.WORKER.fetch(ctx.request);
}
return ctx.next();
};Two details matter here:
ctx.env.WORKERdoes not exist automatically; it comes fromwrangler.toml/api/*and/auth/*are just forwarded to the Worker
So the Pages Function is a transport layer, not the business layer.
Worker-side configuration
apps/api/wrangler.toml
name = "react-hono-starter-api"
main = "src/worker.ts"
compatibility_date = "2024-09-25"
compatibility_flags = ["nodejs_compat"]
workers_dev = false
[dev]
port = 8013The important part is:
name = "react-hono-starter-api"Pages
servicemust match this nameworkers_dev = falseonly affects the default*.workers.devdomain
The rest of the Worker config covers its own bindings:
D1
KV
R2
secrets and environment variables
Those bindings do not directly affect the Pages service binding, but they do affect whether the Worker can process requests successfully once the binding is in place.
Multi-environment setup
One Worker, multiple environments
This fits the current project well:
[env.production]
vars = { ENVIRONMENT = "production", IS_DEBUG = "false" }
[[env.production.services]]
binding = "WORKER"
service = "react-hono-starter-api"You can also define a development environment:
[env.dev]
vars = { ENVIRONMENT = "development", IS_DEBUG = "true" }
[[env.dev.services]]
binding = "WORKER"
service = "react-hono-starter-api"Separate production and dev Workers
If you want a layout closer to sso-serverless, you can split the bindings:
[[services]]
binding = "BACKEND_PROD"
service = "react-hono-starter-api"
[[services]]
binding = "BACKEND_DEV"
service = "react-hono-starter-api-dev"Then choose the binding at runtime:
const backend = hostname.includes('localhost') ? env.BACKEND_DEV : env.BACKEND_PROD;
return backend.fetch(request);This is useful when:
production and preview environments are separate
you have multiple domains
you want explicit environment selection
What workers_dev does
workers_dev is a Worker-level switch.
When enabled
The Worker gets a default *.workers.dev public URL.
When disabled
The Worker no longer exposes that default public URL.
What it does not do
It does not:
create Pages service bindings
make
context.env.WORKERappearfix a missing binding in Pages
When to disable it
If the Worker is only meant to be called internally by Pages, and you do not want an extra public endpoint, then disabling it is a good idea:
workers_dev = falseThis reduces exposure and keeps the deployment model explicit.
Why the issue happened
The failure was configuration-related, not code-related.
The live Pages project initially did not carry the service binding into the production environment, so:
the Pages Function still executed
context.envhad noWORKERrequests to
/auth/meand/auth/loginfailed immediately with a missing binding
The fix is:
Declare top-level
[[services]]Declare
[[env.production.services]]Keep the Worker
nameand PagesservicealignedRedeploy Pages
Minimal working template
apps/web/wrangler.toml
name = "react-hono-starter"
pages_build_output_dir = "dist"
compatibility_date = "2024-09-25"
[[services]]
binding = "WORKER"
service = "react-hono-starter-api"
[env.production]
vars = {}
[[env.production.services]]
binding = "WORKER"
service = "react-hono-starter-api"apps/web/functions/[[path]].ts
interface Env {
WORKER: Fetcher;
}
export const onRequest: PagesFunction<Env> = (ctx) => {
const { pathname } = new URL(ctx.request.url);
if (pathname.startsWith('/api/') || pathname.startsWith('/auth/')) {
return ctx.env.WORKER.fetch(ctx.request);
}
return ctx.next();
};apps/api/wrangler.toml
name = "react-hono-starter-api"
main = "src/worker.ts"
compatibility_date = "2024-09-25"
compatibility_flags = ["nodejs_compat"]
workers_dev = falseSummary
In a Cloudflare Pages + Workers architecture, the most common failure mode is not code, but configuration inheritance.
Keep these three terms in mind:
service bindingenv.productionworkers_dev
The first two determine whether Pages can forward requests to the Worker. The last one only controls whether the Worker has a default workers.dev public endpoint.
Download this blog in MD format: https://blog-media.ropean.org/2026/06/efd544d8-ff24-4a6e-8ab4-1b22dec2df3d.md