Configuration
Every setting is an environment variable. Most have a sensible default; an install needs
only the database, SECRET_KEY and BOOTSTRAP_ADMIN_EMAIL to start. Several (email, the
push relay, Slack, incident levels) can also be changed later on the Settings page,
which overrides the environment.
Settings marked 🔒 are secrets: put them in a Kubernetes Secret or your .env, never in a
values file or a repository.
General
| Variable |
Default |
What it does |
PORT |
8080 |
The private listener: web UI, sign-in, alert ingest, MCP, API, /metrics. Keep it off the internet. |
Exposure
| Variable |
Default |
What it does |
PUBLIC_PORT |
0 |
A SECOND listener for the internet. Only what phones and calendar apps need answers there: the API with DEVICE tokens (shd_…) only, phone pairing and calendar feeds. The web UI, sign-in, ingest, MCP, /metrics and /docs answer only on PORT, which stays on the private network. 0 = no public listener (everything on PORT, as before). |
PUBLIC_API_URL |
— |
The URL phones and calendar apps reach the PUBLIC listener at (pairing links, feed URLs). Unset = PUBLIC_URL. |
TRUSTED_PROXIES |
— |
Peers allowed to tell us the client address (X-Forwarded-For, or CLIENT_IP_HEADER). Only a request whose socket peer is in one of these is believed; the chain is then read right to left, skipping trusted hops. Default: loopback + private ranges (a proxy in front). |
CLIENT_IP_HEADER |
— |
Optional single-value header a trusted proxy sets to the client address, e.g. CF-Connecting-IP or X-Real-IP. Read only from a trusted peer. |
DEVICE_REQUESTS_PER_MINUTE |
600 |
Rate limits on the public surface (counted in the database: one budget across replicas). |
PUBLIC_AUTH_FAILURES_PER_10MIN |
300 |
Failed credentials per client address on the public listener (generous: phones share carrier NAT). |
FEED_FETCHES_PER_HOUR |
120 |
Fetches per calendar feed per hour. |
FEED_MISSES_PER_10MIN |
60 |
Unknown calendar-feed tokens per client address. |
LOGIN_FAILURES_PER_IP |
20 |
Break-glass password sign-in: failures per client address and per email, per 15 minutes. |
LOGIN_FAILURES_PER_EMAIL |
10 |
Failed password sign-ins per email address per 15 minutes. |
Serving
| Variable |
Default |
What it does |
THREADS |
8 |
Request threads per process. Scale with replicas, never with processes. |
CONNECTION_LIMIT |
200 |
Open connections per process. |
CHANNEL_TIMEOUT |
120 |
Seconds an idle connection is kept. |
SHUTDOWN_DELAY_SECONDS |
5 |
On SIGTERM, keep serving this long so load balancers stop sending first. |
SHUTDOWN_GRACE_SECONDS |
20 |
Then finish in-flight requests for up to this long. |
Database
| Variable |
Default |
What it does |
POSTGRES_HOST |
— |
The Postgres server. Set it for anything real; unset = a local SQLite file (DATABASE_URL), for trying Shorthop out. More than one replica needs Postgres. |
POSTGRES_PORT |
5432 |
Postgres port. |
POSTGRES_DB |
shorthop |
Postgres database. |
POSTGRES_USER |
shorthop |
Postgres user. |
POSTGRES_PASSWORD 🔒 |
— |
The Postgres password. POSTGRESQL_PASSWORD is accepted too; POSTGRES_PASSWORD wins. |
POSTGRES_SSLMODE |
disable |
libpq sslmode: disable, prefer, require, verify-full … |
DATABASE_URL |
sqlite:///shorthop.db |
Used only when POSTGRES_HOST is unset: a local SQLite file, for trying Shorthop out. Run Postgres for anything real. |
Web session
| Variable |
Default |
What it does |
SECRET_KEY 🔒 |
— |
Stable in production (a random one per process logs everybody out on every restart). |
SESSION_COOKIE_SECURE |
true |
Send the session cookie over HTTPS only. Leave true; browsers accept it on http://localhost too. |
PUBLIC_URL |
— |
The address people open the web UI at, e.g. https://oncall.example.com — used for links in notifications and MCP output. |
Google SSO
| Variable |
Default |
What it does |
GOOGLE_CLIENT_ID |
— |
Google sign-in (optional). Without it people sign in with a password. |
GOOGLE_CLIENT_SECRET 🔒 |
— |
Google sign-in client secret. |
GOOGLE_REDIRECT_URL |
— |
Must be /auth/google/callback, and registered on the Google client. |
GOOGLE_ALLOWED_DOMAINS |
— |
Only these email domains may sign in with Google (comma-separated). Empty = anyone you have invited. |
BOOTSTRAP_ADMIN_EMAIL |
— |
First admin on an empty install. Only consulted while the users table is EMPTY, so it can stay set forever without re-creating anyone. Signs in with Google (or a password set later through the API by that admin). |
Alert handling
| Variable |
Default |
What it does |
SIGNATURE_IGNORE_LABELS |
— |
Labels that identify an INSTANCE of a problem rather than the problem itself. They are dropped when computing an alert's signature, so the same fault on another pod is "the same alert as last time". Overridable per integration. |
DEFAULT_ALERT_TIMEOUT_MINUTES |
5 |
A firing alert with no explicit endsAt is resolved this long after it was last seen. Prometheus sends its own endsAt (≈4x resend interval) so this only covers webhooks. |
MAX_ALERTS_PER_REQUEST |
2000 |
Hard cap on alerts in one ingest request: marshmallow materialises every row before a handler can count them, so the bound has to be in the schema. |
MAX_SILENCE_DAYS |
30 |
Longest silence anyone can set. |
MCP_MAX_SILENCE_HOURS |
24 |
Silences created through MCP are capped tighter: an assistant should not be able to mute something for a month. |
Background loop
| Variable |
Default |
What it does |
SCHEDULER_INTERVAL_SECONDS |
30 |
How often the leader runs housekeeping (expiry, watchdogs, handovers). |
HEARTBEAT_URL |
— |
Dead-man's switch: pinged every loop. Point it at something OUTSIDE this cluster (healthchecks.io) so a dead Shorthop is noticed by something other than Shorthop. |
HEARTBEAT_DISPATCH_MAX_AGE_SECONDS |
60 |
The heartbeat means "I can DELIVER", not "the process is up": it is withheld while this process's dispatch loop (escalations + sending) has not completed a tick for this long, so a wedged dispatch goes quiet on the outside path too. Several dispatch intervals wide (5 s), so one slow send does not cost a ping. |
WATCHDOG_STALE_MINUTES |
10 |
An integration whose Watchdog alert has not been seen for this long is reported as silent. |
Shadow comparison against a Prometheus
| Variable |
Default |
What it does |
PROMETHEUS_ALLOWED_HOSTS |
— |
An integration may name the Prometheus it receives from; Shorthop then asks it what is firing and compares. The URL is set through the API, so the HOST must be on this list (exact names, comma-separated). Empty = the comparison is off everywhere. |
SHADOW_CHECK_INTERVAL_SECONDS |
60 |
How often the shadow comparison asks Prometheus what is firing. |
Delivery
| Variable |
Default |
What it does |
DISPATCH_INTERVAL_SECONDS |
5 |
The fast loop: due escalation steps and pending notifications. Separate from the 30 s housekeeping tick because a page that waits 30 s for the loop to come round is a slow page. |
DELIVERY_MAX_ATTEMPTS |
5 |
Tries per notification before it is marked failed. |
DELIVERY_RETRY_BASE_SECONDS |
30 |
Back-off between tries (doubles each time). |
SUPPRESSED_RECHECK_SECONDS |
60 |
A step whose alerts are all silenced or inhibited is held, not skipped, and re-checked this often — so when the silence ends, paging resumes where it was. |
SMTP_HOST |
— |
Email over SMTP. Unset SMTP_HOST = email delivery off (notifications are logged as failed with a reason, never silently dropped). |
SMTP_PORT |
587 |
SMTP port (587 STARTTLS, 465 TLS). |
SMTP_USER |
— |
SMTP user. |
SMTP_PASSWORD 🔒 |
— |
SMTP password. |
SMTP_FROM |
— |
Sender, e.g. Shorthop <alerts@example.com>. |
SMTP_STARTTLS |
true |
Use STARTTLS. |
WEBHOOK_PRIVATE_HOSTS |
— |
Webhook channels may not point at private, loopback or link-local addresses (the server would otherwise be a way into its own network). Hosts listed here are exempt — e.g. an internal chat server. Exact host names or IPs. Overridable on the Settings page. |
EXPO_ACCESS_TOKEN 🔒 |
— |
Optional: Expo's "enhanced push security" access token. Unset works with Expo's defaults. |
RELAY_URL |
— |
Push relay (app/relay.py). Both set = phones register with the relay and pages go through it (how a self-hosted install reaches the published app). Unset = straight to Expo with this install's own credentials. Overridable on the admin Settings page. |
RELAY_KEY 🔒 |
— |
Your installation key (shr_…) from https://account.shorthop.io. |
RELAY_SYNC_INTERVAL_SECONDS |
3600 |
How often the leader tells the relay which phones checked in, and reads back its warnings. |
Slack app
| Variable |
Default |
What it does |
SLACK_BOT_TOKEN 🔒 |
— |
Socket Mode: Shorthop opens an OUTBOUND WebSocket to Slack, so buttons in Slack work with no public URL. Both tokens set = on. Overridable on the admin Settings page. SLACK_BOT_TOKEN xoxb-… (OAuth & Permissions → Bot User OAuth Token) SLACK_APP_TOKEN xapp-… (Basic Information → App-Level Tokens, scope connections:write) |
SLACK_APP_TOKEN 🔒 |
— |
Slack app-level token (xapp-…), for Socket Mode. |
SLACK_RECONCILE_SECONDS |
30 |
How often each replica re-reads the Slack settings and (re)connects if they changed. |
SLACK_COMMAND |
/shorthop |
The slash command the app manifest declares. |
INCIDENT_LEVELS |
— |
Incident levels (core/levels.py) as JSON; unset = P1–P4 → critical/high/warning/info. |
METRICS_TOKEN 🔒 |
— |
If set, /metrics needs Authorization: Bearer <token>. |
Shorthop Watch
| Variable |
Default |
What it does |
WATCH_FORWARD_MIN_SECONDS |
60 |
A Watchdog is forwarded to its Watch check at most this often. Prometheus re-sends the Watchdog every minute or so; one ping a minute is all a check needs. |
Edit this page on GitHub