Skip to main content

Channels

A channel is out-of-band delivery to one recipient — the last hop after policy has already decided that someone should be notified. The channel adapter interface is deliberately dumb: it only transmits an already-rendered notification.

type Recipient struct {
SubjectID string
Address string // adapter-specific: email address, phone number, device token, webhook URL...
}

type Rendered struct {
Title string
Body string
Data map[string]any
}

type Channel interface {
Send(ctx context.Context, recipient Recipient, rendered Rendered) error
}

Rendering (template selection, localization, channel-specific formatting) happens upstream, in the policy layer — not in the channel adapter. Resolving a subject_id to a channel-specific address (an email, a phone number) is a policy/identity-map concern too, not this adapter's job.

Today: in-app only

The only channel wired up is inapp — delivered via the same in-process path every event's notification fan-out already runs through, not a separate outbound integration. Concretely, internal/adapters/channel/inapp.Channel.Send persists one Notification per (tenant_id, subject_id, event_id) — delivery time, read time (nil until marked read), title/body/data — idempotent on re-send (re-saving the same key preserves the original delivery time). This is the record an inbox UI would read; GET /v1/feed itself reads the event store directly rather than this package (see Events & the envelope), so the two stay in sync without a second write path.

:::info Coming later Email, WhatsApp, Teams, Slack, SMS, and push are all named in the design as Phase-2 channel adapters, behind this same interface — none are implemented yet. Wiring one is additive: the policy engine's channels field already carries adapter names, it's just informational until more than one channel adapter exists to route between. :::