Configuration¶
This page is the complete key-by-key reference. New to Notifycat? Read Configuration basics first — it shows the whole model (two files, one change loop) in two minutes; come back here to look up exact keys, defaults, and rules.
Notifycat is configured through a single config.yaml file that holds all non-secret settings. Secrets and infra-interpolation values live in environment variables (or .env for local development). For local development, .env is loaded automatically if the file exists. Outside Docker, the server and CLIs read .env from their working directory; a variable already set in the environment wins over the file.
The config file path defaults to ./config.yaml. Override with NOTIFYCAT_CONFIG_FILE. The Docker image default is /app/config.yaml.
The validation cache config.lock is written next to the config file. Override with NOTIFYCAT_LOCK_FILE when that folder is read-only — for example when the platform injects config.yaml as a file and the writable volume lives elsewhere.
Secrets should stay in environment variables or your deployment secret manager. Do not put real tokens in committed files. See config.example.yaml for a copy-paste starting point.
Secrets (environment variables only)¶
Which webhook secret and read token apply depends on git_provider: a github deployment uses the GITHUB_* variables, a bitbucket deployment uses the BITBUCKET_* variables. SLACK_BOT_TOKEN is always required; a deployment never needs the other provider's credentials.
| Variable | Required | Notes |
|---|---|---|
SLACK_BOT_TOKEN |
Required | Slack bot token, usually starting with xoxb-. |
SLACK_SIGNING_SECRET |
Optional | Turns on Slack interactions: the server serves POST /webhook/slack/interactions and PR messages carry the Start review button. Find it under your Slack app's Basic Information → App Credentials → Signing Secret. If unset, notifycat stays outbound-only and posts no button. |
GITHUB_WEBHOOK_SECRET |
Required for git_provider: github |
The secret configured on the GitHub webhook. Use a long random value; 32 characters or more is a good baseline. |
GITHUB_TOKEN |
Optional (github) | PAT used by notifycat-config validate and notifycat-doctor to read repository webhook config and by path routing to read a PR's changed files. Required scope: admin:repo_hook (or repo for private repositories). The server does not need this; if unset, the webhook-coverage and path-routing checks are skipped. |
BITBUCKET_WEBHOOK_SECRET |
Required for git_provider: bitbucket |
The secret configured on the Bitbucket repository/workspace webhook. Bitbucket signs deliveries with X-Hub-Signature: sha256=<hmac>; an unsigned delivery (no secret configured) is rejected. |
BITBUCKET_TOKEN |
Optional (bitbucket) | Access token (Bearer) used by validate/doctor to read webhook config and by path routing to read changed files — exact GITHUB_TOKEN degradation parity. Least-privilege scopes: repository + pullrequest + webhook. |
NOTIFYCAT_ADMIN_KEY_HASH |
Required when server.admin_addr is set |
bcrypt hash of the admin key, generated by notifycat-keygen. Must be wrapped in single quotes in .env so $ is not expanded. |
BITBUCKET_AUTH_EMAIL |
Optional (bitbucket) | Pair with BITBUCKET_TOKEN to switch to HTTP Basic with a scoped Atlassian API token (the Free-plan path for workspace-wildcard listing). Unset ⇒ Bearer. |
The server and CLIs fail fast when SLACK_BOT_TOKEN is missing, or when the webhook secret required by the selected git_provider is missing (GITHUB_WEBHOOK_SECRET for git_provider: github, BITBUCKET_WEBHOOK_SECRET for git_provider: bitbucket). They also fail fast when server.admin_addr is set without NOTIFYCAT_ADMIN_KEY_HASH, when server.admin_addr equals server.addr, and when NOTIFYCAT_ADMIN_KEY_HASH is not a valid bcrypt hash.
GITHUB_TOKEN is also read by scripts/github-webhook-create.sh, but that script creates the webhook and only needs the Webhooks: Read and write permission on a fine-grained PAT. The validate/doctor reading path needs admin:repo_hook / repo. A single token that has both works everywhere; otherwise issue separate PATs.
Infra-interpolation (environment variables only)¶
These are not secrets but must live in .env because docker-compose and Caddy read them directly and cannot access config.yaml.
| Variable | Notes |
|---|---|
DOMAIN |
Public DNS name. Caddy uses it as the virtual-host name and obtains a Let's Encrypt certificate. Required when using compose.yaml. Also set server.domain in config.yaml so notifycat-doctor can derive the webhook URL. |
ACME_EMAIL |
Contact email for Let's Encrypt registration. Required when using compose.yaml — Caddy will fail to start without it. |
config.yaml reference¶
git_provider¶
| Key | Default | Notes |
|---|---|---|
git_provider |
(required) | The git host this deployment serves — github or bitbucket. An absent or unknown value fails startup with an error naming the key and pointing here. |
git_provider selects which webhook secret is required (git_provider: github ⇒ GITHUB_WEBHOOK_SECRET, git_provider: bitbucket ⇒ BITBUCKET_WEBHOOK_SECRET; SLACK_BOT_TOKEN is always required regardless), which /webhook/... route the server registers (/webhook/github or /webhook/bitbucket), and which provider's validation probes run. A deployment serves exactly one provider and never needs the other's credentials. Switching git_provider against an existing database requires a fresh database; see Upgrading.
Bitbucket behavior notes¶
Two provider-specific behaviors are worth knowing when running git_provider: bitbucket:
- Self-healing draft/ready transitions. Bitbucket has no distinct "opened as draft" vs "marked ready" events — both arrive as
pullrequest:updated. Notifycat reads the PR'sdraftflag on each update:draft: trueconverts the tracked message to draft (deletes it), anddraft: falseon anOPENPR treats it as ready-for-review. Anupdatedevent on a non-OPENPR is ignored. This means a PR toggled draft↔ready re-announces correctly without a dedicated event. - The user-account-bot blind spot. Bot suppression keys on
actor.type != "user"(soteamandapp_useractors are treated as bots). A bot that authenticates as an ordinary Bitbucket user account reportsactor.type: "user"and is therefore indistinguishable from a human — its reviews are not suppressed even withreviews.ignore_ai_reviews: true. Prefer app-based integrations for reviewers you want suppressed.
server¶
| Key | Default | Notes |
|---|---|---|
server.addr |
:8080 |
HTTP listen address. |
server.log_level |
info |
Supported values: debug, info, warn, error. |
server.log_format |
text |
Use json for structured production logs. |
server.domain |
(unset) | Public domain name. notifycat-doctor derives https://$domain/webhook/github from this, and the server builds the agent invite redeem_command and the NOTIFYCAT_URL line of the redeem output from it. When unset, the redeem_command uses $NOTIFYCAT_URL instead, and the redeem output has no NOTIFYCAT_URL line. |
server.admin_addr |
(unset) | Listen address of the private admin server for agent invites and tokens. Must differ from server.addr. Plain HTTP; never publish it. Requires NOTIFYCAT_ADMIN_KEY_HASH. |
agent_api¶
| Key | Default | Notes |
|---|---|---|
agent_api.enabled |
false |
Serves the /api/v1/ agent routes. When on, notifycat also records the open PRs of configured repositories for the agent feed. Off means no recording, no /api/v1/ routes and no admin server. Invites can only be created when server.admin_addr is set too; with the API on and no admin_addr, notifycat logs WARN admin server not started and existing tokens keep working. Turning the API off hides the routes but does not revoke tokens; turning it on again restores them. To revoke a token, use DELETE /admin/v1/tokens/{id}. Each start with the API off removes every stored feed PR, because nothing records a close while it is off. So turning the API on again starts the feed empty, like a first enable; subscriptions to mapped repositories stay. With the API on, each start removes the stored PRs of repositories that left mappings:. With the API on or off, each start removes the subscriptions to repositories that left mappings:; subscribe again if one is mapped again. |
database¶
| Key | Default | Notes |
|---|---|---|
database.url |
file:./data/notifycat.db |
SQLite path or file: DSN. Stores the per-PR Slack message timestamps. Mappings live in config.yaml, not the database. |
For Docker, set database.url in your mounted config.yaml. The recommended value is file:/app/notifycat.db (the historical image path) or file:/data/notifycat.db if you mount a /data volume for persistence.
slack¶
| Key | Default | Notes |
|---|---|---|
slack.base_url |
https://slack.com |
Override for tests only. |
slack.reactions.enabled |
true |
Turns reaction updates on or off. |
slack.reactions.new_pr |
eyes |
Added when a PR is opened. Doubles as the leading emoji of the new-PR message. |
slack.reactions.merged_pr |
twisted_rightwards_arrows |
Added when a PR is merged. |
slack.reactions.closed_pr |
x |
Added when a PR is closed without merge. |
slack.reactions.approved |
white_check_mark |
Added when a review approves the PR. |
slack.reactions.commented |
speech_balloon |
Added when a review comments on the PR. |
slack.reactions.request_change |
exclamation |
Added when a review requests changes. |
slack.reactions.bot_review |
robot_face |
Distinct marker added alongside the normal reaction when a bot reviewer's activity is not suppressed (i.e. reviews.ignore_ai_reviews: false). Lets bot reviews stay visible but recognisable. Set empty to keep bot reviews indistinguishable from human ones. Mutually exclusive with suppression: when reviews.ignore_ai_reviews: true the bot reaction is skipped entirely, so this marker never appears. |
Use Slack emoji names without surrounding colons. For example, set approved: shipit, not :shipit:.
github¶
| Key | Default | Notes |
|---|---|---|
github.base_url |
https://api.github.com |
Override for GitHub Enterprise or tests. Used when git_provider: github. |
bitbucket¶
| Key | Default | Notes |
|---|---|---|
bitbucket.base_url |
https://api.bitbucket.org/2.0 |
Override for tests. Used when git_provider: bitbucket. |
cleanup¶
| Key | Default | Notes |
|---|---|---|
cleanup.message_ttl_days |
30 |
Days a slack_messages row may go without an update before the in-process cleanup removes it. Must be > 0. The cleanup runs once at startup and then once every 24 hours; it only deletes the DB row, never the actual Slack message. |
cleanup.delete_on_close |
false |
When true, a merged or declined PR has its Slack message deleted instead of updated with the [Merged]/[Closed] tag, and its database row is dropped. Every channel the PR fanned out to loses its message; no closing reaction is added. Overridable per repository tier. Deletion is permanent — Slack has no undo. See Remove messages on close. |
reviews¶
| Key | Default | Notes |
|---|---|---|
reviews.ignore_ai_reviews |
false |
When true, suppress reactions.add for any review event whose sender.type == "Bot" — Copilot, Claude, Codex, dependabot, github-actions, release-please, and any other GitHub App or legacy bot account. Detection is intentionally coarse: notifycat does not distinguish AI reviewers from scripted bots. See Reactions & bot reviews for the trade-off and failure-mode guide. |
reviews.dependabot_format |
true |
When true, PRs opened by dependabot[bot] or renovate[bot] post a compact Slack message instead of the standard "please review" format: :package: <bot> bumped <link> for routine bumps, or :rotating_light: <bot> security update <link> when the PR body shows a security advisory. Set false to render those PRs with the standard format. See Reactions & bot reviews → Dependabot and Renovate for detection details. |
digest¶
| Key | Default | Notes |
|---|---|---|
digest.enabled |
false |
Turns the stuck-PR digest on or off. The feature is opt-in: omitting this section — or setting only schedule:/timezone: without enabled: true — leaves the digest off. |
digest.schedule |
0 9 * * * |
Standard 5-field cron expression, evaluated in digest.timezone. An invalid expression fails server startup. |
digest.timezone |
UTC |
IANA timezone name (e.g. Europe/Kyiv) the schedule and the "stuck since before today" cutoff are evaluated in. Absent/empty means UTC. An unrecognized zone fails server startup. Global only — a per-repository digest: override may not set it. |
digest.country |
(none) | ISO 3166-1 alpha-2 country code (e.g. DE) selecting the public-holiday calendar the digest skips, evaluated in digest.timezone. Case-insensitive. Absent/empty means no holidays are skipped — weekends still are — and the server warns once at boot. An unrecognized code does not fail startup: the server warns, lists the supported set, and runs weekends-only. Global only — a per-repository digest: override may not set it. See Weekends and holidays for the supported set and its limits. |
Weekends are always skipped and there is no key for it: Saturday and Sunday never get a digest, whatever digest.schedule says.
digest.timezone and digest.country also drive the sends of the scheduled notification strategy, even when digest.enabled is false.
mappings¶
Routing from repositories to Slack channels lives in the mappings: section of config.yaml. The per-repository-tier schema (0.18+) organizes each org into named repository tiers plus an optional "*" catch-all tier. Each tier sets its own channel and optional mentions; repository tiers inherit from the "*" tier. Behavioral settings (reactions, reviews, digest) can also be overridden per-repository tier; when omitted they inherit from the "*" tier or fall back to the global values. A tier's notifications: block picks the notification strategy: per-event (default) or scheduled. See Mappings for the full schema reference and examples.
Config CLI¶
The file is operated via the notifycat-config binary — list prints the parsed config, validate checks each entry end-to-end against Slack and the git host, with results cached in config.lock so only changed entries revalidate. The server runs the same validation at boot and refuses to start on failure. Commands, checks, and the remediation table: CLI → notifycat-config.