# ChannelRoutingBinding

## Description

ChannelRoutingBinding maps a polymorphic source entity to a destination channel surface for the DESTINATION delivery topology. Each row binds a `(targetType, targetId)` reference to a single `channelId` and the `externalChannelRef` the destination adapter posts to (e.g. a Slack channel id `C0123456789`). When the dispatcher processes an event whose `(sourceType, sourceId)` matches a binding's `(targetType, targetId)`, it emits one post per active binding to that binding's `externalChannelRef`, independent of the event's recipient set.

The binding key is deliberately generic: a polymorphic `(targetType, targetId)` pair instead of an entity-specific foreign key, and a channel-agnostic `(channelId, externalChannelRef)` pair instead of a provider-specific id, so the notification module can route any source entity to any DESTINATION channel without knowing the entity's domain or the provider.

A binding is only meaningful for a `NotificationChannel` whose `kind = DESTINATION`. PERSONAL channels (`IN_APP`, `EMAIL`, `SMS`, `PUSH`) resolve addresses per-recipient and never consult this table. The `isActive` flag is the per-binding kill-switch: an inactive binding is skipped at dispatch exactly like an absent one, leaving the historical row intact for re-activation.

> **Scope note:** The module ships no binding-management command surface — bindings are created by the host application (seed data or direct inserts at the app layer, e.g. when the host wires a source entity to a Slack channel) and consumed read-only by the dispatcher's destination resolution loop. Each post attempt against a binding is audited on [DestinationDeliveryLog](./DestinationDeliveryLog.md).

## Domain Model Definitions

### Model type

Standard

### Command Definitions

- No module commands — bindings are created by the host application via seed data or direct app-layer inserts and consumed by the dispatcher.

### Query Definitions

- No dedicated query — bindings are read internally by `dispatchNotification` during the destination resolution loop.

### Models

- ChannelRoutingBinding

### Invariants

- `(targetType, targetId, channelId, externalChannelRef)` is unique — a given source entity may hold multiple bindings on the same destination channel as long as each routes to a distinct `externalChannelRef` (e.g. two Slack channels via distinct rows); dispatch emits one post per active binding
- `channelId` must resolve to a `NotificationChannel` whose `kind = DESTINATION`; bindings referencing a PERSONAL channel are not dispatched
- `externalChannelRef` is the opaque destination identifier the adapter posts to (e.g. a Slack channel id) and is never interpreted by the module
- `isActive = false` causes the dispatcher to skip the binding for every event; toggling back to `true` resumes routing on subsequent dispatches and never replays past events
- `targetType` / `targetId` are an opaque polymorphic reference; the module does not enforce that the referenced entity exists
- A disabled `NotificationChannel` (`enabled = false`) suppresses all of its bindings regardless of each binding's `isActive` flag

### Relationships

- **References NotificationChannel as channelId**: the destination channel this binding routes to; must be a `kind = DESTINATION` channel
- **Read by Notification dispatch (DESTINATION stage)**: `dispatchNotification` matches the event's `(sourceType, sourceId)` against `(targetType, targetId)` to find the destinations to post to
- **Audited by DestinationDeliveryLog**: each post attempt against a binding is logged in the DESTINATION-side audit, keyed by the resolved `(targetType, targetId, externalChannelRef)`
