Setup manifest
September 19, 2026 · View on GitHub
This document describes the infrastructure and secrets that kody expects.
Cloudflare resources
This project uses the following resources:
- D1 database
database_name:<app-name>
- R2 bucket for the self-hosted Nx remote cache
bucket_name:kody-nx-cache- Worker:
kody-nx-cacheatnx-cache.kody.codes(packages/nx-cache) - Production deploy ensures the bucket and a 14-day
v1/lifecycle (1-day incomplete multipart abort) when the cache worker path changes or that workflow is dispatched onmain. The dedicated.github/workflows/nx-cache-deploy.ymlworkflow does the same on its ownworkflow_dispatchwithout a full production deploy.
- KV namespace for OAuth/session storage
binding:OAUTH_KVtitle:<app-name>-oauth
- Cloudflare Email Sending / Email Service Worker binding
binding:EMAILwranglerkey:send_email- Production domains need Cloudflare-side sender/domain verification before sends succeed.
- Cloudflare Email Routing for inbound mail
- Configure MX records and selected route aliases in the Cloudflare dashboard.
- Route only aliases that should persist inbound mail to the Worker.
- Cloudflare Queue for Email Sending delivery events
- Production CI ensures
kody-email-deliveryandkody-email-delivery-dlq, then reconciles anemail.sendingevent subscription for the configured user email domain. - The API token needs
Workers Queues:Edit; the domain must already be enabled for Cloudflare Email Sending.
- Production CI ensures
- Cloudflare Queue for Artifacts repository lifecycle events
- Queue:
kody-artifacts-repo-events - Dead-letter queue:
kody-artifacts-repo-events-dlq - Production CI ensures both queues and reconciles an account-level
artifactsevent subscription forrepo.created/repo.deleted/repo.pushed. New repos do not create per-repoartifacts.repopush subscriptions; leftover per-repo rows are still deleted during artifact cleanup. UsesCLOUDFLARE_ACCOUNT_ID+CLOUDFLARE_API_TOKEN. - Production and preview Workers bind
ARTIFACTS(wranglerartifacts, namespacesproduction/preview). Create/get prefer that JSRPC binding and fall back to REST when the binding is absent (local/tests). - The production consumer batches at most 10 messages for 5 seconds, retries
three times, and routes exhausted messages to the dedicated dead-letter
queue. Consumers filter by
ARTIFACTS_NAMESPACEand ignore session fork repos, session workspace branch pushes (sessions/<id>), and publish git-notes (refs/notes/commits). Mapped package topics:repo.pushed,repo.created,repo.deleted.
- Queue:
- Cloudflare Queue for durable platform-feedback subscription dispatch
- Producer binding:
PLATFORM_FEEDBACK_DISPATCH_QUEUE - Queue:
kody-platform-feedback-dispatch - Dead-letter queue:
kody-platform-feedback-dispatch-dlq - The production consumer batches at most 10 messages for 5 seconds, retries three times, and routes exhausted messages to the dedicated dead-letter queue. Production CI ensures both resources.
- Queue messages contain only
{ feedbackId }. The consumer reloads current feedback metadata, acknowledges invalid or deleted ids, and retries transient load, subscription-discovery, or package-invocation wrapper infrastructure failures before routing exhausted messages to the DLQ. Stored failures replay under the same idempotency key rather than automatically rerunning; terminal handler execution failures stay isolated.
- Producer binding:
- Cloudflare Queue for durable community-activity subscription dispatch
- Producer binding:
COMMUNITY_ACTIVITY_DISPATCH_QUEUE - Queue:
kody-community-activity-dispatch - Dead-letter queue:
kody-community-activity-dispatch-dlq - The production consumer uses the same batch, retry, and DLQ settings as platform-feedback dispatch. Production CI ensures both resources.
- Queue messages contain only
{ eventId, kind, activityId }. The consumer reloads the metadata-only activity projection, acknowledges invalid or deleted activity, and retries transient lookup, subscription-discovery, or package-invocation infrastructure failures.
- Producer binding:
- Cloudflare Queue for durable community-listing-published subscription dispatch
- Producer binding:
COMMUNITY_LISTING_PUBLISHED_DISPATCH_QUEUE - Queue:
kody-community-listing-published-dispatch - Dead-letter queue:
kody-community-listing-published-dispatch-dlq - The production consumer uses the same batch, retry, and DLQ settings as platform-feedback dispatch. Production CI ensures both resources.
- Queue messages contain only
{ eventId, listingId }. The consumer reloads the metadata-only listing projection, acknowledges invalid or inactive listings, and retries transient lookup, subscription-discovery, or package-invocation infrastructure failures. Only first publish enqueues; republish does not.
- Producer binding:
- Cloudflare Queue for durable package-emitted event dispatch
- Producer binding:
PACKAGE_EVENTS_DISPATCH_QUEUE - Queue:
kody-package-events-dispatch - Dead-letter queue:
kody-package-events-dispatch-dlq - The production consumer uses the same batch, retry, and DLQ settings as platform-feedback dispatch. Production CI ensures both resources.
- Queue messages carry the full event (emitting user, source package, topic, idempotency key, payload, and invocation depth). The consumer resolves the emitting user's subscribed packages at delivery time, invokes handlers with exactly-once idempotency, acknowledges terminal handler failures, and retries pre-execution package-invocation infrastructure failures.
- Preview and local runtimes without this production-only queue binding deliver inline through the same consumer code path so package events remain testable.
- Producer binding:
- Cloudflare Queue for isolated scheduled maintenance
- Producer binding:
SCHEDULED_DISPATCH_QUEUE - Queue:
kody-scheduled-dispatch - Dead-letter queue:
kody-scheduled-dispatch-dlq - The production consumer receives one lane message per invocation, permits up
to 16 concurrent lane invocations, and retries only replay-safe
d1_lock_contentionoutcomes three times (10s / 30s / 90s backoff) before routing exhausted messages to the dedicated dead-letter queue. Other handled lane failures and invalid bodies are acknowledged as terminal. Production CI ensures both resources. - Preview and local runtimes without this production-only queue binding run the same registry inline so maintenance behavior remains testable.
- Producer binding:
- Vectorize indexes for MCP capability search (
CAPABILITY_VECTOR_INDEX)- Production:
kody-capabilities-prod - Preview:
kody-capabilities-preview - Create once per account, for example:
wrangler vectorize create kody-capabilities-prod --dimensions=384 --metric=cosine(same for preview). Dimensions must match the embedding model inpackages/worker/src/vectorize/embedding.ts(@cf/baai/bge-small-en-v1.5, 384 dimensions,clspooling).
- Production:
- Cloudflare Images binding for package-icon and integration-logo ingest
binding:IMAGES- Origin (
packages/worker/wrangler.jsonc) and platform (packages/platform-worker/wrangler.jsonc) bind the same Images API. Derived community icons and OAuth / MCP logos are fitted to 256px WebP at ingest. No extra Cloudflare resource to create; local wrangler uses the offline simulator.
- Workers AI binding for semantic search embeddings and ranked-search scoring
binding:AI- Production and preview route embedding and Jev Score (
typesafe/jev) calls through this binding. WhenAI_GATEWAY_IDis configured, calls are sent through AI Gateway via the Workers AI binding options.typesafe/jevrequires that gateway (authentication plus Unified Billing credits or BYOK); embeddings call Workers AI directly when the id is unset.
- Second registrable domain for hosted package apps
- Production:
kody.run(zone in the same Cloudflare account, on Cloudflare nameservers), served by the runtime Worker via zone routes plus proxied placeholder DNS records — deliberately not a Workers custom domain. Per-user package apps are served fromhttps://{username}.kody.run/packages/{kodyId}/...; the apex serves redirects (/@user/packages/...→ per-user subdomain,/to the app origin).__Host-kody_pkg_sessionis host-only, so each package-app zone has its own sessions. - No custom domain in this zone. The deploy publishes this zone's Worker route table, and replacing a zone's routes detaches any Workers custom domain in that zone and deletes its DNS record. Package-app hosts therefore use zone routes plus proxied placeholder DNS only. Custom domains stay reserved for the app-origin zones, whose route tables the deploy never publishes.
- DNS records (idempotent, per deploy). Zone routes do not create DNS
records. Production CI (
tools/ci/production-resources.ts ensure) idempotently ensures proxied records for both names in every package-app zone (canonical plus anyPACKAGE_APP_LEGACY_HOSTS):kody.runand*.kody.run→ AAAA100::(orange-cloud proxied). The deploy token needs DNS:Edit on those zones (in addition to Workers deploy permissions). Forks must create the zone and either runensureor add the records manually before the first per-user-subdomain deploy. - Zone routes.
tools/ci/runtime-worker-config.tspublishes{ pattern: "kody.run/*", zone_name: "kody.run" }and{ pattern: "*.kody.run/*", zone_name: "kody.run" }on the runtime Worker. Cloudflare Universal SSL covers one wildcard label (*.kody.run), which is enough for{username}.kody.run. Do not attach*kody.run/*tokody-domain-redirect: that wildcard would shadow the runtime Worker.www.kody.run/*is the only redirect route on this zone (301 to the app origin), matchingwww.kody.codes. - The attach happens on deploy, but the routes are generated, not
committed:
writeGeneratedWranglerConfigderives onecustom_domainroute per app-origin var (APP_BASE_URLandAPP_LEGACY_HOSTS) while writingpackages/worker/wrangler-production.generated.json; the package-app zone routes are generated into the runtime Worker config. Those vars are the single source of truth for both the hosts the Workers route on and the domains the deploy attaches, so the two cannot drift. routesreplaces the Worker's whole route set — it does not add to it. Omitting an attached custom domain detaches that origin and deletes its DNS record. The generator therefore always lists the app origin alongside the package-app apex and wildcard zone route, and fails the deploy whenPACKAGE_APP_BASE_URLis set withoutAPP_BASE_URLrather than publishing a partial set. Any domain attached out-of-band must be added here before the next deploy, or that deploy will remove it. List every dual-served legacy app host in theAPP_LEGACY_HOSTSrepository variable (comma-separated bare hostnames) so generated routes keep those origins attached. List every dual-served package-app host inPACKAGE_APP_LEGACY_HOSTSso the runtime Worker publishes matching apex and wildcard zone routes alongsidePACKAGE_APP_BASE_URL. Production leaves both lists unset.- Publishing routes also flips
workers_devtofalse, which silently drops the<name>.<subdomain>.workers.devtrigger (Cloudflare then answers that hostname with error 1042). The generator setsworkers_dev: truealongside the routes so that backup access path — which MCP clients may point at, and which the deploy's URL fallback looks for — survives. - The routes deliberately do not live in
packages/worker/wrangler.jsonc:npm run devrunswrangler devagainst the production environment, and Wrangler resolves local request URLs against the first configured route, so a committed route makes every local request arrive ashttp://kody.run/...— canonical URLs, OAuth resource metadata, and login redirects then point at the production domain from localhost. - Attaching a custom domain needs a deploy token with edit access to the target zone. Without it the deploy step fails on the custom domain instead of silently skipping it.
- The domain exists to be a different registrable domain from the app origin so author-supplied package code is cross-site. Do not point it at a subdomain of the app origin, and do not host anything first-party on it. See Hosted package app origin isolation.
- Production forks must register a second domain and set
PACKAGE_APP_BASE_URL; package-app requests return500when it is missing or not on a separate registrable domain. Confirmed local, preview, and test runtimes may leave it unset and use inline serving.
- Production:
- Workers Observability OTLP destination (account-level)
-
Workers automatic tracing is enabled via
observability.tracesinpackages/worker/wrangler.jsonc; traces are viewable in the Workers Observability dashboard with no further setup. -
Production traces are additionally exported to Sentry through the account-level Traces destination
sentry-otlp-traces, referenced fromobservability.traces.destinationsin the production environment only (preview and test deploys keep dashboard-only tracing). In the production Cloudflare account it points at thekody-cloudflareSentry project's OTLP endpoint (https://<HOST>/api/<PROJECT_ID>/integration/otlp/v1/traceswith thex-sentry-auth: sentry sentry_key=<public key>header derived from the project DSN). -
Forks must create their own destination under the same name, or remove the
destinationsline — deploys can fail on unknown destinations. Create it in the dashboard (Workers Observability → Destinations, type Traces) or via the API:curl -X POST \ "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/workers/observability/destinations" \ -H "Authorization: Bearer <API_TOKEN>" \ -H "Content-Type: application/json" \ -d '{ "name": "sentry-otlp-traces", "enabled": true, "configuration": { "type": "logpush", "logpushDataset": "opentelemetry-traces", "url": "https://<HOST>/api/<PROJECT_ID>/integration/otlp/v1/traces", "headers": { "x-sentry-auth": "sentry sentry_key=<public key>" } } }' -
Production pins
SENTRY_TRACES_SAMPLE_RATEto0(Wrangler var) so the Sentry SDK does not duplicate the exported traces; see environment-variables.md.
-
The checked-in
packages/worker/wrangler.jsonc
declares bindings and names but does not commit remote D1 database_id or
KV id / preview_id, so forks do not accidentally bind to another project’s
resources.
Production CI deploys ensure these resources exist (create when missing) and
write resolved IDs into packages/worker/wrangler-production.generated.json
before migrations and deploy. Preview deploys do the same per preview worker via
packages/worker/wrangler-preview.generated.json (see
docs/contributing/setup/local-development.md).
Cloudflare deploys do not auto-create these resources from bindings alone, so
the deploy workflow runs node tools/ci/production-resources.ts ensure first.
Disaster-recovery control plane
Production backup resources (R2 bucket locks/lifecycle) are provisioned
out-of-band with tools/ci/backup-resources-cli.ts. The control-plane
Worker/Workflows under packages/backup-control-plane/ live in the
independently administered DR Cloudflare account. Its BACKUP_BUCKET R2 binding
is private. Immutable prefixes include daily/, weekly/, daily/full/, and
content-addressed blobs/sha256/.
Code deploys are automated by the production deploy workflow
(.github/workflows/deploy.yml job deploy-backup-control-plane) when a main
push changes packages/backup-control-plane/ or packages/shared/src/backup-*,
and on every manual workflow_dispatch of that workflow. The job uses
DR_DEPLOY_TOKEN + DR_BACKUP_ACCOUNT_ID (never the production-account
CLOUDFLARE_API_TOKEN) and sets BUILD_COMMIT to the deploy SHA. Worker
secrets on the control plane remain one-time / out-of-band.
The production Worker also stages non-D1 canonical stores into the same bucket
when DR_EXPORT_ENABLED=true (StorageRunner dumps, EMAIL_BLOBS /
COMMUNITY_ASSETS blobs, published BUNDLE_ARTIFACTS_KV source snapshots).
REPO_SESSION_BLOBS is ephemeral RepoSession Workspace scratch and is not
exported. The control plane seals complete days and hosts the Access-protected
Admin UI for drills and graduated production restore. See
Disaster recovery.
Use a separate provisioner token (never a Worker secret) to create the bucket and apply 35-day daily and 400-day weekly lock/lifecycle rules:
node tools/ci/backup-resources-cli.ts plan \
--source-account-id "<PRODUCTION_ACCOUNT_ID>" \
--destination-account-id "<DR_ACCOUNT_ID>" \
--source-d1 "<PRODUCTION_D1_UUID>:kody" \
--deny-production-resource kody-email-blobs \
--deny-production-resource kody-community-assets \
--deny-production-resource kody-repo-session-blobs
apply is an explicit mutation and must be run only after reviewing the plan.
The control-plane runtime receives a source-account token with Cloudflare
Account D1 Edit as CLOUDFLARE_API_TOKEN (export + production D1 import).
Cloudflare grants this permission account-wide and it can mutate D1; the
runtime's UUID/name allowlist reduces mistakes but does not scope the token.
Keep that token separate from the provisioner token, drill token
(DRILL_API_TOKEN), and production→DR S3 credentials on the app Worker.
Scheduling remains inert until the blocking-export benchmark is approved and
both ENABLE_PRODUCTION_D1_BACKUPS and BACKUP_BENCHMARK_APPROVED are exactly
"true".
Reviewed non-secret control-plane vars include manifest signing key id /
verifying public key, trusted restore baseline id/digest, Access
(ACCESS_TEAM_DOMAIN, ACCESS_APP_AUD, ACCESS_ALLOWED_EMAIL),
DRILL_ACCOUNT_ID, and PRIMARY_WORKER_ORIGIN. Worker secrets include
BACKUP_MANIFEST_SIGNING_PRIVATE_KEY_PKCS8_BASE64, DRILL_API_TOKEN,
RESTORE_CONFIRM_SECRET, and DR_RESTORE_SECRET. Never commit private keys.
Offline CLI restore still trusts only the checked-in manifest public-key,
production-identity, and restore-baseline registries.
Status page worker
The public status page (packages/status/, served at status.kody.codes via a
wrangler custom domain on the kody.codes zone) is an independently deployed
Worker with a cron trigger and one StatusStore Durable Object (SQLite). It
probes public endpoints on the origin worker (/health, /health/components)
and package-runtime liveness on kody.run (/__runtime/health), and probes the
jobs worker over a service binding — never through the origin app and never via
a public jobs hostname. It does not probe kody-platform HTTP (kody-platform
is health-only and not a public product surface). It never touches APP_DB (see
decision record 0004).
Component probes do not use the status hostname.
Code deploys are automated by the production deploy workflow
(.github/workflows/deploy.yml job deploy-status-worker) when a main push
changes packages/status/, and on every manual workflow_dispatch of that
workflow. The job deploys with the production-account CLOUDFLARE_API_TOKEN,
sets BUILD_COMMIT to the deploy SHA, and syncs the same token as the Worker
secret CLOUDFLARE_API_TOKEN so the status worker can send operator alert email
through the Cloudflare Email REST API (from ALERT_EMAIL_FROM to
ALERT_EMAIL_TO, both non-secret vars in packages/status/wrangler.jsonc).
Without that secret, alert sends are skipped and logged.
MCP execute evidence on the status page is a timestamp-only last-success
heartbeat from real authenticated execute completions, plus at most one hourly
synthetic when the last organic success is older than a minute. The public card
is "recent · organic" when that heartbeat is younger than one hour — organic
traffic alone keeps it green. Public status GETs and origin GET /health never
trigger that execute. When the status worker's stored last-success is already
older than a minute, public / and /status.json re-read origin
GET /health/components executeEvidence so the card can pick up a newer
heartbeat without waiting for the next cron write. The optional origin Worker
secret MCP_EXECUTE_HEALTH_CANARY_ACCESS_TOKEN is a dedicated canary OAuth
access token for the synthetic; when it is unset the fallback stays unknown
rather than impersonating a customer, and the card still stays recent from
organic traffic within the hour.
An optional Worker secret STATUS_INCIDENT_EVENT_SECRET (synced from the
same-named GitHub Actions secret when present) is shared with the main worker.
On incident open or resolve the status worker POSTs metadata to
https://kody.codes/__maintenance/status-incidents so admin packages can
subscribe to status.incident.opened / status.incident.resolved. The notify
is fire-and-forget with a short timeout so a down or missing secret cannot stall
probes or email. The same bearer authenticates
POST https://status.kody.codes/__maintenance/incidents/:id/retrospective,
which attaches an operator writeup to a resolved probe-derived incident on the
status Durable Object. That write stays on the status worker so origin /
APP_DB being down cannot block it. When the GitHub secret is unset, emit stays
skipped and the retrospective route returns 503; packages can reconcile incident
open/resolve from the public /status.json snapshot.
Optional Cloudflare offerings
The default footprint stays intentionally small. If you want to add additional Cloudflare offerings (R2, Workers AI, AI Gateway, or a separate KV namespace for app data), see:
docs/contributing/cloudflare-offerings.md
Rate limiting (Cloudflare dashboard)
Use Cloudflare's built-in rate limiting rules instead of custom Worker logic.
- Open the Cloudflare dashboard for the zone that routes to your Worker.
- Go to
Security→WAF→Rate limiting rules(orRules→Rate limiting rules). - Create a rule that targets auth endpoints, for example:
- Expression:
(http.request.method eq "POST" and http.request.uri.path in {"/auth" "/oauth/authorize" "/oauth/token" "/oauth/register"}) - Threshold:
10requests per1 minuteper IP (tune as needed). - Action:
BlockorManaged Challenge.
- Expression:
Environment variables
Local development uses packages/worker/.env, which Wrangler loads
automatically:
COOKIE_SECRET(generate withopenssl rand -hex 32)SECRET_STORE_KEY(required; generate withopenssl rand -base64 48)OIDC_SIGNING_KEY_ID(non-empty JWTkidfor MCP OIDC ID tokens; production should use a deployment-specific value such askody-oidc-2026-09)OIDC_SIGNING_PRIVATE_KEY_PEM(PKCS#8 RSA private key PEM for RS256 ID tokens; generate withopenssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out oidc.pem. Local dev and tests use the committed example key in.env.example; production must set a dedicated secret viasync-worker-secrets)APP_BASE_URL(optional; used as the fallback public origin when no request URL is available — e.g. workflows and email. Examplehttps://kody.codes. Most request-scoped app/MCP URLs use the inbound request origin so OAuth metadata matches the host the client connected to. Password reset email sends require a system email domain and usekody@<domain>as the sender — theSYSTEM_EMAIL_DOMAINoverride when set, otherwise theAPP_BASE_URLhostname.)PACKAGE_APP_BASE_URL(Wranglervar; required in production and optional for confirmed local/preview/test runtimes; origin for hosted package apps. Production setshttps://kody.runinpackages/worker/wrangler.jsonc, and the deploy publishes apex and wildcard zone routes (kody.run/*,*.kody.run/*) on the runtime Worker (see the Cloudflare resources list above — never a custom domain in this zone). Per-user apps use{username}.kody.runsubdomains; production CI ensures the proxied apex and wildcard DNS records. Must be a separate registrable domain fromAPP_BASE_URL— see Hosted package app origin isolation. Local dev ignores any value it cannot serve itself, and preview/test leave it unset, so those keep serving package apps inline on the app origin. Point it athttp://packages.localhost:<port>inpackages/worker/.envto exercise the two-origin flow locally.)PACKAGE_APP_LEGACY_HOSTS(optional Wranglervaror GitHub Actions variable; additional package-app apex hostnames dual-served alongsidePACKAGE_APP_BASE_URL. Generated runtime zone routes replace the whole set, so omitting a listed host detaches it and deletes its DNS. Production leaves this unset.)PACKAGE_APP_LEGACY_REDIRECT(optional GitHub Actions variable; exact stringtrueenables GET/HEAD 308s from{username}.<legacy-apex>to{username}.kody.run. Leave unset to dual-serve. Production leaves this unset.)APP_COMMIT_SHA(optional; set automatically by deploy workflows for version-aware/healthchecks)APP_DEPLOY_INFO(optional; set automatically by deploy workflows as base64url JSON.GET /healthdecodes it for commit message/date, commit and PR links, and deploy job metadata. Invalid values are ignored.)CLOUDFLARE_ACCOUNT_ID(required for the Cloudflare Email Service REST API fallback used by local mocks and preview deploys)CLOUDFLARE_API_TOKEN(used by the Cloudflare Email Service REST API fallback when local/preview email is routed through the Cloudflare mock or API)SENTRY_DSN(optional Cloudflare Worker secret; enables error reporting and tracing for the Worker and Durable Objects)SENTRY_ENVIRONMENT(set per deploy viapackages/worker/wrangler.jsoncvarsasproduction,preview, ortest; optional override via env for local dev)SENTRY_TRACES_SAMPLE_RATE(optional0–1, defaults to1.0in code when unset; production pins0via a Wrangler var — see environment-variables.md)FATHOM_SITE_ID(optional public Wrangler var; when set, SSR pages embed the Fathom Analytics tracker script. Committed for production inpackages/worker/wrangler.jsonc; intentionally unset for local dev, preview, and tests — see environment-variables.md)YOUTUBE_ALLOWED_PLAYLIST_IDS(optional public Wrangler var; comma-separated playlist ids for the/?youtubeId=overlay.nonedisables playlists. Unset skips playlist fetch. Production and preview set Kent's public playlist inpackages/worker/wrangler.jsonc— see environment-variables.md)YOUTUBE_ALLOWED_VIDEO_IDS(optional public Wrangler var; extra YouTube video ids allowed by the overlay and/youtube-thumb/:videoIdproxy)YOUTUBE_DATA_API_KEY(optional origin-only Worker secret; YouTube Data API key for the homepage hero chooser playlist order. When unset, origin reads the unlisted playlist through YouTube's public browse endpoint — see environment-variables.md)APP_COMMIT_SHA(used as the Sentry release when present, in addition to/healthversioning)CLOUDFLARE_API_BASE_URL(optional; defaults tohttps://api.cloudflare.com. Production email uses the default public API base when this is unset. Localnpm run devtargets the Cloudflare mock unlessSKIP_CLOUDFLARE_MOCK=1. The internal Cloudflare API client expects paths under/client/v4/.)ARTIFACTS_NAMESPACE(optional Worker var; defaults todefault. Set per Wrangler environment inpackages/worker/wrangler.jsonc— e.g.productionandpreview— so Artifacts repos are partitioned by deploy environment.)AI_GATEWAY_ID(optional Worker secret; routes Workers AI embedding and Jev Score calls through the configured Cloudflare AI Gateway when set. Fortypesafe/jev, that gateway must have authentication enabled and Unified Billing credits, or BYOK; authentication off yields HTTP 403, zero credits yields HTTP 402. Embeddings work without Gateway; Jev does not.)CAPABILITY_REINDEX_SECRET(strongly recommended for production — CI skips the post-deploy reindex and origin-only execute smoke check when unset; optional locally and for previews; bearer auth forPOST /__maintenance/reindex-capabilities. Production deploy refreshes builtin capability vectors only ({ "phases": ["capabilities"] }). Omitphasesto rebuild memories, jobs, and saved packages too. Saved package projections also refresh when packages are saved or published. Same bearer authenticatesPOST /__maintenance/reencrypt-secrets— see Secret rotation.)JOB_REINDEX_SECRET(optional Worker secret; bearer auth forPOST /__maintenance/reindex-jobsfor a jobs-only Vectorize rebuild. Not required for production deploys — job vectors upsert on write. Omit locally and for previews unless you need the jobs-only endpoint.)GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET,GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET,X_CLIENT_ID/X_CLIENT_SECRET,DISCORD_CLIENT_ID/DISCORD_CLIENT_SECRET(optional Worker secrets; enable the "Sign in with GitHub / Google / X / Discord" login buttons. AMOCK_-prefixed client id activates the in-worker mock flow on non-production runtimes. Seedocs/contributing/social-login.md.)DISCORD_BOT_TOKEN/DISCORD_GUILD_ID/DISCORD_MEMBER_ROLE_ID/DISCORD_STANDARD_ROLE_ID/DISCORD_PRO_ROLE_ID(optional Worker secrets; when bot token and guild id are set, Discord social login best-effort joins the official guild. At least one role id enables member/plan role writes on login and Stripe plan refresh. The bot needs Create Instant Invite and Manage Roles. Seedocs/contributing/social-login.md.)STRIPE_SECRET_KEY(optional Worker secret; enables Stripe checkout linking, billing portal, andusers.stripe_planrefresh. When unset, billing degrades to manual plans.)STRIPE_WEBHOOK_SECRET(optional Worker secret; Stripe webhook signing secret forPOST /webhooks/stripe. When unset, the webhook endpoint returns 503.)STRIPE_API_BASE_URL(optional; defaults tohttps://api.stripe.com. Override for tests/mocks.)STRIPE_STANDARD_PRICE_ID(optional public Wrangler var committed inpackages/worker/wrangler.jsonc; Stripe Price id mapped to the $12/monthstandardplan and used for authenticated Checkout Sessions.)STRIPE_STANDARD_YEARLY_PRICE_ID(optional public Wrangler var committed inpackages/worker/wrangler.jsonc; Stripe Price id mapped to the $120/yearstandardplan.)STRIPE_PRO_PRICE_ID(optional public Wrangler var committed inpackages/worker/wrangler.jsonc; Stripe Price id mapped to the $49/monthproplan and used for authenticated Checkout Sessions.)STRIPE_PRO_YEARLY_PRICE_ID(optional public Wrangler var committed inpackages/worker/wrangler.jsonc; Stripe Price id mapped to the $480/yearproplan.) Each price id is independent; an unset value only disables checkout for that tier and interval.STRIPE_BILLING_PORTAL_CONFIGURATION_ID(optional public Wrangler var committed inpackages/worker/wrangler.jsoncfor production; Stripe Billing Portal configurationbpc_...used for Manage subscription and the proratedsubscription_updateflow that existing subscribers use to switch plans. Preview and test leave it unset so Stripe uses the account default.)
Tests run with CLOUDFLARE_ENV=test (set by Playwright) and read local secrets
from packages/worker/.env.
GitHub Actions configuration
Configure these GitHub Actions secrets and variables for workflows:
CLOUDFLARE_API_TOKEN(Workers deploy + D1 edit access on the correct account; also reused for remote AI and Cloudflare API workflows that run with account secrets + package workflows)CLOUDFLARE_RUNTIME_API_TOKEN(optional; the value uploaded to thekody-runtimeWorker as itsCLOUDFLARE_API_TOKENsecret. The runtime lane executes package capabilities in-process, and the only Cloudflare REST APIs those reach are Email Sending (/email/sending/send) and Artifacts (/artifacts/namespaces/..., including repo token minting, which has no binding equivalent), so this token needs exactlyAccount · Email Sending · EditandAccount · Artifacts · Edit. Workers AI, Images, Vectorize, D1, and Queues are reached through bindings or are not used by the runtime lane. When unset, the deploy falls back toCLOUDFLARE_API_TOKEN.)CLOUDFLARE_ACCOUNT_ID(required GitHub Actions variable for Cloudflare resource provisioning and Email Service)CLOUDFLARE_ZONE_ID(required GitHub Actions variable for the zone that owns the user email sending domain; Email Sending event subscriptions require both this zone id and the domain)COOKIE_SECRET(same format as local)SECRET_STORE_KEY(same format as local; required for deploys; also sealed into the DR bucket via.github/workflows/dr-escrow.ymlusingSECRET_ESCROW_PASSPHRASEplusDR_BACKUP_*S3 credentials — see Disaster recovery)APP_BASE_URL(required GitHub Actions variable used by the deployed Worker as the fallback public app origin when no request URL is available — workflows, password-reset email sender hostname — and written into the generated Workervarsconfig before deploy. Request-scoped MCP/app URLs use the inbound request origin.)AI_GATEWAY_ID(optional for production deploys; enables AI Gateway routing for Workers AI embeddings and Jev Score.typesafe/jevrequires this gateway to have authentication enabled and Unified Billing credits or BYOK)AI_GATEWAY_ID_PREVIEW(optional for preview deploys; enables AI Gateway routing for Workers AI embeddings and Jev Score; same auth/credits requirement fortypesafe/jev)SENTRY_DSN(optional; create a JavaScript/Cloudflare project in Sentry and paste the DSN; syncs to the Worker as a secret when set in GitHub Actions)YOUTUBE_DATA_API_KEY(optional origin-only; YouTube Data API key for the homepage hero chooser. Unset is fine: origin falls back to public Innertube browse. Store as a GitHub Actions secret so production deploy can sync it.)CAPABILITY_REINDEX_SECRET(strongly recommended for production; optional locally and for previews; authenticates post-deploy maintenance calls such as capability reindex — CI skips those calls when it is unset)JOB_REINDEX_SECRET(optional; authenticatesPOST /__maintenance/reindex-jobsfor a jobs-only Vectorize rebuild. Production deploys do not require it — job vectors upsert on write.)DR_BACKUP_ACCOUNT_ID/DR_BACKUP_BUCKET_NAME/DR_BACKUP_ACCESS_KEY_ID/DR_BACKUP_SECRET_ACCESS_KEY(production DR staging into the DR bucket; also used by.github/workflows/dr-escrow.yml.DR_BACKUP_ACCOUNT_IDis also the Cloudflare account id for control-plane deploys. Pair with Worker varDR_EXPORT_ENABLED=trueonly after enablement — see Disaster recovery)DR_DEPLOY_TOKEN(DR-account API token fordeploy-backup-control-planein.github/workflows/deploy.yml. Needs Workers Scripts Edit/Read, Account Workers Scripts Edit, and Workflows Edit on the DR account only — keep separate from productionCLOUDFLARE_API_TOKEN)DR_RESTORE_SECRET(shared bearer for control-plane →POST /__maintenance/dr-restore)SECRET_ESCROW_PASSPHRASE(operator passphrase for sealingSECRET_STORE_KEYintoescrow/secret-store-key.v1.json; keep the same value in the personal password manager)OAUTH_GITHUB_CLIENT_ID/OAUTH_GITHUB_CLIENT_SECRET,OAUTH_GOOGLE_CLIENT_ID/OAUTH_GOOGLE_CLIENT_SECRET,OAUTH_X_CLIENT_ID/OAUTH_X_CLIENT_SECRET,OAUTH_DISCORD_CLIENT_ID/OAUTH_DISCORD_CLIENT_SECRET(optional; social login provider app credentials. The production deploy workflow syncs them to the Worker as the unprefixedGITHUB_CLIENT_ID-style secrets — theOAUTH_prefix exists because GitHub Actions reserves theGITHUB_*secret namespace. Seedocs/contributing/social-login.mdfor provider app setup.)DISCORD_BOT_TOKEN/DISCORD_GUILD_ID/DISCORD_MEMBER_ROLE_ID/DISCORD_STANDARD_ROLE_ID/DISCORD_PRO_ROLE_ID(optional; official Kody Discord guild join and role sync. Synced to the Worker under the same names. The bot needs Create Instant Invite and Manage Roles.)KIT_API_KEY(optional GitHub / Worker secret; Kit / kit.com API key for exist-only lifecycle tagging when the email already exists in Kit. Production deploy syncs it when set; without it, account events skip Kit. Preview deploys intentionally omit the key so preview/E2E do not write to the production Kit audience. Create a Kit API key at https://app.kit.com/account_settings/developer_settings and use the same value as the Kody user secretkitApiKeywhen convenient.)STRIPE_SECRET_KEY(optional GitHub / Worker secret; Stripe secret API key for account billing. Production deploy syncs it when set. The Standard and Pro price ids are public Wrangler vars committed inpackages/worker/wrangler.jsonc, not GitHub secrets.)STRIPE_WEBHOOK_SECRET(optional GitHub / Worker secret; Stripe endpoint signing secret (whsec_...) for platform billing webhooks atPOST /webhooks/stripe. Production deploy syncs it when set.)KODY_WEBHOOK_URL_RUN(optional GitHub secret; weekly site-perf workflow only. When aneeds-fixverdict is recorded,.github/workflows/weekly-site-perf.ymlPOSTs this minted inbound webhook URL for@kentcdodds/weekly-site-perfwebhookrun(inputMode: "params",responseMode: "sync"). The value is the Kody user secretweeklySitePerfWebhookRun(https://kody.codes/account/secrets/user/weeklySitePerfWebhookRun); Kent copies it into GitHub. Agents never paste the URL. Not a Worker secret; the weekly job skips invoke when this is unset, blank, or not a validhttp(s)URL.)SENTRY_AUTH_TOKEN(optional GitHub secret; Sentry auth token withproject:releases/ source map upload permissions — used only by CI to runnpm run sentry:upload-sourcemapsafter deploy)NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN(optional GitHub secret; write bearer for the Nx HTTP cache worker athttps://nx-cache.kody.codes. Generate withopenssl rand -hex 32. Production deploy and the dedicated🧊 Nx cache workerworkflow sync it to the worker asCACHE_ACCESS_TOKEN. Use the same value in Cursor Cloud Agent environments so agentvalidate/test:pushcan populate the cache. Same-repo validate (push,workflow_dispatch, and non-forkpull_request) uses this write token too. Leave unset to run without remote writes.)NX_SELF_HOSTED_REMOTE_CACHE_READ_TOKEN(optional GitHub secret; read bearer for the same worker. Generate a secondopenssl rand -hex 32value, not the write token. Forkpull_requestvalidate jobs set Nx'sNX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKENfrom this secret so they can GET and cannot PUT. The worker syncs it asCACHE_READ_TOKEN. PUT with this token returns 403. Leave unset to run fork CI with local.nx+actions/cacheonly.)- Repository variables
SENTRY_ORGandSENTRY_PROJECT(optional; Sentry organization and project slugs for source map upload — same values as in the Sentry wizard’s--org/--projectflags)
How to get/set each value:
CLOUDFLARE_API_TOKEN- In Cloudflare Dashboard, create an API Token with permissions to deploy Workers and edit D1 on the target account. This is the same token to reuse for remote AI and Cloudflare API workflows that run with account secrets and saved packages; when you do, also include the product permissions needed for those APIs.
- In GitHub:
Settings→Secrets and variables→Actions→New repository secret.
COOKIE_SECRET- Generate locally:
openssl rand -hex 32 - Store the exact value as a repository secret in GitHub Actions.
- Generate locally:
APP_BASE_URL(optional)- Use your production app URL (for example
https://kody.codes) as the fallback public origin for workflows and password-reset email. - Add it when password reset email should send; the sender is
kody@<system email domain>(theSYSTEM_EMAIL_DOMAINoverride when set, otherwise theAPP_BASE_URLhostname), so verify that sender/domain in Cloudflare Email Service. - It also lets deploy-time health/version checks use a fixed URL.
- Production CI writes this into the generated Wrangler
varsconfig before deploy, rather than syncing it as a Worker secret. - Request-scoped MCP/app URLs use the inbound request origin so OAuth resource metadata matches the host the client connected to.
- Do not also upload
APP_BASE_URLthroughwrangler secret bulkor pass it as a deploy-time--var, because Wrangler treats that as a conflicting binding name.
- Use your production app URL (for example
CLOUDFLARE_ACCOUNT_ID/CLOUDFLARE_ZONE_ID- Copy both identifiers from the Cloudflare dashboard overview for the production zone.
- In GitHub: Settings → Secrets and variables → Actions → Variables, add each under its exact name.
USER_EMAIL_DOMAIN(optional GitHub Actions variable; overridesinbox.<APP_BASE_URL hostname>for user inboxes, outbound senders, and the Email Sending event subscription).- In GitHub: Settings → Secrets and variables → Actions → Variables, add it only when the production user email domain differs from the default.
- Production also commits
USER_EMAIL_DOMAIN=inbox.kody.codes(andSYSTEM_EMAIL_DOMAIN=kody.codes) inpackages/worker/wrangler.jsoncso the email domains can never silently rederive fromAPP_BASE_URL; the deploy tooling reads the same committed pin. OptionalLEGACY_USER_EMAIL_DOMAINS/LEGACY_SYSTEM_EMAIL_DOMAINSaccept inbound mail to additional addresses on the same inboxes; production leaves those lists unset.
APP_LEGACY_HOSTS/APP_LEGACY_REDIRECT(optional GitHub Actions variables for dual-served app hosts; see environment-variables.md).APP_LEGACY_HOSTSlists additional app hostnames (comma-separated) that stay attached to the Worker as custom domains and are dual-served. Production leaves this unset.APP_LEGACY_REDIRECT=trueenables 308 redirects for browser GET/HEAD navigation from those hosts to the canonical origin; protocol surfaces (/mcp, OAuth, well-known, auth callbacks, webhooks, health) always keep serving directly.
PACKAGE_APP_LEGACY_HOSTS/PACKAGE_APP_LEGACY_REDIRECT— package-app dual-serve; see environment-variables.md. Production leaves both unset.AI_GATEWAY_ID- Create a Cloudflare AI Gateway in the dashboard and copy its production
gateway ID. The Worker uses this for Workers AI embedding and Jev Score
calls when set. Embeddings call Workers AI directly when unset;
typesafe/jevcannot — ranked-search Jev Score requires this gateway. Enable authentication on the gateway and keep Unified Billing credits (or provide BYOK). Authentication off yields HTTP 403; zero credits yields HTTP 402. - Store that value as the production GitHub Actions secret.
- Create a Cloudflare AI Gateway in the dashboard and copy its production
gateway ID. The Worker uses this for Workers AI embedding and Jev Score
calls when set. Embeddings call Workers AI directly when unset;
AI_GATEWAY_ID_PREVIEW- Create a separate Cloudflare AI Gateway for previews and copy its gateway ID.
- Store that value as the preview GitHub Actions secret so preview deploys sync a different worker secret than production.
KODY_WEBHOOK_URL_RUN(optional)- Minted inbound webhook URL for
@kentcdodds/weekly-site-perfwebhookrun. - Kent copies the value from the Kody user secret
weeklySitePerfWebhookRunat https://kody.codes/account/secrets/user/weeklySitePerfWebhookRun into the GitHub Actions repository secretKODY_WEBHOOK_URL_RUN. Agents never paste the URL. - The weekly workflow uses this secret only to invoke that package; it is not
synced to the Worker. Rotate with
webhookUrlRotate, then update both the Kody user secret and this GitHub secret.
- Minted inbound webhook URL for
SENTRY_DSN(optional)- In Sentry: create a project, copy the DSN, and add it as the repository
secret
SENTRY_DSN. Production and preview deploy workflows sync it withsync-worker-secrets.tswhen the secret is present.
- In Sentry: create a project, copy the DSN, and add it as the repository
secret
SENTRY_AUTH_TOKEN(optional)- In Sentry: Settings → Auth Tokens (or Organization settings), create a
token that can upload releases/source maps, and store it as the
SENTRY_AUTH_TOKENrepository secret.
- In Sentry: Settings → Auth Tokens (or Organization settings), create a
token that can upload releases/source maps, and store it as the
NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN(optional)- Generate locally:
openssl rand -hex 32 - Store the exact value as the repository secret
NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN, and use the same value in Cursor Cloud Agent environments (write token). Production deploy and the dedicated🧊 Nx cache workerworkflow sync it to thekody-nx-cacheWorker asCACHE_ACCESS_TOKEN. After rotating the GitHub secret, run that workflow onmainso the worker secret matches. Same-repo validate presents this write token to Nx.
- Generate locally:
NX_SELF_HOSTED_REMOTE_CACHE_READ_TOKEN(optional)- Generate a second
openssl rand -hex 32value. Store it as the repository secretNX_SELF_HOSTED_REMOTE_CACHE_READ_TOKENonly. Do not put this value on Cloud Agent environments. The same deploy workflow syncs it asCACHE_READ_TOKEN. After adding or rotating the GitHub secret, run that workflow onmainso the worker secret matches. Forkpull_requestvalidate uses this token so a fork cannot PUT.
- Generate a second
SENTRY_ORG/SENTRY_PROJECT(optional)- In GitHub: Settings → Secrets and variables → Actions → Variables, add
SENTRY_ORGandSENTRY_PROJECTwith your Sentry slugs (for example fromnpx @sentry/wizard@latest -i sourcemaps).
- In GitHub: Settings → Secrets and variables → Actions → Variables, add
CAPABILITY_REINDEX_SECRET(strongly recommended for production; optional locally and for previews)- Generate a long random secret (for example
openssl rand -hex 32), store it as the repository secretCAPABILITY_REINDEX_SECRET, and let the deploy workflow sync it to the Worker. After each production deploy, CI POSTs{ "phases": ["capabilities"] }to/__maintenance/reindex-capabilitieswithAuthorization: Bearer …and loops on the returnedcursoruntilcompleteis true (each POST is bounded to about 70 seconds; CI follows the cursor for up to 8 sweeps), refreshing builtin capability embeddings. User-owned memory, job, and saved-package vectors upsert on write. Unchanged embed text and Vectorize metadata (same model, dimensions, and fingerprint version) skip embed and Vectorize upsert. For a full rebuild after changing the embedding model, pooling, or Vectorize index dimensions, POST{ "force": true }withoutphasesand follow the same cursor loop so existing rows are rebuilt with compatible vectors. Pooling is not part of the fingerprint, so a pooling-only change also needsforce(or avectorEmbedFingerprintVersionbump). After Vectorize data loss,forceis required so restored D1 fingerprints cannot skip an empty index. The same bearer authenticatesPOST /__maintenance/reencrypt-secrets(see Secret rotation). Local and preview environments can omit it; CI skips reindex and origin-only execute-smoke when the secret is unset.
- Generate a long random secret (for example
JOB_REINDEX_SECRET(optional; jobs-only reindex)- Bearer token for
POST /__maintenance/reindex-jobs. Generate and sync the same way asCAPABILITY_REINDEX_SECRETonly if you want the jobs-only maintenance endpoint. Production deploys do not require it; job vectors upsert on write. Local and preview can omit it.
- Bearer token for
Preview deploys for pull requests create an app Worker per PR named
<app-name>-pr-<number> (for kody: kody-pr-123), sibling platform, runtime,
and jobs Workers (…-platform, …-runtime, …-jobs), plus one Worker per mock
service named <app-name>-pr-<number>-mock-<service>. The same
CLOUDFLARE_API_TOKEN must be able to create/update and delete those Workers.