Guide
Secrets at the Edge
A docs-as-code sample: how to set, read, and verify runtime secrets in a Cloudflare Workers / Pages app, without leaking them into the client bundle.
Last updated 2026-06-25
Quickstart
Three steps: add a secret, read it from a function, and verify it is wired without exposing the value.
1. Add the secret
For a deployed Pages project, set it as an encrypted secret (you are prompted for the value, which is never stored in your repo):
npx wrangler pages secret put DEMO_SECRET For local development, put it in .dev.vars (which is gitignored), so wrangler pages dev can read it:
# .dev.vars
DEMO_SECRET=local-development-value 2. Read it from a Function
Secrets arrive as bindings on env. Read them at request time and never log the value:
export const onRequestGet = async ({ env }) => {
const present = typeof env.DEMO_SECRET === 'string' && env.DEMO_SECRET.length > 0;
// use env.DEMO_SECRET to sign or authorize; do not return or log it
return Response.json({ hasBinding: present });
}; 3. Verify
Hit the companion endpoint. It reports presence and length only, never the value:
curl https://www.cloudflarecowboy.com/api/secrets-demo
# { "ok": true, "binding": "DEMO_SECRET", "hasBinding": true, "length": 21 } Concepts
Build-time variables vs runtime secrets
Anything prefixed for the client (for example a PUBLIC_ variable) is baked into the
JavaScript bundle and is readable by anyone. That is fine for non-sensitive config and wrong for
secrets. A real secret is a runtime binding: it exists only on the server at request
time and is never shipped to the browser.
Least privilege
Scope each secret to the smallest surface that needs it. A single central function that calls a third party should hold that provider key, rather than spreading the same key across every route.
Rotation
Treat rotation as routine, not an incident. Because the value lives in one binding, rotating is
updating the secret and redeploying. Code that reads env.DEMO_SECRET does not change.
Never log a secret
Logs, error messages, and analytics are the most common leak path. Report presence and length for health checks, never the value.
API reference
GET /api/secrets-demo returns the binding status. It never returns the secret value.
| Field | Type | Description |
|---|---|---|
ok | boolean | Request succeeded. |
binding | string | The binding name checked (DEMO_SECRET). |
hasBinding | boolean | Whether the secret is set and non-empty. |
length | number | Character length of the value, or 0. Never the value itself. |
note | string | Human-readable next step. |
Troubleshooting
hasBinding is false in production
The secret was not set for the deployed project, or you set it but have not redeployed. Run wrangler pages secret put DEMO_SECRET and trigger a new deploy.
It is undefined locally
Add it to .dev.vars and run wrangler pages dev ./dist. Plain astro dev does not run Functions, so the endpoint will 404.
The value showed up in the client
It was a build-time variable, not a runtime secret. Move it out of any client-exposed config and read it from env in a Function instead.