Skip to content

Operations

Notifycat is designed to be operated as a single process plus a SQLite file. No background workers, no external queue, no runtime dependencies beyond Slack and your git host.

Process model

Run one notifycat-server behind your normal HTTPS ingress. The git host posts webhooks in; Notifycat makes outbound HTTPS calls to Slack. The server exposes:

Route Purpose
GET /healthz Liveness/readiness. Returns 200 once the listener is up — and because startup validation and migrations run before the listener opens, a 200 means the process is healthy in both senses. Wire it as the target-group health check or Kubernetes probe path.
POST /webhook/github GitHub receiver — registered only when git_provider: github.
POST /webhook/bitbucket Bitbucket receiver — registered only when git_provider: bitbucket.

Only the configured provider's route exists; posting a Bitbucket payload to /webhook/github (or vice versa) returns 404.

Startup and shutdown

The server fails fast on missing or invalid configuration — see Server exits at startup for the causes it names. It applies embedded migrations at startup, so a simple deployment starts the server directly; stricter setups can run notifycat-migrate up as a separate init step first.

Startup validation distinguishes what the operator controls from what lives on the git host:

Check Outcome Effect
mapping, channel-format, slack-auth, slack-channel FAIL Startup aborts. The config or the Slack install is broken, so nothing can be posted.
webhook, org-repos WARN Startup continues. A missing webhook, missing events, or a hooks listing the read token may not fetch (403) limits that one entry — PR events don't arrive, or coverage can't be confirmed — while every other repository keeps working.

Each warning logs startup validate warning with entry, repository, check, and detail, followed by one startup validation completed with warnings line naming the affected entries. Warned entries are not written to config.lock, so every boot re-probes them and re-logs the warning until the problem is fixed — no warning goes quiet on its own.

When the agent API is enabled and server.admin_addr is set, the admin server is a second listener. It starts and stops together with the public one. If either port cannot be bound, startup stops with an error that names the server, and the exit code is 1. The error is printed on stderr, not as a log line: notifycat-server: admin server: listen tcp … (or http server: …). A later fatal error on either listener also exits with code 1. See Agent API access.

Shutdown listens for SIGINT/SIGTERM and lets in-flight requests finish within the shutdown window.

Persistence and backup

State lives in two places:

  • config.yaml — the declarative source of truth for routing and all non-secret settings. Keep it in version control and deploy it alongside the binary. The sibling config.lock caches successful validation so steady-state boots don't re-contact Slack or the git host. Entries that warned are excluded from it by design.
  • SQLite — per-PR Slack message timestamps, so Notifycat can keep updating the same message across the PR lifecycle.

Back up the SQLite file if losing notification state would hurt. If it's lost, webhooks still work — but PRs that were already announced get fresh Slack messages, because the old timestamp mapping is gone. config.yaml and config.lock live in your ops repository, so a lost container copy is harmless.

A background cleanup removes slack_messages rows untouched for longer than cleanup.message_ttl_days (default 30), once at startup and then every 24 hours. It deletes only the database row — never the Slack message.

That sweep is separate from cleanup.delete_on_close, which does delete Slack messages — but only for a PR that has just been merged or declined, and only where the flag is on. See Remove messages on close.

Changing git_provider requires a fresh database

Stale rows keyed by the old provider collide with the new one and silently suppress posts — details in Upgrading.

Logging

Set server.log_format: json in config.yaml for production log aggregation, and server.log_level: debug when triaging. Logs never contain raw webhook payloads, signatures, or secrets.

The one log contract worth knowing by heart: every intentionally-ignored delivery logs ignored webhook event with a reason field. That line is how you answer "the webhook returned 200 — why didn't Slack change?" from logs alone. The reason table lives in Troubleshooting.

The agent API logs these lines. None of them carries a credential or an Authorization header.

Message Level Meaning
admin server listening info The admin listener is up (addr).
admin server not started warn No admin listener runs. reason is agent_api.enabled is false (server.admin_addr is set but the agent API is disabled) or server.admin_addr is empty (the agent API is on but has no admin port, so no invites can be created; existing tokens keep working).
admin server stopped error The admin listener failed after it started; the process exits with code 1.
admin auth failed warn A request to the admin port had a missing or wrong admin key.
agent invite created info An invite was issued (user, invite_id, expires_at).
agent token created info An invite was redeemed (user, token_id).
agent invite redeem failed warn reason is malformed, unknown, mismatch, used, or expired.
agent api auth failed warn reason is malformed, unknown, or mismatch.
agent token deleted info An admin removed a token (user, token_id).
agent token last_used_at not updated warn The token worked but its usage timestamp could not be written.
agent api request failed error An internal error on a public agent route.
admin request failed error An internal error on an admin route.
feed subscriptions added info A token subscribed to new repositories (user, token_id, count).
feed subscription removed info A token removed a subscription (user, token_id, repository).
feed pull requests delivered info A poll returned new PRs (user, token_id, count). Logged only when count is above 0.
feed pull request not recorded warn A webhook PR could not be stored for the feed (repository, pr, kind, err). The webhook response does not change.
feed pull requests removed info At startup, notifycat removed stored feed PRs (count, reason). reason is agent_api_off (the agent API is off, so every stored PR goes) or unmapped (the PRs of repositories that left mappings:). Logged only when count is above 0.
feed subscriptions removed info At startup, notifycat removed the subscriptions of every token to repositories that left mappings: (count, reason, repositories). reason is unmapped, and repositories lists the removed repository names, sorted. The * subscription stays, and so does every subscription when mappings: has no repository. Logged on every start, with the API on or off, only when count is above 0.
feed cleanup failed warn A startup removal of feed PRs or of feed subscriptions failed (err). Notifycat still starts, and the other removal still runs.

ignored webhook event with reason=no_handler also shows kind=updated (a new push) and kind=reopened, whether the agent API is on or off. Slack ignores these kinds. The feed records them when the agent API is on. See the agent API guide.

Release images

Each release publishes multi-arch images to ghcr.io/mptooling/notifycat; tags — including the per-PR beta images — are covered in Supported tags. To move a running Compose deployment to a new release: docker compose pull && ./notifycat up. Release-specific operator actions are collected in Upgrading.