# `PhoenixKit.Notifications.ChannelConfig`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.6.0/lib/phoenix_kit/notifications/channel_config.ex#L1)

Per-user configuration for notification delivery channels, stored on
`users.custom_fields` (JSONB — no migration).

**One top-level key per channel** — `"notification_channel:<key>"` — so that
`Auth.merge_user_custom_fields/3`'s atomic top-level `custom_fields || ...`
merge updates a channel without clobbering another channel or the unrelated
`notification_preferences` map. (A single shared `"notification_channels"`
object would be replaced wholesale by that shallow merge, losing concurrent
writes.)

The stored map is opaque to the core except two reserved keys the routing
layer reads:

  * `"enabled"` — channel master switch (default **on** once configured).
  * `"types"` — `%{type_key => boolean}`, per-type opt-in. Absent/unknown ⇒
    **off** (external routing is fail-closed, unlike the fail-open in-app inbox).

Channel-specific keys (e.g. Telegram's `"connection_uuid"` / `"chat_id"` /
`"status"`) live in the same map and are the channel's business.

All reads/writes take a loaded `%User{}` (the caller already has it — the
settings page has the current user, the delivery worker loads the recipient).

# `all_for`

```elixir
@spec all_for(map()) :: %{optional(String.t()) =&gt; map()}
```

Every channel config on the user, as `%{channel_key => config_map}`.

# `cadence`

```elixir
@spec cadence(map(), String.t()) :: String.t()
```

Delivery cadence for `type_key` on this channel (from the aggregation popup) —
`"immediate"` (default, send each as it happens) or a digest window
(`"hourly"` / `"12h"` / `"daily"` / `"weekly"`, batched into one summary).

# `cadences`

```elixir
@spec cadences() :: [String.t()]
```

The valid delivery cadences, in escalating order.

# `config_key`

```elixir
@spec config_key(String.t()) :: String.t()
```

The `custom_fields` key a channel's config is stored under.

# `delete`

```elixir
@spec delete(map(), String.t()) :: {:ok, map()} | {:error, term()}
```

Removes a channel's config entirely (e.g. user disconnects it).

# `enabled?`

```elixir
@spec enabled?(map()) :: boolean()
```

Channel master switch — defaults ON once the channel has any config.

# `for_channel`

```elixir
@spec for_channel(map(), String.t()) :: map()
```

The config map for one channel (`%{}` if unset).

# `immediate?`

```elixir
@spec immediate?(map(), String.t()) :: boolean()
```

True unless `type_key` is routed on a digest (non-immediate) cadence.

# `put`

```elixir
@spec put(map(), String.t(), map()) :: {:ok, map()} | {:error, term()}
```

Writes a channel's config, replacing that channel's key atomically and leaving
every other `custom_fields` key untouched. `ensure_definitions: false` because
this is an internal routing blob, not a user-facing custom-field definition.

# `type_enabled?`

```elixir
@spec type_enabled?(map(), String.t()) :: boolean()
```

Whether `type_key` is routed to this channel — fail-CLOSED (default off).

# `update`

```elixir
@spec update(map(), String.t(), (map() -&gt; map())) :: {:ok, map()} | {:error, term()}
```

Read-modify-write a channel's config with `fun`. Reads the config off the
passed `user`; callers that must not lose a concurrent same-channel write
should pass a freshly-loaded user.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
