Configuration
Every deploy setting, Worker binding and secret, and the deploy token permissions.
Deploy-time settings (alchemy.run.ts)
Read from the environment of alchemy deploy, usually .env.local (gitignored) through --env-file.
| Variable | Default | Purpose |
|---|---|---|
CLOUDFLARE_API_TOKEN |
— (required) | Deploy token, see permissions below |
CLOUDFLARE_ACCOUNT_ID |
— (required) | Target account |
EMITKIT_DOMAIN |
— (required) | Zone that serves every hostname |
EMITKIT_APP_HOST |
app.$EMITKIT_DOMAIN |
PWA + auth + RPC hostname |
EMITKIT_API_HOST |
api.$EMITKIT_DOMAIN |
Public API hostname |
EMITKIT_SITE_HOST |
empty | Hostname of the marketing site and docs (emitkit.com sets its apex). Empty serves no site: every host of the Worker shows the app, and docs links point at emitkit.com. When the app host is a subdomain of the site host, the site’s header shows Dashboard to signed-in visitors; otherwise it always shows Sign in |
EMITKIT_REDIRECT_HOSTS |
www.$EMITKIT_DOMAIN |
Only with a site host: comma-separated hosts that 301 to the site; empty for none |
EMITKIT_DOCS_HOST |
docs.$EMITKIT_DOMAIN |
Only with a site host: old docs hostname, attached to the Worker and 301’d to <site>/docs/<same path>; empty for none |
EMITKIT_MAIL_DOMAIN |
mail.$EMITKIT_DOMAIN |
Email Sending subdomain (DKIM/SPF records are created automatically) |
EMITKIT_EMAIL_FROM |
noreply@$EMITKIT_MAIL_DOMAIN |
Sender of auth and invitation email |
EMITKIT_WORKER_NAME |
emitkit |
Worker, D1 (<name>-db) and queue (<name>-events) names |
EMITKIT_AE_DATASET |
emitkit_events |
Analytics Engine dataset |
EMITKIT_ALLOW_SIGNUP |
true |
false disables self-service sign-up (invitations still work) |
EMITKIT_EVENT_STORE |
analytics-engine |
d1 keeps events in D1 for 90 days instead, and mints no analytics token |
EMITKIT_AI |
true |
false removes the Workers AI binding; emoji suggestions fall back to keyword matching |
EMITKIT_VAPID_SUBJECT |
mailto:hi@$EMITKIT_DOMAIN |
Contact for push services |
EMITKIT_AUTUMN_SECRET_KEY |
empty | Hosted billing through Autumn (am_sk_…). Leave it empty when self-hosting: nothing is gated, every organization has everything, and Settings → Billing says the server has no billing. |
EMITKIT_AUTUMN_WEBHOOK_SECRET |
empty | Signing secret (whsec_…) of the Autumn webhook endpoint https://<app host>/api/billing/webhook. Without it billing still works, refreshing each organization’s plan at most every 15 minutes, but no billing push notices are sent. |
EMITKIT_TELEMETRY_EMITKIT_API_KEY |
empty | Product telemetry for whoever runs the deployment: an EmitKit API key (emitkit_…, for a project on emitkit.com) that gets a push for every sign-up, trial, payment, milestone and deletion. Empty sends nothing. See Request flows. |
EMITKIT_TELEMETRY_OPENPANEL_CLIENT_ID, EMITKIT_TELEMETRY_OPENPANEL_CLIENT_SECRET |
empty | An OpenPanel project’s client id and secret, which receives every telemetry event plus user profiles. Set both or neither. The client id is public: pages carry it to report page views from the browser, so add the site and app origins (e.g. emitkit.com, app.emitkit.com) to that client’s allowed domains in OpenPanel, or it refuses them. The secret stays in the Worker. |
EMITKIT_TELEMETRY_OPENPANEL_API_URL |
empty | A self-hosted OpenPanel’s API URL (e.g. https://openpanel.example.com/api). Empty sends to OpenPanel Cloud. |
EMITKIT_CONTEXT_DEV_API_KEY |
empty | Context.dev secret key (ctxt_secret_…) for brand lookups on the hosted paywall: the brand behind a new owner’s work domain (or the website they enter) names the organization, gives it an avatar and picks the example notifications shown before the trial. Only matters with billing on. Empty shows the general examples and a picker. Locally, set CONTEXT_DEV_API_KEY in .dev.vars. |
EMITKIT_PARSEW_KEY |
empty | Publishable Parsew key (pk_…) for brand icons (projects by their website, users by their email domain, links). It is public: the browser puts it in image URLs, so restrict it to the app origin in the Parsew dashboard. Empty shows initials and dots instead. Locally, set PARSEW_PUBLISHABLE_KEY in .dev.vars. |
Build-time settings (pnpm build)
| Variable | Default | Purpose |
|---|---|---|
DOCS_SITE_URL |
https://emitkit.com |
Origin used for the docs’ canonical URLs, sitemap and Open Graph images (docs/blume.config.ts). Only matters with EMITKIT_SITE_HOST: set it to that origin. The docs are served at /docs on the site host. |
DOCS_APP_URL |
https://app.emitkit.com |
Target of the docs’ “Open dashboard” link. Only matters with EMITKIT_SITE_HOST. |
Deploy token permissions
Create a custom account API token scoped to the account and the zone:
- Account: Workers Scripts Edit, D1 Edit, Queues Edit, Workers AI Read, Email Sending Edit, Account API Tokens Edit, Account Settings Read, Secrets Store Edit.
- Zone: Zone Read, DNS Edit, Workers Routes Edit, SSL and Certificates Edit, Single Redirect Edit.
“Account API Tokens Edit” lets the stack mint the Worker’s read-only Analytics Engine token (<name>-analytics-read); leave it out with EMITKIT_EVENT_STORE=d1. “Workers AI Read” can go with EMITKIT_AI=false, and “Single Redirect Edit” is only used for the redirect hosts of a site. “Secrets Store Edit” lets Alchemy reach its state store, whose bearer token lives in the account Secrets Store.
Worker variables and secrets
The stack sets these on the Worker. You don’t set them yourself.
| Binding / variable | Kind | Source |
|---|---|---|
DB |
D1 | Cloudflare.D1.Database, migrations from ./migrations |
EVENTS_DATASET |
Analytics Engine | Cloudflare.AnalyticsEngine.Dataset |
EVENTS_QUEUE |
Queue producer (+ consumer) | Cloudflare.Queues.Queue |
EMAIL |
send_email |
restricted to EMITKIT_EMAIL_FROM |
AI |
Workers AI | emoji suggestions; absent with EMITKIT_AI=false |
ASSETS |
static assets | ./dist/client |
APP_URL, API_URL |
text | from the hosts above |
SITE_URL |
text | from EMITKIT_SITE_HOST; unset without a site |
EVENT_STORE |
text | EMITKIT_EVENT_STORE |
ALLOW_SIGNUP, EMAIL_FROM, VAPID_SUBJECT, AE_DATASET, CF_ACCOUNT_ID |
text | from settings |
VAPID_PUBLIC_KEY |
text | derived from the generated VAPID private key |
BETTER_AUTH_SECRET |
secret | Alchemy.Random (32 bytes), kept in Alchemy state |
VAPID_PRIVATE_KEY |
secret | Alchemy.Random (P-256 scalar), kept in Alchemy state |
CF_ANALYTICS_TOKEN |
secret | account token minted by the stack (Account Analytics Read); only with Analytics Engine |
AUTUMN_SECRET_KEY, AUTUMN_WEBHOOK_SECRET |
secret | optional, from the EMITKIT_AUTUMN_* settings; only when billing is on |
TELEMETRY_EMITKIT_API_KEY, TELEMETRY_OPENPANEL_CLIENT_SECRET |
secret | optional, from the EMITKIT_TELEMETRY_* settings |
TELEMETRY_OPENPANEL_CLIENT_ID, TELEMETRY_OPENPANEL_API_URL |
text | optional, from the EMITKIT_TELEMETRY_OPENPANEL_* settings |
CONTEXT_DEV_API_KEY |
secret | optional, from EMITKIT_CONTEXT_DEV_API_KEY |
BETTER_AUTH_SECRET also derives the key that seals API keys so owners can reveal them again (api_key_secret). If you change it, existing keys keep working but can no longer be revealed; rotate a key to make it revealable again.
The Worker validates its configuration at startup (src/server/platform/config.ts). A missing or malformed value fails every request with a logged ConfigError.
Local development (wrangler.jsonc + .dev.vars)
wrangler.jsonc declares the same bindings for @cloudflare/vite-plugin with EVENT_STORE=d1 and *.localhost:5173 URLs. Local secrets go in the gitignored .dev.vars:
BETTER_AUTH_SECRET=<at least 32 random characters>
VAPID_PRIVATE_KEY=<base64url P-256 private scalar>
VAPID_PUBLIC_KEY in wrangler.jsonc must be the matching public key. Development shows how to generate a pair.
Telemetry stays off locally unless you add its keys to .dev.vars (TELEMETRY_EMITKIT_API_KEY, TELEMETRY_OPENPANEL_CLIENT_ID, TELEMETRY_OPENPANEL_CLIENT_SECRET). Use a separate EmitKit project and OpenPanel project for that, not production’s. Local dev sign-in (dev@example.com) and the E2E accounts are never reported.
To try hosted billing locally, add an Autumn sandbox key (AUTUMN_SECRET_KEY=am_sk_test_…) to .dev.vars, and optionally a made-up AUTUMN_WEBHOOK_SECRET=whsec_<base64> for the signed-webhook check in e2e/scenarios/14-billing.spec.ts. In a worktree, replace the .dev.vars symlink with a copy first, so the key stays out of the main checkout. The plans live in autumn.config.ts; pnpm billing:push previews changes against the sandbox and pnpm billing:push --yes applies them.