Per-user notifications driven by PhoenixKit.Activity.
When an activity is logged with a target_uuid that differs from the
actor_uuid, a row is inserted into phoenix_kit_notifications for the
target user. The user sees it in the bell dropdown (count_unread/1,
recent_for_user/2) and in the inbox at /notifications (list_for_user/2).
Each row carries its own seen_at and dismissed_at — the same activity
can be "seen but not dismissed" for one user and "unseen" for another.
The whole feature is gated by the global notifications_enabled setting
(default "true"); when "false", maybe_create_from_activity/1 is a no-op.
Registered as a core toggleable module (use PhoenixKit.Module) so it
appears on the admin Modules page and contributes the /admin/notifications
overview tab. The module enable/disable flips the same
notifications_enabled kill-switch enabled?/0 reads.
Summary
Functions
Aggregate counts for the admin overview page: total notifications,
unread (neither seen nor dismissed), and dismissed. A single
count(...) FILTER (WHERE ...) query — one table scan, not three.
Rescues to zeros so the page never crashes on a query hiccup.
Counts undismissed, unseen notifications for a user. Drives the badge.
Create a standalone notification — one not tied to an activity (V126). Use for app-driven notices that don't originate from the activity log (e.g. "your export is ready").
Dismisses a single notification. Idempotent.
Bulk-dismisses all undismissed notifications. Returns {count, nil}.
Is the notifications feature enabled? Default true.
Fetches one notification scoped to the recipient. Returns nil if missing.
Returns {notifications, total_count} for the given user, newest first.
Bulk-marks all unseen notifications as seen. Returns {count, nil}.
Marks a single notification as seen. Idempotent — already-seen rows return
{:ok, notification} unchanged.
Inserts a notification for the activity's target user, if the rules allow it.
Deletes notifications whose underlying activity is older than days.
Returns the N most-recent undismissed notifications for a user.
Retention period in days. Falls back to activity retention if unset.
Functions
Aggregate counts for the admin overview page: total notifications,
unread (neither seen nor dismissed), and dismissed. A single
count(...) FILTER (WHERE ...) query — one table scan, not three.
Rescues to zeros so the page never crashes on a query hiccup.
Counts undismissed, unseen notifications for a user. Drives the badge.
Create a standalone notification — one not tied to an activity (V126). Use for app-driven notices that don't originate from the activity log (e.g. "your export is ready").
attrs keys:
:recipient_uuid(required) — who receives it:text/:icon/:link— convenience, folded intometadataasnotification_text/notification_icon/notification_link(the keysRenderreads):metadata— raw metadata map (merged under the convenience keys):type— optional notification type key (e.g."account","posts", or a module-contributed type). When given, the send is filtered through the recipient's per-type preference (Prefs.user_wants_type?/2, fail-open).:action— optional action string (e.g."post.commented"). When given, filtered throughPrefs.user_wants?/2(which maps the action to a type). Use:typeOR:action, not both.Notifications.create(%{ recipient_uuid: user.uuid, text: "Your export is ready.", icon: "hero-arrow-down-tray", link: "/exports/123" })
Honors the global notifications_enabled kill-switch. With neither
:type nor :action, it's an unconditional app-driven send (no
preference filtering). Returns {:ok, %Notification{}},
{:ok, :skipped} (disabled or filtered out by prefs), or
{:error, changeset}. Broadcasts {:notification_created, n} on success.
Create a standalone notification for many recipients in one call —
the multi-recipient counterpart to create/1. recipient_uuids is a
list; attrs is the same shape as create/1 minus :recipient_uuid
(it's supplied per recipient).
The recipient list is the caller's responsibility (e.g. the followers
of an author) — this is the generic fan-out primitive, not an audience
resolver. Duplicate uuids are de-duped. Each recipient is filtered
independently through :type / :action prefs when given, so muted
users are skipped. Honors the kill-switch once up front.
Notifications.create_many(follower_uuids, %{
type: "posts",
text: "Alice published a new post.",
link: "/posts/#{post.id}"
})Returns {:ok, created_count} (notifications actually inserted, i.e.
excluding disabled / pref-skipped) or {:ok, :skipped} when
notifications are globally disabled.
Dismisses a single notification. Idempotent.
Bulk-dismisses all undismissed notifications. Returns {count, nil}.
Is the notifications feature enabled? Default true.
Fetches one notification scoped to the recipient. Returns nil if missing.
Returns {notifications, total_count} for the given user, newest first.
Options:
:page(default 1) /:per_page(default 25):status—:unread(seen_at nil) |:all(default):include_dismissed— include dismissed rows (defaultfalse)
Bulk-marks all unseen notifications as seen. Returns {count, nil}.
Marks a single notification as seen. Idempotent — already-seen rows return
{:ok, notification} unchanged.
Inserts a notification for the activity's target user, if the rules allow it.
Returns one of:
{:ok, %Notification{}}— row created; broadcast on the per-user topic{:ok, :skipped}— filtered out (no target, self-action, feature disabled){:error, changeset}— insert failed (logged, never raised)
Deletes notifications whose underlying activity is older than days.
Returns the N most-recent undismissed notifications for a user.
Drives the bell dropdown. Activity (and actor) are preloaded.
Retention period in days. Falls back to activity retention if unset.