Skip to content

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.

[app]
name = "Whatomate"
environment = "development" # development, staging, production
debug = true
encryption_key = "" # AES-256 key (32+ chars) for encrypting secrets at rest. Required in production.
[server]
host = "0.0.0.0"
port = 8080
read_timeout = 30
write_timeout = 30
base_path = "" # Set to "/subpath" if behind nginx proxy_pass at a sub-path
allowed_origins = "" # Comma-separated CORS origins. Empty = allow all (dev only).
[database]
host = "localhost" # Use "db" inside Docker compose
port = 5432
user = "whatomate"
password = "your-password"
name = "whatomate"
ssl_mode = "disable" # disable, require, verify-ca, verify-full
max_open_conns = 25
max_idle_conns = 5
conn_max_lifetime = 300 # seconds
[redis]
host = "localhost" # Use "redis" inside Docker compose
port = 6379
username = "" # Redis 6+ ACL username. Empty = default user.
password = ""
db = 0
tls = false # true for managed Redis (Upstash, Redis Cloud)
[jwt]
secret = "change-me-in-production" # 32+ chars in production
access_expiry_mins = 15
refresh_expiry_days = 1
[storage]
type = "local" # local | s3
local_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 switch
login_max_attempts = 10
register_max_attempts = 10
refresh_max_attempts = 30
sso_max_attempts = 10
window_seconds = 60
trust_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 routes
api_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 # seconds
audio_dir = "./audio"
# hold_music_file = "hold_music.opus"
# ringback_file = "ringback.opus"
transfer_timeout_secs = 120
recording_enabled = true
udp_port_min = 10000
udp_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"
FieldRequiredNotes
namenoDisplay name. Defaults to "Whatomate".
environmentyesdevelopment | staging | production. Sets debug behavior, forces cookie.secure=true in production.
debugnoVerbose logging.
encryption_keyproductionAES-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.
FieldNotes
host, portDefault 0.0.0.0:8080.
read_timeout, write_timeoutSeconds.
base_pathLeave empty unless serving at a sub-path (https://app.example.com/whatomate) behind a proxy.
allowed_originsComma-separated list, e.g. "https://app.example.com,https://admin.example.com". Empty allows all origins — only safe in development.

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.

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.

FieldNotes
secretHMAC signing key. Must be 32+ chars in production. Rotating invalidates all sessions.
access_expiry_minsDefault 15.
refresh_expiry_daysDefault 1.

JWTs are stored in httpOnly cookies — the frontend never sees them.

Controls media storage (template samples, uploaded files, optional call recordings).

  • type = "local" — files under local_path (default ./uploads). Suitable for single-host deployments. The directory must be writable by the process and persisted (Docker volume).
  • type = "s3" — fill in s3_bucket, s3_region, s3_key, s3_secret. Required if you set calling.recording_enabled = true and want recordings retained off-host.
FieldNotes
domainSet to .example.com to share auth cookies across subdomains. Leave empty for single-host.
secureThe Secure cookie flag (HTTPS-only). Automatically forced to true when app.environment = "production", regardless of this setting.

Off by default. Turn on with enabled = true for any internet-facing deployment.

FieldNotes
login_max_attemptsPer IP, per window_seconds.
register_max_attemptsPer IP, per window. Throttles sign-up attempts.
refresh_max_attemptsPer IP, per window. Higher because legitimate clients refresh often.
sso_max_attemptsPer IP, per window for /api/auth/sso/*.
api_max_requestsGlobal per-user cap across all /api routes once authenticated.
trust_proxySet 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_binarypiper standalone TTS executable.
  • piper_model.onnx voice model from piper-voices.
  • opusenc_binaryopusenc from opus-tools (apt install opus-tools / dnf install opus-tools). Defaults to looking up opusenc in PATH.

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.

Required only if you use WhatsApp voice calling / IVR.

FieldNotes
max_call_durationHard cap in seconds; calls beyond this are forcibly terminated.
audio_dirWhere prompt audio files (hold_music.opus, ringback.opus, IVR prompts) live.
hold_music_file, ringback_fileFilenames inside audio_dir.
transfer_timeout_secsHow long to wait for an agent to accept a transferred call before falling through.
recording_enabledRecords 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_maxRTP/SRTP UDP port range. Whatever range you set must be open in your firewall / security group. Default 10000–10100 gives 100 concurrent streams.
public_ipRequired 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_onlyIf 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.

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.

Any key in config.toml can be overridden by an environment variable. The mapping is:

WHATOMATE_<SECTION>_<KEY>

with underscores in <KEY> preserved. Examples:

VariableTOML field
WHATOMATE_APP_ENVIRONMENTapp.environment
WHATOMATE_APP_ENCRYPTION_KEYapp.encryption_key
WHATOMATE_DATABASE_HOSTdatabase.host
WHATOMATE_DATABASE_SSL_MODEdatabase.ssl_mode
WHATOMATE_REDIS_HOSTredis.host
WHATOMATE_REDIS_TLSredis.tls
WHATOMATE_JWT_SECRETjwt.secret
WHATOMATE_STORAGE_S3_BUCKETstorage.s3_bucket
WHATOMATE_RATE_LIMIT_ENABLEDrate_limit.enabled
WHATOMATE_CALLING_PUBLIC_IPcalling.public_ip
WHATOMATE_DEFAULT_ADMIN_PASSWORDdefault_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.

Terminal window
# Create database
createdb whatomate
# Or using psql
psql -c "CREATE DATABASE whatomate;"
Terminal window
./whatomate server -migrate

Schema 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 Cloud API credentials are not in config.toml. Configure them per organization in the UI:

  1. Navigate to Settings → Accounts
  2. Click Add Account
  3. 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
Terminal window
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.

Terminal window
make build-prod # Single binary with embedded frontend
CommandFrontendUse Case
make buildnot embeddedDevelopment
make build-prodembeddedProduction
Terminal window
./whatomate <command> [options]
CommandDescription
serverStart the API server (with optional embedded workers)
workerStart background workers only (no API server)
versionShow version information
helpShow help message
Terminal window
./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)
Terminal window
./whatomate worker [options]
-config string Path to config file (default "config.toml")
-workers int Number of workers to run (default 1)

API and workers in a single process — simplest, fine up to moderate campaign volume:

Terminal window
./whatomate server

Disable embedded workers on the API host:

Terminal window
./whatomate server -workers=0

Run workers on separate machines:

Terminal window
./whatomate worker -workers=4

Both API and worker processes need access to the same Postgres and Redis.

Terminal window
# Start all services
docker compose up -d
# Scale workers
docker compose up -d --scale worker=3
  • Set app.environment = "production" and app.debug = false.
  • Set app.encryption_key to 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 (and redis.username for ACL) and enable redis.tls = true for managed Redis.
  • Enable rate_limit.enabled = true and set rate_limit.trust_proxy to match your reverse proxy.
  • Restrict server.allowed_origins to your actual frontend hostnames.
  • For voice calling: set calling.public_ip on cloud hosts, ensure UDP udp_port_minudp_port_max is 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.