Configuration
Kilat is configured through wrangler.toml and Wrangler secrets. There is no
.env file in production — Workers reads env vars from the env binding, not
process.env.
wrangler.toml
Section titled “wrangler.toml”name = "kilat"main = "src/worker.ts"compatibility_date = "2025-07-01"compatibility_flags = ["nodejs_compat"]
[observability]enabled = truehead_sampling_rate = 1
[vars]NODE_ENV = "development"SSR = "true"MAIL_DRIVER = "log"MAIL_FROM = "no-reply@example.com"
[[kv_namespaces]]binding = "RATE_LIMIT_KV"id = "YOUR_KV_NAMESPACE_ID"
[[d1_databases]]binding = "DB"database_id = "YOUR_D1_DATABASE_ID"database_name = "kilat"migrations_dir = "migrations"
[[r2_buckets]]binding = "AVATARS"bucket_name = "kilat-avatars"
[assets]directory = "./dist"binding = "ASSETS"not_found_handling = "none"run_worker_first = ["/*", "!/assets/*"][vars] — non-secret environment
Section titled “[vars] — non-secret environment”Public config lives under [vars]. These are checked into git and read
per-request by initConfig(env) in src/server/config.ts.
| Var | Purpose | Default |
|---|---|---|
NODE_ENV |
production enables secure cookies |
— |
SSR |
false disables server-side rendering (client-only) |
true |
MAIL_DRIVER |
log | resend | mailtrap — see Mailer |
log |
MAIL_FROM |
From address for outgoing email | no-reply@example.com |
SESSION_CACHE_ENABLED |
true enables KV session caching (requires SESSION_KV) |
false |
SESSION_CACHE_TTL_SECONDS |
KV session cache TTL in seconds | 300 |
Adding a config key means updating three places: config.ts (the EnvVars
interface + initConfig), wrangler.toml [vars], and the README env table.
Secrets — sensitive values
Section titled “Secrets — sensitive values”Never put secrets in wrangler.toml (it’s committed to git). Use Wrangler’s
encrypted secrets, stored separately on Cloudflare:
wrangler secret put RESEND_API_KEYwrangler secret put GOOGLE_CLIENT_IDwrangler secret put GOOGLE_CLIENT_SECRETwrangler secret put MAILTRAP_API_TOKENFor local dev, put them in .dev.vars (gitignored):
RESEND_API_KEY=re_xxxxxGOOGLE_CLIENT_ID=xxxxxGOOGLE_CLIENT_SECRET=xxxxxSecrets arrive in the same env binding as [vars] — initConfig(env) reads
them identically. config.ts validates relationships (e.g.
MAIL_DRIVER=resend requires RESEND_API_KEY; Google ID and secret must be set
together).
KV binding (rate limiting)
Section titled “KV binding (rate limiting)”[[kv_namespaces]]binding = "RATE_LIMIT_KV"id = "YOUR_KV_NAMESPACE_ID"The rate limiter (src/server/rate-limit.ts) is KV-backed. binding = "RATE_LIMIT_KV" exposes the namespace as env.RATE_LIMIT_KV in the Worker,
where counters are read and incremented per request. Create the namespace once
per project and paste its id:
npx wrangler kv namespace create RATE_LIMIT_KVLocal dev (wrangler dev --local) simulates the namespace via Miniflare, so any
placeholder id works. Without the binding the limiter fails OPEN — requests
pass through unthrottled — so keep this binding in your production
wrangler.toml. Optional tuning vars (RATE_LIMIT_GLOBAL_MAX,
RATE_LIMIT_GLOBAL_WINDOW, RATE_LIMIT_AUTH_MAX, RATE_LIMIT_AUTH_WINDOW) override the defaults of 200 req/60s global and
30 req/60s auth.
KV binding (session caching, optional)
Section titled “KV binding (session caching, optional)”[[kv_namespaces]]binding = "SESSION_KV"id = "YOUR_SESSION_KV_NAMESPACE_ID"Session caching is off by default — every request resolves the session from D1 (2 queries). Enable it for high-traffic deployments where D1’s single-threaded ~1K QPS limit becomes a bottleneck:
[vars]SESSION_CACHE_ENABLED = "true"SESSION_CACHE_TTL_SECONDS = "300"resolveSession checks KV first (session:{hash} key). On a hit, it returns
the cached user + flash with zero D1 queries. On a miss, it falls back to D1
and populates KV. Logout and flash consumption invalidate the KV entry. D1
remains the source of truth. See Sessions & guards
for the full pattern and security trade-offs.
npx wrangler kv namespace create SESSION_KVLocal dev simulates the namespace via Miniflare. Without the binding,
initSessionCache receives undefined and all cache operations are no-ops —
resolveSession falls back to D1-only.
D1 binding
Section titled “D1 binding”[[d1_databases]]binding = "DB"database_id = "bd0f2555-332c-4a2c-bd56-7fae6827de87"database_name = "kilat"migrations_dir = "migrations"binding = "DB" exposes the database as env.DB in the Worker.
initDb(env.DB) runs per-request in src/worker.ts to set the module-level
d1 reference used by db.ts. All queries are async:
await env.DB.prepare(sql).bind(...).first().
R2 binding (file uploads)
Section titled “R2 binding (file uploads)”[[r2_buckets]]binding = "AVATARS"bucket_name = "kilat-avatars"File uploads stream to R2 via multipart form-data. binding = "AVATARS"
exposes the bucket as env.AVATARS in the Worker. Create the bucket once
per project:
npx wrangler r2 bucket create kilat-avatarsUploaded files are served from /avatars/* via avatars.routes.ts, with
edge caching. Local dev (wrangler dev --local) simulates the binding via
Miniflare. See File uploads for the full
pattern — key structure, security, graceful degradation, and adding new
upload types.
ASSETS binding
Section titled “ASSETS binding”[assets]directory = "./dist"binding = "ASSETS"run_worker_first = ["/*", "!/assets/*"]Static assets (the esbuild output in dist/) are served via the Workers Static
Assets binding, not a custom handler. run_worker_first routes everything
through the Worker except /assets/*, which bypass directly to the static
binding for maximum speed.
nodejs_compat
Section titled “nodejs_compat”compatibility_flags = ["nodejs_compat"]Enables a subset of Node.js APIs on Workers. However, prefer the Web Crypto
API (crypto.subtle, crypto.getRandomValues, crypto.randomUUID) for
crypto operations — node:crypto is not available. nodejs_compat is mainly
for libraries that expect Node globals.
Environments
Section titled “Environments”The base config above is the local-dev default. Named environments override only the fields that differ; each environment needs its own remote IDs for KV, D1, and R2.
[env.staging]name = "kilat-staging"
[env.staging.vars]NODE_ENV = "production"SSR = "true"MAIL_DRIVER = "log"MAIL_FROM = "no-reply@staging.example.com"
[env.staging.observability]enabled = truehead_sampling_rate = 1
[[env.staging.kv_namespaces]]binding = "RATE_LIMIT_KV"id = "YOUR_STAGING_KV_ID"
[[env.staging.kv_namespaces]]binding = "SESSION_KV"id = "YOUR_STAGING_SESSION_KV_ID"
[[env.staging.d1_databases]]binding = "DB"database_id = "YOUR_STAGING_D1_ID"database_name = "kilat-staging"migrations_dir = "migrations"
[[env.staging.r2_buckets]]binding = "AVATARS"bucket_name = "kilat-avatars-staging"
[env.production]name = "kilat"
[env.production.vars]NODE_ENV = "production"SSR = "true"MAIL_DRIVER = "resend"MAIL_FROM = "no-reply@example.com"
[env.production.observability]enabled = truehead_sampling_rate = 0.1
[[env.production.kv_namespaces]]binding = "RATE_LIMIT_KV"id = "YOUR_PRODUCTION_KV_ID"
[[env.production.kv_namespaces]]binding = "SESSION_KV"id = "YOUR_PRODUCTION_SESSION_KV_ID"
[[env.production.d1_databases]]binding = "DB"database_id = "YOUR_PRODUCTION_D1_ID"database_name = "kilat-production"migrations_dir = "migrations"
[[env.production.r2_buckets]]binding = "AVATARS"bucket_name = "kilat-avatars-production"Deploy to a specific environment:
wrangler deploy --env stagingwrangler deploy --env productionSecrets are also per-environment:
wrangler secret put RESEND_API_KEY --env productionProduction samples 10% of requests (head_sampling_rate = 0.1) versus staging’s
100% (1). Create the per-environment bindings before first deploy:
npx wrangler kv namespace create RATE_LIMIT_KV --env stagingnpx wrangler d1 create kilat-stagingnpx wrangler r2 bucket create kilat-avatars-stagingnpx wrangler kv namespace create SESSION_KV --env staging
npx wrangler kv namespace create RATE_LIMIT_KV --env productionnpx wrangler r2 bucket create kilat-avatars-productionnpx wrangler kv namespace create SESSION_KV --env production