App
| Variable | Required | Default | Description |
|---|
APP_ENV | Yes | development | development | production |
APP_URL | Yes | http://localhost:3000 | Public origin of the dashboard. Used for invite emails, OAuth/SSO redirects, deep links in alerts. |
INGEST_HOST | Yes | http://localhost:3001 | Base URL of the ingest API. Shown in DSN snippets in the dashboard. |
NODE_ENV | No | development | Standard Node.js env flag. |
Database — Neon Postgres
| Variable | Required | Description |
|---|
DATABASE_URL | Yes | — | Full Postgres connection string. Production: Neon with ?sslmode=require. |
Why Neon? The @neondatabase/serverless driver uses HTTP fetch — no TCP socket required, so it works on Cloudflare Workers.
Cache — Upstash Redis
| Variable | Required | Description |
|---|
UPSTASH_REDIS_REST_URL | Yes | — | REST API endpoint from the Upstash console. |
UPSTASH_REDIS_REST_TOKEN | Yes | — | REST API token from the Upstash console. |
Auth — JWT RS256
| Variable | Required | Default | Description |
|---|
JWT_PRIVATE_KEY | Yes | — | Base64-encoded RSA-2048 private key, no newlines. |
JWT_PUBLIC_KEY | Yes | — | Base64-encoded RSA-2048 public key, no newlines. |
JWT_ACCESS_EXPIRES | No | 15m | Access token TTL. |
JWT_REFRESH_EXPIRES | No | 30d | Refresh token TTL. |
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
base64 -i private.pem | tr -d '\n' # → JWT_PRIVATE_KEY
base64 -i public.pem | tr -d '\n' # → JWT_PUBLIC_KEY
Encryption keys
| Variable | Required | Description |
|---|
TOTP_ENCRYPTION_KEY | Yes (if 2FA used) | — | 32-byte hex (64 chars). AES-256-GCM key for TOTP secrets at rest. |
ENCRYPTION_KEY | Yes (for SSO + trackers) | — | 32-byte hex. AES-256-GCM key for SSO client secrets and Linear/Jira tokens at rest. |
openssl rand -hex 32
Email — Resend
| Variable | Required | Description |
|---|
RESEND_API_KEY | Yes (for emails) | — | From resend.com. |
RESEND_FROM | Yes (for emails) | — | TillPulse <noreply@mail.tillpulse.io> — sender domain must be verified in Resend. |
AI — Anthropic / OpenRouter
| Variable | Required | Description |
|---|
OPENROUTER_API_KEY | Yes (for Ask Pulse + TillMind + NLQ) | — | From openrouter.ai/keys. Required by both web app and event-processor. Models: configured via NLQ_MODEL / TILLMIND_MODEL (any OpenRouter-hosted model). |
Analytics — ClickHouse
| Variable | Required | Default | Description |
|---|
CLICKHOUSE_URL | Yes (for metrics) | — | HTTPS URL, e.g. https://host:8443. |
CLICKHOUSE_USER | No | default | ClickHouse user. |
CLICKHOUSE_PASSWORD | Yes (if auth) | — | ClickHouse password. |
CLICKHOUSE_DB | No | tillpulse | Database name. |
Search — Meilisearch
| Variable | Required | Description |
|---|
MEILI_URL | Yes (for search) | — | Meilisearch server URL. |
MEILI_MASTER_KEY | Yes (if auth) | — | Meilisearch master key. |
If unset, full-text search degrades to status-tab listing — no error.
Slack
| Variable | Required | Description |
|---|
SLACK_SIGNING_SECRET | Yes (for Slack) | — | From your Slack app's Basic Information page. |
SLACK_ACTION_SIGNING_SECRET | Yes (for Slack action buttons) | — | Random 32-byte hex string. Same value in event-processor and web app. |
Tracker webhook secrets
| Variable | Required | Description |
|---|
LINEAR_WEBHOOK_SECRET | Yes (for Linear) | — | Linear webhook HMAC secret (set in Linear dashboard). |
JIRA_WEBHOOK_TOKEN | Yes (for Jira) | — | Shared token put on the Jira webhook URL as ?token=...; Jira REST webhooks are unsigned. |
Ingest API (Fastify)
| Variable | Required | Default | Description |
|---|
INGEST_PORT | No | 3001 | Fastify port. |
INGEST_RATE_LIMIT_PER_DSN | No | 10000 | Max events per DSN per window. |
INGEST_RATE_LIMIT_WINDOW_SECONDS | No | 3600 | Sliding window in seconds. |
SOURCEMAPS_DIR | No | /tmp/tillpulse-sourcemaps | Filesystem path for uploaded source maps. |
Event processor
| Variable | Required | Default | Description |
|---|
PROCESSOR_REGION | No | af-south-1 | Pin worker to a region's Redis stream + region-suffixed DLQ. |
GEO_DB_PATH | No | — | MaxMind GeoLite2-City mmdb path. If unset, geo enrichment is skipped. |
RELEASE_HEALTH_INTERVAL_MS | No | 3600000 | Background release-health refresh cadence (default 1h). |
AI_MAX_CONCURRENT | No | 4 | Max concurrent TillMind analyses across the worker pool. |
CLI overrides
| Variable | Required | Description |
|---|
TILLPULSE_API_URL | No | — | Override default API base URL for tillpulse CLI. |
TILLPULSE_TOKEN | No | — | Personal API key for non-interactive CLI use (CI). |
Local development minimum
# apps/web/.env.local — bare minimum to boot the dashboard
DATABASE_URL=postgresql://tillpulse:tillpulse@localhost:5432/tillpulse
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=xxx
JWT_PRIVATE_KEY=<base64>
JWT_PUBLIC_KEY=<base64>
TOTP_ENCRYPTION_KEY=<64 hex>
APP_URL=http://localhost:3000
Everything else is optional. The app degrades gracefully — Ask Pulse is hidden without an Anthropic key, email actions are dropped (with a warning) without a Resend key, search disables silently without Meili, etc.