# `PhoenixKit.Notifications.Prefs`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.14.2/lib/phoenix_kit/notifications/prefs.ex#L1)

Per-user notification preferences.

Preferences live inside the user's existing `custom_fields` JSONB column
under the `"notification_preferences"` key — a flat `%{type_key =>
boolean}` map. Unset keys default to the type's own `:default` flag via
`PhoenixKit.Notifications.Types.default_for/1`, so behaviour before a
user has opted in anywhere is unchanged.

The filter function `user_wants?/2` is called once per notification
fan-out from `PhoenixKit.Notifications.maybe_create_from_activity/1`. It's
designed to fail open: any lookup error, unknown action, or malformed
prefs map returns `true` so the system never silently drops a
notification due to a bad row.

# `get`

```elixir
@spec get(PhoenixKit.Users.Auth.User.t() | String.t()) :: %{
  optional(String.t()) =&gt; boolean()
}
```

Returns the user's raw preference map.

Accepts either a loaded `%User{}` (zero DB work) or a UUID (one lookup).
Missing preferences return `%{}`.

# `merge`

```elixir
@spec merge(PhoenixKit.Users.Auth.User.t(), %{optional(String.t()) =&gt; boolean()}) ::
  {:ok, PhoenixKit.Users.Auth.User.t()} | {:error, :not_found}
```

Merge-preserving write: reads the user's current preferences and overlays only
the keys in `prefs`, so keys this caller doesn't render survive.

This is the safe write for any partial surface (the settings page renders all
keys but uses this anyway; the deprecated dashboard settings component renders
only base keys and MUST use this or it would drop every sub-type entry). Using
it by construction closes the whole "full-replace caller drops keys" class.

Note the overlay is computed from the passed `user`'s prefs, so a concurrent
write to the *same* `notification_preferences` key can still be lost; only
sibling `custom_fields` keys are protected. Callers that must not lose one
should pass a freshly-loaded user (same caveat as `ChannelConfig.update/3`).

Returns `{:error, :not_found}` if the user row was deleted concurrently.

# `update`

```elixir
@spec update(PhoenixKit.Users.Auth.User.t(), %{optional(String.t()) =&gt; boolean()}) ::
  {:ok, PhoenixKit.Users.Auth.User.t()} | {:error, :not_found}
```

Replaces the user's preference map with `prefs` (a `%{key => boolean}` map).

Full replace of the `notification_preferences` value — callers must include
every key they intend to keep, so this is for cases that own the whole map
(pause/resume, which pass a computed full snapshot). For a partial write that
must NOT drop keys it doesn't render, use `merge/2`. Other `custom_fields`
keys are preserved either way — the write touches the
`notification_preferences` key alone (see `write/2`).

Returns `{:error, :not_found}` if the user row was deleted concurrently.

# `user_wants?`

```elixir
@spec user_wants?(String.t() | PhoenixKit.Users.Auth.User.t(), String.t()) ::
  boolean()
```

Answers "would this user want a notification for this action?"

Resolves the action to its most-specific preference key (`Types.key_for_action/1`)
and applies the master switch: a base key is checked alone; a dotted sub key is
wanted only when BOTH its base master AND the sub itself are enabled. Fail-open
on every ambiguity — unknown action, missing pref (→ key default), or any raise
→ `true` — so a notification is never silently dropped by a bad row.

# `user_wants_type?`

```elixir
@spec user_wants_type?(String.t() | PhoenixKit.Users.Auth.User.t(), String.t()) ::
  boolean()
```

Like `user_wants?/2` but checks a preference **key** directly — a base type key
(`"account"`, `"posts"`) or a dotted sub key (`"comments.replies"`). Used by
`Notifications.create/1` when a caller passes `:type`. Applies the same master
switch and the same fail-open contract (unknown key → its default; crash → `true`).

---

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