Environment variables
All environment variables used by Dirework
Dirework's variables come in two flavors: app secrets the app needs to run, and deploy secrets that only matter when deploying to Cloudflare.
Where they live
| Environment | Where to put them |
|---|---|
| Local development | packages/infra/.env — copy it from .env.example. Alchemy loads this file (plus apps/web/.env and apps/server/.env if present) when you run bun run dev. |
| Production | GitHub Settings → Secrets and variables → Actions. Sensitive values go under the Secrets tab (CLOUDFLARE_API_TOKEN, ALCHEMY_PASSWORD, ALCHEMY_STATE_TOKEN, BETTER_AUTH_SECRET, TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET); the non-secret origins go under the Variables tab (BETTER_AUTH_URL, CORS_ORIGIN). The deploy workflow reads both on every push to main, and fails fast with a clear message if any are missing. |
App secrets
These are what Dirework itself needs to run:
| Variable | Description |
|---|---|
BETTER_AUTH_SECRET | Secret key for session encryption (min 32 characters — generate with openssl rand -base64 32) |
BETTER_AUTH_URL | The web app's public origin (local: http://localhost:3001, production: https://dirework.<your-subdomain>.workers.dev) |
CORS_ORIGIN | Allowed CORS origin — same value as BETTER_AUTH_URL |
NEXT_PUBLIC_SERVER_URL | Public origin of the API worker, baked into the web build (local: http://localhost:3000). Local only — in production Alchemy injects the API worker's resolved URL itself, so you do not set this anywhere for a deploy. |
TWITCH_CLIENT_ID | Twitch application client ID from dev.twitch.tv |
TWITCH_CLIENT_SECRET | Twitch application client secret |
In production, BETTER_AUTH_URL and CORS_ORIGIN aren't secrets — set them as GitHub
repository Variables (Settings → Secrets and variables → Actions → Variables). The
deploy workflow reads them via ${{ vars.* }}. Point both at your web origin — swap in
your own workers.dev subdomain.
Don't set NEXT_PUBLIC_SERVER_URL as a GitHub Variable. Alchemy resolves the API
worker's URL during the deploy and injects it into both the web build and the runtime
binding, so a repository Variable is ignored — it looks load-bearing while doing nothing,
and a stale value there is misleading when debugging. It is a local-development variable
only.
Deploy secrets
Only needed when deploying to Cloudflare (as GitHub secrets, or in your shell for
bun run deploy):
| Variable | Description |
|---|---|
CLOUDFLARE_API_TOKEN | Cloudflare API token with Workers and D1 edit permissions |
ALCHEMY_PASSWORD | Any long random string — encrypts Alchemy's deploy state (openssl rand -base64 32) |
ALCHEMY_STATE_TOKEN | Authenticates the account-wide alchemy-state CI store worker. Generate a unique token for your state-store operator and reuse it only for apps intentionally sharing that same worker. It is not needed for local deploys, which use filesystem state. |
Optional variables
| Variable | Default | Description |
|---|---|---|
DOCS_URL | https://mrdemonwolf.github.io/dirework | URL to your documentation site. Used by the !dwhelp chat command to point viewers at the commands page. |
PRIVACY_POLICY_URL | (unset) | URL to your Privacy Policy page. When set, a link appears in the web app's footer. |
TERMS_OF_SERVICE_URL | (unset) | URL to your Terms of Service page. When set, a link appears in the web app's footer. |
SKIP_ENV_VALIDATION | (unset) | Set to "true" during CI or builds to skip env validation when runtime secrets aren't available. You won't need this locally. |
DEV_LOGIN | (unset) | Server worker; local development only. Enables the POST /api/auth/dev-login Twitch-less owner-session bypass. Never set in production. |
NEXT_PUBLIC_DEV_LOGIN | (unset) | Web build var; local development only. Shows the dev-bypass button that calls the dev-login endpoint. Never set in production. |
The legal-page URLs are read by the web worker. Set them in apps/web/.env for
local development or as GitHub repository Variables for production; the deploy workflow
validates and forwards them. If neither is set, the footer's legal links row is hidden.
Access to Dirework is single-owner: the first Twitch account to sign in claims the instance, and no one else can take it over. There's no separate allowlist to configure.
Example packages/infra/.env
# Authentication (min 32 chars — generate with: openssl rand -base64 32)
BETTER_AUTH_SECRET="<your-secret-here>"
# Public URL of the WEB worker (the browser-facing origin)
BETTER_AUTH_URL="http://localhost:3001"
# CORS allowed origin (same as BETTER_AUTH_URL)
CORS_ORIGIN="http://localhost:3001"
# Twitch OAuth (from dev.twitch.tv)
TWITCH_CLIENT_ID="your_client_id_here"
TWITCH_CLIENT_SECRET="your_client_secret_here"
# Documentation site URL (used by the !dwhelp chat command)
# DOCS_URL="http://localhost:4000"Production notes
For production deployments:
- Generate fresh secrets for production — never reuse the ones from your computer
BETTER_AUTH_URLandCORS_ORIGINmust point at your web worker (https://dirework.<your-subdomain>.workers.dev) — set as GitHub repository Variables, read by the deploy workflow via${{ vars.* }}- Do not set
NEXT_PUBLIC_SERVER_URLfor production — Alchemy injects the API worker's URL itself (see the warning above) - The deploy workflow validates required values, secret lengths, same-origin HTTPS URLs, optional external URLs, and disabled development-login flags before deploying
- There is no
DATABASE_URL— the D1 database is created and bound automatically by Alchemy - Update the Twitch OAuth redirect URLs to match your workers.dev domain (see Twitch integration setup)
- Protect
mainand require the Verify and CodeQL workflows before merge - Enable GitHub secret scanning and push protection
- Treat bot and overlay URLs as bearer credentials and regenerate them after disclosure