Environment Variables
Configure Cloudflare bindings, Worker secrets, vars, and Vite build values.
The template has five configuration planes. Keeping them separate prevents the most common Cloudflare deployment mistakes.
Configuration planes
| Plane | Examples | Local development | Production |
|---|---|---|---|
| Cloudflare bindings | DB, UPLOADS | Created by the local Worker runtime from wrangler.jsonc | Declared in wrangler.jsonc and backed by D1/R2 resources |
| Server-only runtime values | Auth, OAuth, Resend, OpenAI, Replicate, Creem | .env | wrangler secret put <NAME> |
| Non-secret Worker vars | BETTER_AUTH_URL | .env | wrangler.jsonc → vars |
| Vite build-time values | VITE_CREEM_PRODUCT_*, VITE_IS_DEMO | .env | Build/CI environment; rebuild after changes |
| Drizzle Kit remote credentials | CLOUDFLARE_ACCOUNT_ID, database ID, D1 token | Shell or .env.local | Developer/CI tooling only; never app runtime |
DB and UPLOADS are bindings, not environment variables. Runtime code reads
them at request time from cloudflare:workers. There is no DATABASE_URL
in this D1/SQLite template.
Why .env works locally
@cloudflare/vite-plugin loads local variables for the Worker environment while
Vite also exposes VITE_* values through import.meta.env. That makes .env
the single file for normal pnpm dev work. If you run wrangler dev directly,
.dev.vars can be used as a fallback.
Neither .env nor .dev.vars is uploaded by wrangler deploy. Production gets
bindings and non-secret vars from wrangler.jsonc, server secrets from
wrangler secret put, and VITE_* values from the environment that runs the
build.
Core authentication
| Variable | Requirement | Description |
|---|---|---|
BETTER_AUTH_SECRET | Required core | Stable random secret used by Better Auth. Generate with openssl rand -base64 32. |
BETTER_AUTH_URL | Required core | Canonical app origin. Use http://localhost:3000 locally and the exact deployed origin in production. |
Safe local example:
BETTER_AUTH_SECRET=generate-a-real-random-value
BETTER_AUTH_URL=http://localhost:3000Production wrangler.jsonc keeps the URL as a non-secret var:
"vars": {
"BETTER_AUTH_URL": "https://app.example.com"
}Set the secret separately:
pnpm wrangler secret put BETTER_AUTH_SECRETOAuth providers
| Variable | Requirement | Description |
|---|---|---|
GITHUB_CLIENT_ID | Feature | GitHub OAuth application client ID |
GITHUB_CLIENT_SECRET | Feature | GitHub OAuth application secret |
GOOGLE_CLIENT_ID | Feature | Google OAuth application client ID |
GOOGLE_CLIENT_SECRET | Feature | Google OAuth application secret |
Set both values for each enabled provider. If a provider is not configured, remove or hide its UI button. Callback URLs are covered in Authentication.
Email and AI
| Variable | Requirement | Description |
|---|---|---|
RESEND_API_KEY | Feature | Sends verification, password-reset, and contact email |
OPENAI_API_KEY | Feature | OpenAI key used by the streaming chat route |
REPLICATE_API_TOKEN | Feature | Replicate token used by image, audio, and video routes |
These are Worker secrets in production. See Email, AI Chat, and AI Media for feature setup and hardening.
Creem payments
Server-only runtime values
| Variable | Requirement | Description |
|---|---|---|
CREEM_API_KEY | Feature | Creem API key used to create checkouts |
CREEM_WEBHOOK_SECRET | Feature | HMAC secret for creem-signature verification |
CREEM_SERVER_IDX | Feature | Creem server selection; template default is 0 when unset |
The production setup uses wrangler secret put for all three so they remain
server-only.
Build-time product IDs
| Variable | Requirement | Description |
|---|---|---|
VITE_CREEM_PRODUCT_PRO_MONTHLY | Feature | Product ID for the Pro monthly price |
VITE_CREEM_PRODUCT_PRO_YEARLY | Feature | Product ID for the Pro yearly price |
VITE_CREEM_PRODUCT_LIFETIME | Feature | Product ID for the Lifetime price |
These IDs are read by src/config/price-config.ts and inlined by Vite. They are
not Worker secrets. A changed product ID has no effect until you rebuild and
redeploy. See Payments.
Application flags
| Variable | Requirement | Description |
|---|---|---|
VITE_IS_DEMO | Optional | When "true", enables demo-specific navigation behavior in user-sidebar-config.ts |
Like every VITE_* value, this is visible in the built client and must not
contain a secret.
D1 and R2 bindings
Bindings are declared in wrangler.jsonc:
"d1_databases": [
{
"binding": "DB",
"database_name": "soar-tanstack",
"database_id": "replace-with-your-d1-id",
"migrations_dir": "drizzle"
}
],
"r2_buckets": [
{ "binding": "UPLOADS", "bucket_name": "soar-tanstack-uploads" }
]Application code imports env from cloudflare:workers and reads env.DB or
env.UPLOADS inside request-time factories/handlers. Do not replace this with
process.env.DB, and do not capture a binding during top-level module
evaluation.
Drizzle Kit remote credentials
| Variable | Requirement | Description |
|---|---|---|
CLOUDFLARE_ACCOUNT_ID | Optional tooling | Cloudflare account containing remote D1 |
CLOUDFLARE_DATABASE_ID | Optional tooling | Remote D1 database UUID |
CLOUDFLARE_D1_TOKEN | Optional tooling | D1-scoped API token |
drizzle.config.ts uses these only for Drizzle Kit commands that contact D1
over its d1-http driver. It explicitly loads .env.local; exporting the
values in your shell also works. pnpm db:generate is offline, and the
template's pnpm db:migrate uses Wrangler, so app runtime does not need these
credentials.
Production secret setup
Run each command and enter the value interactively:
pnpm wrangler secret put BETTER_AUTH_SECRET
pnpm wrangler secret put GITHUB_CLIENT_ID
pnpm wrangler secret put GITHUB_CLIENT_SECRET
pnpm wrangler secret put GOOGLE_CLIENT_ID
pnpm wrangler secret put GOOGLE_CLIENT_SECRET
pnpm wrangler secret put RESEND_API_KEY
pnpm wrangler secret put OPENAI_API_KEY
pnpm wrangler secret put REPLICATE_API_TOKEN
pnpm wrangler secret put CREEM_API_KEY
pnpm wrangler secret put CREEM_WEBHOOK_SECRET
pnpm wrangler secret put CREEM_SERVER_IDXOnly set secrets for features you intend to enable. Never commit real values to
.env.example or wrangler.jsonc.
