> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0041-notification-per-channel-presence-activation.md).

# ADR-0041: Notification channels activate by the presence of their keys

## Status

**Accepted.** Reconstructed from the codebase on 2026-05-30; the decision is live in the source today.

## Context

Once the Notifications subsystem is enabled (ADR-0040), an operator still has to choose *where* alerts go: Slack, a generic webhook, email, or some combination. The platform needs a way to activate exactly the channels an operator wants — without a separate enable flag per channel that could drift out of step with whether the channel is actually configured.

## Decision

**Each channel activates by the presence of its own configuration key.** The three sender beans each carry `@ConditionalOnProperty` on the key that channel needs — `notifications.receivers.slack.url`, `notifications.receivers.webhook.url`, `notifications.receivers.email.sender`. Setting the key creates the bean (the channel is on); leaving it unset means no bean is created and that channel is silently absent. There is no separate `slack.enabled` / `email.enabled` flag — **the configuration key is the toggle.**

This composes with ADR-0040 into a **two-stage opt-in**: the subsystem must be enabled (`notifications.enabled: true`), and then each channel is activated by populating its key. An operator who enables notifications but sets no channel keys gets a running subsystem that delivers nowhere.

The presence test is for the key being set; a key set to an *empty* value is caught separately by the fail-fast empty-value check inside the sender's bean factory (ADR-0018), which aborts startup rather than building a broken sender.

## Consequences

* Operators configure only the channels they want, by populating only those keys — the common case is "set one key, get one channel."
* Adding a new channel is an add-a-bean-method change carrying its own `@ConditionalOnProperty`, not a change to a central channel registry.
* The trade-off is implicitness: an operator who *omits* a channel's key gets no warning (it is indistinguishable from deliberately not wanting that channel). A *malformed* (empty) key is the case the fail-fast check catches; an *absent* key is by-design silent.
* Channel activation is independent — one configured channel works regardless of whether the others are set.

## Evidence

* `odd-platform-api/.../notification/config/NotificationConfiguration.java:37` — `@ConditionalOnProperty(name = "notifications.receivers.email.sender")` on the `mailSender` bean; `:102` — the same key on the `emailNotificationSender` bean (the email channel needs two beans, both presence-gated on one key).
* `odd-platform-api/.../notification/config/NotificationConfiguration.java:75` — `@ConditionalOnProperty(name = "notifications.receivers.slack.url")` on `slackNotificationSender`; `:89` — `@ConditionalOnProperty(name = "notifications.receivers.webhook.url")` on `webhookNotificationSender`.
* `odd-platform-api/src/main/resources/application.yml:180-186` — the `notifications.receivers.{slack.url|webhook.url|email.sender}` keys ship empty, so no channel is active by default.

## See also

* [ADR-0040 — Notifications ship disabled by default behind one condition](/developer-guides/architecture-decision-log/adr-0040-notifications-disabled-by-default.md) — the subsystem gate; this record is the per-channel second stage.
* [ADR-0018 — Outbound-integration config is fail-fast at boot](/developer-guides/architecture-decision-log/adr-0018-fail-fast-outbound-config-at-boot.md) — the empty-value check that turns a malformed channel key into a startup failure.
