Skip to content

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.

name = "kilat"
main = "src/worker.ts"
compatibility_date = "2025-07-01"
compatibility_flags = ["nodejs_compat"]
[observability]
enabled = true
head_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/*"]

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.

Never put secrets in wrangler.toml (it’s committed to git). Use Wrangler’s encrypted secrets, stored separately on Cloudflare:

Terminal window
wrangler secret put RESEND_API_KEY
wrangler secret put GOOGLE_CLIENT_ID
wrangler secret put GOOGLE_CLIENT_SECRET
wrangler secret put MAILTRAP_API_TOKEN

For local dev, put them in .dev.vars (gitignored):

RESEND_API_KEY=re_xxxxx
GOOGLE_CLIENT_ID=xxxxx
GOOGLE_CLIENT_SECRET=xxxxx

Secrets 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_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:

Terminal window
npx wrangler kv namespace create RATE_LIMIT_KV

Local 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_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.

Terminal window
npx wrangler kv namespace create SESSION_KV

Local 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_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_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:

Terminal window
npx wrangler r2 bucket create kilat-avatars

Uploaded 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]
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.

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.

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 = true
head_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 = true
head_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:

Terminal window
wrangler deploy --env staging
wrangler deploy --env production

Secrets are also per-environment:

Terminal window
wrangler secret put RESEND_API_KEY --env production

Production samples 10% of requests (head_sampling_rate = 0.1) versus staging’s 100% (1). Create the per-environment bindings before first deploy:

Terminal window
npx wrangler kv namespace create RATE_LIMIT_KV --env staging
npx wrangler d1 create kilat-staging
npx wrangler r2 bucket create kilat-avatars-staging
npx wrangler kv namespace create SESSION_KV --env staging
npx wrangler kv namespace create RATE_LIMIT_KV --env production
npx wrangler r2 bucket create kilat-avatars-production
npx wrangler kv namespace create SESSION_KV --env production