PhoenixKit.Notifications.ChannelConfig (phoenix_kit v2.4.0)

Copy Markdown View Source

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).

Summary

Functions

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

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).

The valid delivery cadences, in escalating order.

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

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

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

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

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

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.

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

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.

Functions

all_for(user)

@spec all_for(map()) :: %{optional(String.t()) => map()}

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

cadence(config, type_key)

@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()

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

The valid delivery cadences, in escalating order.

config_key(channel_key)

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

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

delete(user, channel_key)

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

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

enabled?(config)

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

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

for_channel(user, channel_key)

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

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

immediate?(config, type_key)

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

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

put(user, channel_key, config)

@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?(config, type_key)

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

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

update(user, channel_key, fun)

@spec update(map(), String.t(), (map() -> 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.