CONTENT_REPO | Git URL or local path to the content repo. Defaults to ../carapace-content (local sibling checkout) if unset — fine for local dev, required (a real URL) for the Cloudflare Pages build, which can't see a local path. Also set this as a Cloudflare Pages project environment variable for the deployed build, not just here for local dev. | (unset) |
CONTENT_REPO_TOKEN | Fine-grained GitHub PAT, Contents: Read-only, scoped to just the content repo. Embedded into an https:// CONTENT_REPO URL for authenticated cloning — needed because the content repo is private and the Cloudflare Pages build container has no other credentials. Also set this as a Cloudflare Pages project environment variable for the deployed build. | (unset) |
SITE_URL | The site's real absolute origin — sitemap.xml, canonical tags, and OG tags all require absolute URLs, on any host. Defaults to http://localhost:3000 for local dev. Not the same thing as the old GitHub-Pages-era BASE_PATH/CUSTOM_DOMAIN (removed during the Cloudflare migration, a hosting-subpath quirk) — this is an unavoidable protocol requirement, not a hosting workaround. Must be set as a real Cloudflare Pages project environment variable for production, or sitemap/canonical/ OG URLs will silently point at localhost. | (unset) |
NODE_ENV | Gates draft/archived/future-dated post filtering (lib/posts.ts::isProductionEnv) — MUST be set to exactly "production" as a real Cloudflare Pages project environment variable, or draft/archived/scheduled content is served live with no filtering at all. Genuinely required, not a platform default: confirmed directly against Cloudflare's own docs that Pages only injects four system variables automatically (CI, CF_PAGES, CF_PAGES_COMMIT_SHA, CF_PAGES_BRANCH, CF_PAGES_URL) — NODE_ENV is never one of them. This was documented as a required setting in README's Hosting section from early on, but the actual Cloudflare Pages project never had it configured in practice — a real, live gap found and fixed 2026-07-22 (see spec §10 item 7), not a hypothetical. Verify it's actually set, don't just assume the docs describe reality. | (unset) |
MCP_PUSH_TOKEN | Optional. If set, MCP server pushes (publish/preview) authenticate as this token instead of ambient git credentials, so the server can push independently of whoever's running it. Also used by list_posts to derive editorial status (in review / changes requested) for posts with an open PR — reads via the GitHub API, so it needs "Pull requests: Read and write" (a fine-grained PAT's single permission level covers both). When unset, or origin isn't a GitHub remote, list_posts still works — those two statuses just fall back to "drafting". | (unset) |
PUBLISH_MODE | direct (default) pushes straight to main. pr pushes the draft branch and opens a real GitHub PR instead — needs MCP_PUSH_TOKEN scoped with "Pull requests: Write" in addition to "Contents: Write" (a separate fine-grained PAT permission; Contents access alone isn't enough). | direct |
CLOUDFLARE_DEPLOY_HOOK_URL | Cloudflare Pages deploy hook URL (Pages project > Settings > Builds & deployments > Deploy Hooks). Used by revalidate()/publish() to trigger a rebuild after content changes — POSTing to this URL is the entire mechanism, no separate token needed. Only needs to exist wherever the MCP server runs. | (unset) |
CLOUDFLARE_API_TOKEN | CLOUDFLARE_API_TOKEN: one Account API Token shared by deploy_status and (see below) the R2 image cache's account/project lookups. Create it at My Profile > API Tokens > Create Token > Custom Token, permission Account > Cloudflare Pages > Read. GOTCHA, cost real time to work out: Cloudflare has *two* separate token systems, both confusingly called "API tokens" in different parts of the dashboard. - "User API Tokens" (tied to your login) vs. "Account API Tokens" (tied to the account itself) — always use an Account API Token for anything automated/service-like (this project, CI, etc.), never a User one. Cloudflare's own token-creation UI recommends this too. - Separately: R2 object-level operations (get/put/list individual files) are NOT supported by Cloudflare's native REST API (api.cloudflare.com) at all, confirmed directly — an Account API Token with full "R2 Storage" permissions still gets a flat 403 no matter what permission level you pick. Object operations only work via R2's S3-compatible endpoint (below), a genuinely different auth system (AWS-style signed requests, not a Bearer token). The native API only covers R2 *bucket* management (list/create/delete buckets), not the objects inside them. | (unset) |
CLOUDFLARE_BUCKET_NAME | R2-backed image cache (site/image-cache-remote.ts) — persists the local .image-cache/ across Cloudflare Pages builds, which otherwise start from a fresh checkout every time and re-encode every image from scratch. Optional: unset means local-only caching (still works, just doesn't survive a fresh CI checkout). Uses R2's S3-compatible API (hand-rolled AWS SigV4 signing) — per the gotcha above, this is the only path that actually works for reading/ writing individual objects. Get CLOUDFLARE_S3_ACCESS_KEY_ID and CLOUDFLARE_S3_SECRET_ACCESS_KEY from R2 > Manage R2 API Tokens > Create API Token (permission: Object Read & Write, scoped to this one bucket) — a *different* page than the Account API Token above, and it produces this Access Key/Secret pair specifically, not a token string. CLOUDFLARE_BUCKET_NAME: the R2 bucket's name. CLOUDFLARE_ACCOUNT_ID is shared with the block above — the R2 endpoint is derived from it directly (https://<account-id>.r2.cloudflarestorage.com), no separate endpoint URL needs storing. | (unset) |
IMAGE_CACHE_PRUNE_AFTER_DAYS | Optional, opt-in — unset means image cache entries live in R2 forever. When set, deletes cache entries no longer referenced by any current post once they've gone unreferenced for this many days (a real "last confirmed still in use" tracking manifest, not R2's own last-modified timestamp — see site/image-cache-remote.ts::pruneImageCache for why that distinction matters). Try 30/60/90 depending on how much churn your content sees. | (unset) |
MCP_ALLOWED_HOSTNAMES | Comma-separated hostnames for DNS-rebinding protection on the MCP server. Defaults to localhost-only; set this if the server is ever exposed beyond your own machine. | (unset) |
MCP_TOOL_ALLOWLIST | Comma-separated MCP tool names. Unset means every tool this server has is registered (normal solo/interactive use). Set to restrict a server instance to a narrow subset — e.g. the docs-refresh CI workflow sets this so its agent structurally cannot call delete_post/rollback/abandon_draft, not just "is told not to" in a prompt. | (unset) |
PORT | Port for the MCP server. Defaults to 3939. | (unset) |
SITE_NAME | Site name/description used in <title>, meta tags, OG tags, the RSS feed, and llms.txt. Both optional — default to "Carapace" and a generic description. | (unset) |
CLOUDFLARE_WEB_ANALYTICS | Opt-in only, off by default — analytics is bring-your-own for this project (see spec §10 item 5's own note), not a default integration. Set to exactly "true" only if you've also enabled Cloudflare's own Web Analytics for this zone in the dashboard (Analytics & Logs > Web Analytics) — that toggle injects a beacon script at the edge, outside this site's own build, so without this flag the CSP has no idea to allow it and blocks it. Does not apply to Google Analytics or any other third-party analytics vendor — this is specifically for Cloudflare's own zone analytics. | (unset) |
PREVIEW_WORKER_URL | The deployed Preview Worker's base URL (workers/preview/, spec §10 Phase 2 item 6) — e.g. "https://carapace-preview.<subdomain>.workers.dev". Read by lib/content-ops.ts::preview() to build the returned preview URL. Optional: unset means preview() still pushes the draft branch, just returns { branch } with no url — the underlying draft-sharing mechanism works whether or not the Worker is deployed. | (unset) |
GITHUB_TOKEN | GITHUB_TOKEN — the Preview Worker's own credential, a fine-grained GitHub PAT, Contents: Read-only, scoped to just the content repo (same category as CONTENT_REPO_TOKEN above, but a *separate* credential: the Worker has its own secret store, distinct from both this file and Cloudflare Pages' env vars). For local `wrangler dev` testing, Wrangler has its own convention — a gitignored `.dev.vars` file (dotenv format, auto-loaded, NOT this file) at the repo root. For the real deployment, `wrangler secret put GITHUB_TOKEN`. Listed here purely for documentation consistency with every other credential in this file, not because anything actually reads GITHUB_TOKEN from .env.local/.env.example. | (unset) |