Configuration
Whatomate uses a TOML configuration file for all settings. By default, it looks for config.toml in the current directory; pass -config /path/to/config.toml to override.
A complete commented example lives in config.example.toml at the repository root — copy it to config.toml and edit. The reference below documents every section.
Configuration File
Section titled “Configuration File”[app]name = "Whatomate"environment = "development" # development, staging, productiondebug = trueencryption_key = "" # AES-256 key (32+ chars) for encrypting secrets at rest. Required in production.
[server]host = "0.0.0.0"port = 8080read_timeout = 30write_timeout = 30base_path = "" # Set to "/subpath" if behind nginx proxy_pass at a sub-pathallowed_origins = "" # Comma-separated CORS origins. Empty = allow all (dev only).
[database]host = "localhost" # Use "db" inside Docker composeport = 5432user = "whatomate"password = "your-password"name = "whatomate"ssl_mode = "disable" # disable, require, verify-ca, verify-fullmax_open_conns = 25max_idle_conns = 5conn_max_lifetime = 300 # seconds
[redis]host = "localhost" # Use "redis" inside Docker composeport = 6379username = "" # Redis 6+ ACL username. Empty = default user.password = ""db = 0tls = false # true for managed Redis (Upstash, Redis Cloud)
[jwt]secret = "change-me-in-production" # 32+ chars in productionaccess_expiry_mins = 15refresh_expiry_days = 1
[storage]type = "local" # local | s3local_path = "./uploads"s3_bucket = ""s3_region = ""s3_key = ""s3_secret = ""
[cookie]domain = "" # e.g. ".example.com" to share across subdomains. Empty = current host only.secure = false # Sets the Secure flag. Auto-forced to true when environment=production.
[rate_limit]enabled = false # Master switchlogin_max_attempts = 10register_max_attempts = 10refresh_max_attempts = 30sso_max_attempts = 10window_seconds = 60trust_proxy = false # true if behind a reverse proxy (uses X-Forwarded-For / X-Real-IP)api_max_requests = 200 # Per-IP cap across all /api routesapi_window_seconds = 60
[tts]# piper_binary = "/usr/local/bin/piper"# piper_model = "/opt/piper/models/en_US-lessac-medium.onnx"# opusenc_binary = "opusenc"
[calling]max_call_duration = 300 # secondsaudio_dir = "./audio"# hold_music_file = "hold_music.opus"# ringback_file = "ringback.opus"transfer_timeout_secs = 120recording_enabled = trueudp_port_min = 10000udp_port_max = 10100# public_ip = "1.2.3.4" # Required on AWS / cloud where the host doesn't see its public IP# relay_only = true # Force all media through TURN
# Configure STUN, TURN, or both. Repeat the [[calling.ice_servers]] block for each.# [[calling.ice_servers]]# urls = ["stun:stun.l.google.com:19302"]# [[calling.ice_servers]]# urls = ["turn:your-turn.example.com:3478"]# username = "user"# credential = "password"
[default_admin]email = "admin@admin.com"password = "admin"full_name = "Admin"Sections in detail
Section titled “Sections in detail”| Field | Required | Notes |
|---|---|---|
name | no | Display name. Defaults to "Whatomate". |
environment | yes | development | staging | production. Sets debug behavior, forces cookie.secure=true in production. |
debug | no | Verbose logging. |
encryption_key | production | AES-256 key used to encrypt access tokens, SSO client secrets, and other secrets at rest in the DB. Must be 32+ chars. Lose it and you lose stored credentials. |
[server]
Section titled “[server]”| Field | Notes |
|---|---|
host, port | Default 0.0.0.0:8080. |
read_timeout, write_timeout | Seconds. |
base_path | Leave empty unless serving at a sub-path (https://app.example.com/whatomate) behind a proxy. |
allowed_origins | Comma-separated list, e.g. "https://app.example.com,https://admin.example.com". Empty allows all origins — only safe in development. |
[database]
Section titled “[database]”PostgreSQL only. The max_open_conns / max_idle_conns / conn_max_lifetime knobs map directly to database/sql pool settings — defaults are tuned for a single API process; raise max_open_conns if you run many workers against the same DB.
ssl_mode accepts standard libpq values: disable, require, verify-ca, verify-full. Use require or stricter in production.
[redis]
Section titled “[redis]”Required. Used for:
- Session/CSRF storage
- Job queue (Redis Streams) for campaign sends
- Pub/Sub for campaign-stats fan-out
- Rate-limit counters
- Cache for chatbot flows / keyword rules
Set tls = true and provide username + password for managed services like Upstash or AWS ElastiCache Serverless.
| Field | Notes |
|---|---|
secret | HMAC signing key. Must be 32+ chars in production. Rotating invalidates all sessions. |
access_expiry_mins | Default 15. |
refresh_expiry_days | Default 1. |
JWTs are stored in httpOnly cookies — the frontend never sees them.
[storage]
Section titled “[storage]”Controls media storage (template samples, uploaded files, optional call recordings).
type = "local"— files underlocal_path(default./uploads). Suitable for single-host deployments. The directory must be writable by the process and persisted (Docker volume).type = "s3"— fill ins3_bucket,s3_region,s3_key,s3_secret. Required if you setcalling.recording_enabled = trueand want recordings retained off-host.
[cookie]
Section titled “[cookie]”| Field | Notes |
|---|---|
domain | Set to .example.com to share auth cookies across subdomains. Leave empty for single-host. |
secure | The Secure cookie flag (HTTPS-only). Automatically forced to true when app.environment = "production", regardless of this setting. |
[rate_limit]
Section titled “[rate_limit]”Off by default. Turn on with enabled = true for any internet-facing deployment.
| Field | Notes |
|---|---|
login_max_attempts | Per IP, per window_seconds. |
register_max_attempts | Per IP, per window. Throttles sign-up attempts. |
refresh_max_attempts | Per IP, per window. Higher because legitimate clients refresh often. |
sso_max_attempts | Per IP, per window for /api/auth/sso/*. |
api_max_requests | Global per-user cap across all /api routes once authenticated. |
trust_proxy | Set to true only if you run behind a trusted reverse proxy (nginx, Cloudflare, ALB) — otherwise clients can spoof their IP via X-Forwarded-For. |
Implemented as Redis fixed-window counters; failures hit Redis, so leaving rate limits enabled with Redis down will block all auth.
Optional, only used when an IVR step needs to synthesize speech (welcome prompt, menu read-out). All three keys are paths to external binaries / models:
piper_binary— piper standalone TTS executable.piper_model—.onnxvoice model from piper-voices.opusenc_binary—opusencfromopus-tools(apt install opus-tools/dnf install opus-tools). Defaults to looking upopusencinPATH.
If unset, IVR flows still work but cannot synthesize new audio prompts on the fly — only pre-rendered Opus files in calling.audio_dir will play.
[calling]
Section titled “[calling]”Required only if you use WhatsApp voice calling / IVR.
| Field | Notes |
|---|---|
max_call_duration | Hard cap in seconds; calls beyond this are forcibly terminated. |
audio_dir | Where prompt audio files (hold_music.opus, ringback.opus, IVR prompts) live. |
hold_music_file, ringback_file | Filenames inside audio_dir. |
transfer_timeout_secs | How long to wait for an agent to accept a transferred call before falling through. |
recording_enabled | Records both directions per call. Requires [storage] type = "s3" and credentials for off-host retention; without S3, recordings are local under local_path. |
udp_port_min, udp_port_max | RTP/SRTP UDP port range. Whatever range you set must be open in your firewall / security group. Default 10000–10100 gives 100 concurrent streams. |
public_ip | Required on AWS / GCP / Azure, where the host’s network interface does not have its public IP attached. Pion uses this for ICE candidate generation. |
relay_only | If true, advertises only TURN candidates (no host/srflx). Useful when the network is too restricted for direct UDP. |
[[calling.ice_servers]] | One block per STUN/TURN server. STUN-only works in permissive networks; configure TURN for guaranteed connectivity behind strict NAT or corporate firewalls. |
[default_admin]
Section titled “[default_admin]”The credentials seeded the first time whatomate server -migrate runs against an empty user table. Subsequent runs are no-ops. Always change these on first login.
Environment variables
Section titled “Environment variables”Any key in config.toml can be overridden by an environment variable. The mapping is:
WHATOMATE_<SECTION>_<KEY>with underscores in <KEY> preserved. Examples:
| Variable | TOML field |
|---|---|
WHATOMATE_APP_ENVIRONMENT | app.environment |
WHATOMATE_APP_ENCRYPTION_KEY | app.encryption_key |
WHATOMATE_DATABASE_HOST | database.host |
WHATOMATE_DATABASE_SSL_MODE | database.ssl_mode |
WHATOMATE_REDIS_HOST | redis.host |
WHATOMATE_REDIS_TLS | redis.tls |
WHATOMATE_JWT_SECRET | jwt.secret |
WHATOMATE_STORAGE_S3_BUCKET | storage.s3_bucket |
WHATOMATE_RATE_LIMIT_ENABLED | rate_limit.enabled |
WHATOMATE_CALLING_PUBLIC_IP | calling.public_ip |
WHATOMATE_DEFAULT_ADMIN_PASSWORD | default_admin.password |
Env vars take precedence over the config file, so the typical pattern is to commit a non-secret config.toml and inject secrets (*_PASSWORD, *_KEY, *_SECRET, JWT_SECRET) via the deployment environment.
Database Setup
Section titled “Database Setup”PostgreSQL
Section titled “PostgreSQL”# Create databasecreatedb whatomate
# Or using psqlpsql -c "CREATE DATABASE whatomate;"Run Migrations
Section titled “Run Migrations”./whatomate server -migrateSchema is generated from GORM struct tags in internal/models/; there are no SQL migration files. Re-running with -migrate is safe — it adds new tables/columns and is a no-op for existing ones.
WhatsApp API Configuration
Section titled “WhatsApp API Configuration”WhatsApp Cloud API credentials are not in config.toml. Configure them per organization in the UI:
- Navigate to Settings → Accounts
- Click Add Account
- Enter:
- Phone Number ID — from Meta Business Suite
- Business Account ID — from Meta Business Suite
- Access Token — generated in Meta for Developers
- Webhook Verify Token — your custom verification token
Building
Section titled “Building”Development
Section titled “Development”make build # Backend only (no frontend embedded)For development, run the backend and frontend separately:
- Backend:
make run-migrate(or./whatomate server -migrate) - Frontend:
cd frontend && npm run dev
The Vite dev server proxies /api to :8080.
Production
Section titled “Production”make build-prod # Single binary with embedded frontend| Command | Frontend | Use Case |
|---|---|---|
make build | not embedded | Development |
make build-prod | embedded | Production |
CLI Reference
Section titled “CLI Reference”./whatomate <command> [options]| Command | Description |
|---|---|
server | Start the API server (with optional embedded workers) |
worker | Start background workers only (no API server) |
version | Show version information |
help | Show help message |
server options
Section titled “server options”./whatomate server [options]
-config string Path to config file (default "config.toml") -migrate Run database migrations on startup -workers int Number of embedded workers, 0 to disable (default 1)worker options
Section titled “worker options”./whatomate worker [options]
-config string Path to config file (default "config.toml") -workers int Number of workers to run (default 1)Deployment Scenarios
Section titled “Deployment Scenarios”All-in-One
Section titled “All-in-One”API and workers in a single process — simplest, fine up to moderate campaign volume:
./whatomate serverSeparate API and Workers
Section titled “Separate API and Workers”Disable embedded workers on the API host:
./whatomate server -workers=0Run workers on separate machines:
./whatomate worker -workers=4Both API and worker processes need access to the same Postgres and Redis.
Docker Compose
Section titled “Docker Compose”# Start all servicesdocker compose up -d
# Scale workersdocker compose up -d --scale worker=3Production Recommendations
Section titled “Production Recommendations”- Set
app.environment = "production"andapp.debug = false. - Set
app.encryption_keyto a 32+ char random string. Back it up — losing it means losing all stored credentials. - Use strong, unique values for
jwt.secret(32+ chars). - Set
database.ssl_mode = "require"(or stricter). - Configure
redis.password(andredis.usernamefor ACL) and enableredis.tls = truefor managed Redis. - Enable
rate_limit.enabled = trueand setrate_limit.trust_proxyto match your reverse proxy. - Restrict
server.allowed_originsto your actual frontend hostnames. - For voice calling: set
calling.public_ipon cloud hosts, ensure UDPudp_port_min–udp_port_maxis open in your security group, configure at least one[[calling.ice_servers]](TURN strongly recommended for traversal across strict NATs). - Terminate TLS at a reverse proxy (nginx, Caddy, or a cloud load balancer) — the binary serves HTTP only.
- Run API and workers as separate processes so a stuck campaign send doesn’t block API responses.