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

PlaneExamplesLocal developmentProduction
Cloudflare bindingsDB, UPLOADSCreated by the local Worker runtime from wrangler.jsoncDeclared in wrangler.jsonc and backed by D1/R2 resources
Server-only runtime valuesAuth, OAuth, Resend, OpenAI, Replicate, Creem.envwrangler secret put <NAME>
Non-secret Worker varsBETTER_AUTH_URL.envwrangler.jsoncvars
Vite build-time valuesVITE_CREEM_PRODUCT_*, VITE_IS_DEMO.envBuild/CI environment; rebuild after changes
Drizzle Kit remote credentialsCLOUDFLARE_ACCOUNT_ID, database ID, D1 tokenShell or .env.localDeveloper/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

VariableRequirementDescription
BETTER_AUTH_SECRETRequired coreStable random secret used by Better Auth. Generate with openssl rand -base64 32.
BETTER_AUTH_URLRequired coreCanonical 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:3000

Production 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_SECRET

OAuth providers

VariableRequirementDescription
GITHUB_CLIENT_IDFeatureGitHub OAuth application client ID
GITHUB_CLIENT_SECRETFeatureGitHub OAuth application secret
GOOGLE_CLIENT_IDFeatureGoogle OAuth application client ID
GOOGLE_CLIENT_SECRETFeatureGoogle 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

VariableRequirementDescription
RESEND_API_KEYFeatureSends verification, password-reset, and contact email
OPENAI_API_KEYFeatureOpenAI key used by the streaming chat route
REPLICATE_API_TOKENFeatureReplicate 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

VariableRequirementDescription
CREEM_API_KEYFeatureCreem API key used to create checkouts
CREEM_WEBHOOK_SECRETFeatureHMAC secret for creem-signature verification
CREEM_SERVER_IDXFeatureCreem 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

VariableRequirementDescription
VITE_CREEM_PRODUCT_PRO_MONTHLYFeatureProduct ID for the Pro monthly price
VITE_CREEM_PRODUCT_PRO_YEARLYFeatureProduct ID for the Pro yearly price
VITE_CREEM_PRODUCT_LIFETIMEFeatureProduct 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

VariableRequirementDescription
VITE_IS_DEMOOptionalWhen "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

VariableRequirementDescription
CLOUDFLARE_ACCOUNT_IDOptional toolingCloudflare account containing remote D1
CLOUDFLARE_DATABASE_IDOptional toolingRemote D1 database UUID
CLOUDFLARE_D1_TOKENOptional toolingD1-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_IDX

Only set secrets for features you intend to enable. Never commit real values to .env.example or wrangler.jsonc.

On this page