A Himalayan cat on a desk next to a keyboard

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

VariableAccessWhy
TURNSTILE_SECRET_KEYServer onlyVerifies tokens
RESEND_API_KEYServer onlySends mail
PUBLIC_TURNSTILE_SITE_KEYPublicRenders 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.