Astro secrets on Cloudflare Workers without leaking to the client
Contact forms on Astro + Cloudflare need secrets at request time: Turnstile secret, Resend API key, inbox addresses. Those must never ship in client JavaScript.
Astro’s astro:env helps, but on Workers I also read bindings from cloudflare:workers when Wrangler injects dashboard secrets (Astro 6 + @astrojs/cloudflare v13).
One helper, two sources
import { env } from 'cloudflare:workers';
import { getSecret } from 'astro:env/server';
export function readEnv(key: string): string | undefined {
const fromWorker = env[key as keyof typeof env];
if (typeof fromWorker === 'string' && fromWorker !== '') {
return fromWorker;
}
const fromAstro = getSecret(key);
if (typeof fromAstro === 'string' && fromAstro !== '') {
return fromAstro;
}
return undefined;
}
Use it in API routes and SSR pages:
const apiKey = readEnv('RESEND_API_KEY');
What goes where
| Variable | Access | Why |
|---|---|---|
TURNSTILE_SECRET_KEY | Server only | Verifies tokens |
RESEND_API_KEY | Server only | Sends mail |
PUBLIC_TURNSTILE_SITE_KEY | Public | Renders the widget |
Public keys can use readEnv too, but only secrets belong in Wrangler Secrets, not in the repo.
Common mistake
Defining a secret in .env locally, deploying without adding it in the Cloudflare dashboard, then wondering why production returns 503. The helper is fine; the binding is missing.
Set production secrets under Workers → your service → Settings → Variables and Secrets, or use wrangler secret put.
Pair this with Local .dev.vars vs production secrets for day-one setup and the Turnstile + Resend contact form guide for a full example.
Hero photo: Bluna the Himmy and his keyboard, CC BY-SA 2.0.