Production env setup checklist
Every env var the blog reads, what feature it gates, what to do if it's missing, and where to set it in Cloudflare Pages.
This is the doc that should have existed before now. Each section gates one feature; setting them turns the feature on and (in most cases) the absence is not fatal — features just no-op gracefully.
The reference values live in .env.example at the repo root. Copy to .env.local for dev, or set in Cloudflare Pages → Settings → Environment variables for prod.
Naming convention
PUBLIC_* — exposed to the client bundle. Safe to read in browser code. Never put secrets here.
Everything else — build-time and server-only. Fine for keys.
1. Comments (Giscus)
| Variable | Required? | Notes |
|---|---|---|
PUBLIC_COMMENTS_ENABLED | Yes (set true to turn on) | Master switch |
PUBLIC_GISCUS_REPO | Yes if enabled | e.g. Dax911/DaxBlogv2 |
PUBLIC_GISCUS_REPO_ID | Yes if enabled | from giscus.app |
PUBLIC_GISCUS_CATEGORY | Optional | default General |
PUBLIC_GISCUS_CATEGORY_ID | Yes if enabled | from giscus.app |
Setup steps:
- Enable Discussions on the GitHub repo.
- Visit giscus.app and walk through the wizard.
- Copy the four IDs from the generated
<script>tag into the env vars above.
2. Post signing (Ed25519 attestations)
| Variable | Required? | Notes |
|---|---|---|
POST_SIGNING_KEY | For prod only | Base64 PKCS#8 Ed25519 private key |
Setup:
bun run sign:keygen # writes a fresh keypair locally
# Commit the .pem (public key) at src/data/post-public-key.pem
# Set POST_SIGNING_KEY in CF Pages env (the base64 PKCS#8 of the private)
If unset, the prebuild script falls back to an ephemeral keypair — signatures still render but won’t verify against the public PEM. Loud warning in the build log.
3. ActivityPub federation
| Variable | Required? | Notes |
|---|---|---|
AP_PRIVATE_KEY | For prod only | Base64 PKCS#8 Ed25519 private key |
Setup: Generated by the same script:
bun run sign:keygen activitypub
Without this, the /actor endpoint serves a placeholder pubkey and remote Mastodon instances can’t follow.
4. Audio TTS (Cloudflare Workers AI + R2)
| Variable | Required? | Notes |
|---|---|---|
CF_ACCOUNT_ID | For audio | Cloudflare account ID |
CF_API_TOKEN | For audio | Token with Workers AI:Edit + R2:Edit |
R2_BUCKET_NAME | For audio | e.g. skill-issue-audio |
AUDIO_PUBLIC_URL | Optional | Default https://audio.skill-issue.dev |
AUDIO_MODEL | Optional | Default @cf/cloudflare/aura-tts |
AUDIO_VOICE | Optional | Default default |
Setup:
- R2 bucket. Create
skill-issue-audioin CF dashboard. Bindaudio.skill-issue.devas a public custom domain. - CORS. Set
Access-Control-Allow-Origin: https://blog.skill-issue.dev. - API token. Create with the two scopes above. Production scope only — preview deploys keep no-op’ing.
Without these, the prebuild’s render-audio.mjs no-ops with a friendly log line and posts render the ”[ audio coming soon ]” badge instead of an <audio> element.
5. AI Q&A (/ask)
| Variable / binding | Required? | Notes |
|---|---|---|
PUBLIC_AI_ASK_ENABLED | Optional | Default true |
Workers AI binding AI | Yes for prod | Pages → Functions → Bindings |
Setup: In Pages dashboard → Functions → Bindings, add a Workers AI binding with variable name AI. The endpoint reads env.AI.run("@cf/meta/llama-3.3-70b-fp8-fast", ...).
Without the binding, /ask returns 503 with a clear error message.
6. Rate limiting
Optional. Create a KV namespace named RATE_LIMITS and bind it in Pages Functions. Without it, /ask and /verify-signature are unrate-limited (fine for low-traffic launch; needed when traffic picks up).
7. Resume / Papers PDF
| Variable | Required? | Notes |
|---|---|---|
PUBLIC_PDF_RESUME_ENABLED | Optional | Default true |
RESUME_PDF_URL | Optional | Default /resume.pdf |
PUBLIC_PAPERS_PDF_ENABLED | Optional | Default false (skips PDF gen) |
PUPPETEER_EXECUTABLE_PATH | If papers PDF enabled | path to a Chrome/Chromium binary |
The papers PDF flag is off by default because puppeteer adds ~250MB to the build image. Turn on only when you actually need the artifact.
8. GitHub contribution graph
| Variable | Required? | Notes |
|---|---|---|
GITHUB_TOKEN | Optional | A read-only PAT (no scopes) bumps API limits |
The graph data is fetched at build time by scripts/fetch-github-graph.mjs and committed to src/data/github-graph.json. Without a token the script uses an anonymous GraphQL request — works but is rate-limited to 60/hour per IP.
9. Cloudflare Pages — non-env settings
These aren’t env vars but are gotchas worth pinning:
- Build command:
bun run build - Build output dir:
dist - Node version: the
.nvmrcsays22.11.0. CF Pages uses 22.22.0 by default which works. - Compatibility flags:
nodejs_compat(Functions setting). Without it, anything usingnode:*modules at runtime (e.g. ActivityPub key-loading) errors. - Rocket Loader: disable. It rewrites inline scripts and breaks ThemeInit + service-worker registration. Cloudflare → Speed → Optimization → Rocket Loader → Off.
Quick verify
After setting env vars, force-rebuild on CF Pages and check the build log for these lines:
[sign-posts] signed N post(s), skipped 1, wrote /opt/buildhome/repo/src/data/post-signatures.json
[render-audio] CF_ACCOUNT_ID / CF_API_TOKEN / R2_BUCKET_NAME not set — skipping
↑ this should disappear once audio env is set
The [sign-posts] WARN: POST_SIGNING_KEY not set line should disappear once the signing key is in place. If it persists, your env var name has a typo.
What hasn’t been documented yet
- Plausible analytics: configured via DOM data attributes in
BaseHead.astro, not env vars. Fine, but worth a follow-up post. - Telegram bot integration (kinkbook/pike_pupday side projects): different repo, different env, beyond this doc’s scope.
- Apple notarization (
APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_ID): for vanta-desktop release builds only, not the blog.