claude-start-cf

Environment Variables

TanStack Start has two env var boundaries: VITE_ prefixed vars are baked into the client bundle at build time. Everything else is server-only.

Where config lives

Cloudflare Workers config sources

wrangler.jsonc varsnon-secret config — committed
wrangler secretproduction secrets — encrypted
.dev.varslocal dev secrets — gitignored
.envVITE_ build-time vars only — gitignored

Client variables (VITE_ prefix)

import.meta.env.VITE_APP_NAME

Variables with the VITE_ prefix are statically replaced at build time and included in the JavaScript bundle. Use for public config only — app name, public API URLs, feature flags.

VITE_APP_NAMEclaude-start-cf
VITE_APP_VERSION0.1.0
VITE_PUBLIC_API_URLhttps://api.example.com

These values are embedded in the JS at build time. Changing them requires a rebuild and redeploy.

Server variables (no prefix)

process.env.SECRET_API_KEY (inside createServerFn)

Variables without VITE_ prefix are only accessible in server functions. The server function returns metadata about the vars — never the actual values.

SECRET_API_KEYnot set
DATABASE_URLnot set
NODE_ENVproduction

The server function only returns whether vars are set, not their values. The actual secrets never leave the server.

Security boundary

import.meta.env.SECRET_API_KEY → undefined

Attempting to access non-VITE_ variables from client code returns undefined. Vite strips them from the bundle at build time.

import.meta.env.SECRET_API_KEYundefined
import.meta.env.DATABASE_URLundefined

// ❌ These return undefined on the client

import.meta.env.SECRET_API_KEY // undefined

process.env.SECRET_API_KEY // not available

// ✅ Access secrets through server functions

const getData = createServerFn().handler(async () => {

return fetch(url, { headers: { Authorization: process.env.SECRET_API_KEY } })

})

Cloudflare vars (wrangler.jsonc)

wrangler.jsonc → "vars": { ... }

Non-secret config values defined in wrangler.jsonc under vars. These are plain text, committed to your repo, and available at runtime via process.env inside server functions. Use for environment labels, feature flags, region config.

APP_ENVIRONMENTproduction
DEMO_WRANGLER_VARset-via-wrangler-jsonc
// wrangler.jsonc
{
  "vars": {
    "APP_ENVIRONMENT": "production",
    "DEMO_WRANGLER_VAR": "set-via-wrangler-jsonc"
  }
}

These values are live — read from the Worker runtime on each request, not baked in at build time. You can also override per-environment using the Cloudflare dashboard.

Cloudflare secrets (encrypted)

wrangler secret put SECRET_NAME

For actual secrets (API keys, database credentials, JWT secrets), use Cloudflare's encrypted secret storage. Secrets are never stored in your repo or wrangler.jsonc — they're set via the CLI or dashboard and encrypted at rest.

Setting secrets

# Set a secret interactively (prompts for value)
wrangler secret put DATABASE_URL

# Set from a pipe (CI/CD)
echo "postgresql://..." | wrangler secret put DATABASE_URL

# Set for a specific environment
wrangler secret put DATABASE_URL --env staging

Managing secrets

# List all secrets (names only, values are hidden)
wrangler secret list

# Delete a secret
wrangler secret delete DATABASE_URL

Accessing in code

// Secrets are accessed the same way as vars
const getDb = createServerFn().handler(async () => {
  const url = process.env.DATABASE_URL
  return db.connect(url)
})

Secrets and vars merge into the same process.env namespace at runtime. The only difference is how they're stored — vars are plain text in wrangler.jsonc, secrets are encrypted on Cloudflare's infrastructure.

How process.env works on Workers

nodejs_compat → process.env

Cloudflare Workers don't natively have process.env. The nodejs_compat flag shims it, making wrangler vars and secrets available through the standard Node.js API.

SourceWhen availableStored in repo?
.dev.varsLocal dev runtimeGitignored
.envBuild time only (VITE_ vars)Gitignored
wrangler.jsonc varsDev + production runtimeYes (plain text)
wrangler secretDev + production runtimeNo (encrypted)
CF DashboardProduction runtime onlyNo (encrypted)

Which to use?

wrangler.jsonc vars — Non-secret config: environment labels, feature flags, public API URLs. Committed to git so the whole team shares them.

wrangler secret put — Actual secrets: database URLs, API keys, JWT signing keys. Encrypted, never in source control. Set once per environment.

CF Dashboard — Same as secrets but managed through the web UI. Good for non-developers or one-off overrides.

.dev.vars — Local dev secrets. Loaded by Miniflare during npm run dev. Not used in production.

.env — Vite build-time vars only (VITE_ prefix). Not used at runtime on Workers.

Type safety with env.d.ts

src/env.d.ts

Declare your env var types so TypeScript catches missing or mistyped variables at compile time.

// src/env.d.ts
interface ImportMetaEnv {
  readonly VITE_APP_NAME: string
  readonly VITE_APP_VERSION: string
  readonly VITE_PUBLIC_API_URL: string
}

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      readonly SECRET_API_KEY: string
      readonly DATABASE_URL: string
    }
  }
}

With this file, import.meta.env.VITE_TYPO would be a TypeScript error. See src/env.d.ts in this project for the full declaration.