Cloudflare Cowboy
← The Forge

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.

FieldTypeDescription
okbooleanRequest succeeded.
bindingstringThe binding name checked (DEMO_SECRET).
hasBindingbooleanWhether the secret is set and non-empty.
lengthnumberCharacter length of the value, or 0. Never the value itself.
notestringHuman-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.

How it works

A documentation sample for setting, reading, and verifying runtime secrets without leaking them to the client. The companion endpoint reports a secret’s presence and length — never its value.

🔑
Secret
wrangler / dashboard
set
⚙️
Edge runtime
env binding
bound at runtime
Verify endpoint
presence + length
hasBinding: true
Value never returned

Key decisions

Presence, not value
The verify endpoint reports only whether a secret is bound and its length, proving the wiring without exposing anything.
Server-side only
Secrets stay in the edge runtime env and never reach the client bundle.
Docs-as-code
Quickstart, concepts, reference, and troubleshooting live in one page, versioned alongside the code they document.
Built with:Pages Functionswrangler secrets.dev.vars