# NotificationChannel

## Description

NotificationChannel is the registry of delivery mediums recognized by the notification module. Each row identifies a single channel via a `channelId` enum (`IN_APP`, `EMAIL`, `SMS`, `PUSH`, `SLACK`, `TEAMS`), declares its delivery topology via a `kind` enum (`PERSONAL` | `DESTINATION`), carries a human-readable `displayName`, declares a `capabilities` block (`supportsHtmlBody`, `supportsAttachments`, `supportsRichActions`) used by template rendering for capability-based branching, and exposes an `enabled` admin toggle that acts as a tenant-wide kill-switch for the channel.

The `kind` field selects which delivery topology the dispatcher uses for the channel. A `PERSONAL` channel (`IN_APP`, `EMAIL`, `SMS`, `PUSH`) resolves a per-recipient address (`addresses[channelId]`) and persists one Notification per recipient. A `DESTINATION` channel (`SLACK`, `TEAMS`) resolves a `ChannelRoutingBinding` for the event's `(targetType, targetId)` and emits one post per active binding to a shared surface, independent of the recipient set. See the notification-destination-delivery feature for the two-topology dispatch contract.

The registry is the source of truth that the dispatcher consults before invoking any channel adapter port. When a channel is registered with `enabled = false`, the dispatcher skips it for every event and creates no Notification row for that channel — the same behavior applied when a reserved channel has no adapter implementation. The active PERSONAL set is `IN_APP` plus the optionally-wired `EMAIL`; `SLACK` is an active DESTINATION channel, dispatched through the destination stage with its concrete adapter bundled as the opt-in Slack destination (`slack` option). `SMS`, `PUSH`, and `TEAMS` are reserved enum values without adapter implementations and are not dispatched to. Concrete adapter implementations (e.g., the SendGrid-backed EMAIL adapter) are wired at the application's composition layer; the registry itself only describes the medium and its capabilities, never the provider.

## Domain Model Definitions

### Model type

Standard

### Command Definitions

- [createNotificationChannel](../command/CreateNotificationChannel.md) - Register a new channel with its capabilities and initial `enabled` flag
- [updateNotificationChannel](../command/UpdateNotificationChannel.md) - Update display name and capabilities of an existing channel
- [activateNotificationChannel](../command/ActivateNotificationChannel.md) - Flip `enabled` to true so the dispatcher resumes routing events to the channel
- [deactivateNotificationChannel](../command/DeactivateNotificationChannel.md) - Flip `enabled` to false so the dispatcher skips the channel for every event

### Query Definitions

- [getNotificationChannel](../query/GetNotificationChannel.md) - Retrieve a single channel registration by `channelId`
- [listNotificationChannels](../query/ListNotificationChannels.md) - List all registered channels with their capabilities and `enabled` state

### Models

- NotificationChannel

### Invariants

- `channelId` must be one of the defined enum values (`IN_APP`, `EMAIL`, `SMS`, `PUSH`, `SLACK`, `TEAMS`) and is unique across the registry
- `kind` must be `PERSONAL` or `DESTINATION`; `IN_APP`/`EMAIL`/`SMS`/`PUSH` are `PERSONAL` and `SLACK`/`TEAMS` are `DESTINATION`. The dispatcher uses `kind` to choose between per-recipient fan-out and destination-binding resolution
- `kind` is derived from `channelId` at registration time per the fixed mapping above — it is never a caller-supplied input and is immutable for the lifetime of the row
- A `DESTINATION` channel is addressed through `ChannelRoutingBinding.externalChannelRef`, never through a per-recipient address; a `PERSONAL` channel is never bound through `ChannelRoutingBinding`
- `displayName` must be non-blank
- `capabilities.supportsHtmlBody`, `capabilities.supportsAttachments`, and `capabilities.supportsRichActions` are each booleans and represent the channel's intrinsic medium capabilities, not provider-specific configuration
- `enabled = false` causes the dispatcher to skip the channel for every event; toggling back to `true` resumes routing on subsequent dispatches and never replays past events
- A reserved `channelId` with no registered adapter is treated by the dispatcher identically to a disabled channel
- The capabilities block is read by template rendering at dispatch time for capability-based branching; it is mutable only through `updateNotificationChannel`, and capability changes apply to subsequent dispatches only (already-persisted Notification rows are never mutated)

### Relationships

- **Referenced by NotificationTemplate as channelId**: each template is keyed by `(eventType, channelId, locale)` and selects a row in this registry
- **Referenced by NotificationPreference as channelId**: per-user opt-in/opt-out rows are keyed by `(userId, categoryId, channelId)`
- **Referenced by EventCategoryBinding as defaultChannels[]**: the seeded eventType→category binding lists the channels the dispatcher fans out to by default
- **Referenced by Notification as channelId**: every persisted Notification row records the channel it was dispatched on
- **Referenced by ChannelRoutingBinding as channelId (DESTINATION only)**: destination bindings route a `(targetType, targetId)` to a `DESTINATION` channel in this registry

> **Channel reference conventions.** Two reference styles intentionally coexist. Models that pin a specific channel **row** by foreign key use a `uuid` `.relation()` toward this registry — `Notification.channelId`, `NotificationTemplate.channelId`, `NotificationPreference.channelId`. Models on the broadcast/registry path that key by the channel **identifier** store the `channelId` enum value as a plain string — `ChannelRoutingBinding.channelId`, `DestinationDeliveryLog.channelId`, and `EventCategoryBinding.defaultChannels[]`. Both resolve to the same registry; the split reflects FK-by-row vs key-by-enum semantics and is not an inconsistency.
