A Notification is an in-app or email alert delivered to a single user when something in Catalio needs their attention. Notifications route through both channels — the in-app bell icon and the user’s email — based on per-type preferences each user controls in Settings → Notifications.
Every notification is scoped to a single user within a single Organization, so multi-tenancy is enforced at the data layer: a user only ever sees notifications for the organization they are currently working in.
Notification Types
Catalio emits five notification types, each tied to a specific platform event:
| Type | Triggered When |
|---|---|
component_sync |
A connected Repository finishes a sync and surfaces new Components |
mention |
Another user mentions you in a comment or discussion |
comment |
A new comment is posted on a Catalio entity you own or follow |
announcement |
A system-wide announcement is published by an admin |
proposals_pending |
One or more Change Proposals are awaiting your review or approval |
The notification_type field on each record uses these exact atom values, and they drive both the routing (which preference toggle applies) and the UI rendering (icon, color).
Delivery Channels
Each notification can be delivered via two channels, controlled independently:
In-app — Appears in the bell icon dropdown and the /notifications index. Delivered in real time via Phoenix PubSub on the topic notifications:{user_id}, so an open browser tab sees the new notification without a refresh.
Email — Delivered to the user’s registered email address via the background email worker. Email delivery respects quiet hours (see below) and the configured digest frequency.
A single notification record can route to one channel, both, or neither, depending on the user’s preferences for that type at the moment it is created.
User Preferences
Every user has a notification_preferences map on their profile, configured at Settings → Notifications. The defaults are:
| Preference | Default Value | What It Controls |
|---|---|---|
inapp_proposals_pending |
true |
In-app notification when a change proposal needs review |
email_proposals_pending |
true |
Email notification when a change proposal needs review |
inapp_component_sync |
true |
In-app notification when a repository sync surfaces Components |
email_component_sync |
false |
Email notification for the same event |
inapp_mentions |
true |
In-app notification when you are @-mentioned |
email_mentions |
false |
Email notification when you are @-mentioned |
inapp_announcements |
true |
In-app notification for system announcements |
email_announcements |
false |
Email notification for system announcements |
inapp_realtime |
true |
Toggle real-time PubSub delivery (vs. waiting for a refresh) |
email_digest_frequency |
"never" |
Roll up emails into a digest (e.g., daily) instead of per-event |
quiet_hours_enabled |
false |
Suppress email delivery during a configured window |
quiet_hours_start |
"22:00" |
Start of the quiet window (UTC, 24-hour format) |
quiet_hours_end |
"08:00" |
End of the quiet window (UTC, 24-hour format) |
Defaults lean toward “in-app yes, email no” for ambient events (component sync, mentions, announcements) and “in-app + email” for the two events that typically require a response — proposals pending review.
Quiet Hours
When quiet_hours_enabled is true, email notifications generated during the configured window are suppressed. In-app notifications still arrive in the bell — the user can read them when they next open Catalio — but no email is sent during the window. The window can wrap midnight (e.g., 22:00 → 08:00 is treated as overnight), and times are currently evaluated in UTC.
Quiet hours apply to direct email delivery only; the digest frequency setting independently controls whether emails accumulate into a roll-up.
Lifecycle and Actions
Notifications have a minimal, immutable-ish lifecycle:
created → (unread) → mark_as_read → (read)
↘ mark_all_as_read → (read)
↘ destroy → (removed)
The read_at timestamp is nil for unread notifications and set to the current time when the user marks the record read. The two read actions are:
:mark_as_read— Update a single notification.:mark_all_as_read— Bulk-update every unread notification belonging to the user (within their current organization). This action broadcasts a single:all_notifications_readPubSub message rather than one per record, so the in-app inbox clears efficiently.
There is no “mark unread” action — once read, a notification stays read. Users who want to revisit a notification can find it in the /notifications index filtered by All or Read.
Key Fields
| Field | Purpose |
|---|---|
| title | Short headline shown in the bell dropdown and at the top of the notification card |
| body | Longer descriptive text rendered below the title |
| notification_type | One of component_sync, mention, comment, announcement, proposals_pending — drives icon and routing |
| metadata | Contextual map (e.g., link, resource ID, mention source) — used by the UI to deep-link to the source entity |
| read_at | nil until the user marks the notification read; set to a timestamp afterward |
| user_id | The recipient — every notification has exactly one |
| organization_id | Multi-tenant scope; the organization the notification belongs to |
| created_by_id | Optional — the user (if any) whose action triggered the notification |
| inserted_at | When the notification was created — drives the time_ago calculation shown in the UI |
Authorization
Notification policies enforce that:
- Only the recipient (
user_id) can read, update, or destroy their own notifications — verified viarelates_to_actor_via(:user). - Creation is open: any actor can create a notification, because notifications are generated by background workers and platform events, not by direct user action.
- The bulk
:mark_all_as_readgeneric action filters internally byuser_idso a caller can only clear their own inbox.
Real-Time Delivery
The Notification resource is wired to declarative Ash.Notifier.PubSub. On :create and :mark_as_read, the framework publishes a Phoenix.Socket.Broadcast message to notifications:{user_id} via CatalioWeb.Endpoint. The notification settings index LiveView subscribes to that topic at mount time and updates the bell badge and the visible list as messages arrive.
Phoenix.PubSub.broadcast/3 is used for the :all_notifications_read message because it is a generic action (not a create/update/destroy), and Ash notifiers cannot fire from generic actions.
Relationships at a Glance
| Related Concept | Relationship |
|---|---|
| Users | Each notification has exactly one recipient (user_id) and optionally one trigger (created_by_id) |
| Organizations | Notifications are organization-scoped — multi-tenancy is enforced via the organization_id attribute |
| Change Proposals | Generate proposals_pending notifications when they enter the review queue |
| Repositories | Generate component_sync notifications when a sync completes |
Best Practices
Leave the proposals_pending defaults alone unless you are flooded.
The two proposals_pending defaults are the only ones that ship with email enabled. They are intentionally noisier because pending proposals block forward motion on an Initiative — if you mute them, your inbox cleans up but your delivery slows down. If volume becomes a problem, switch email_digest_frequency from "never" to a daily digest rather than muting outright.
Use quiet hours instead of muting categories.
Quiet hours preserve in-app delivery (the bell still updates) while suppressing email outside working hours. Muting a category turns off both channels permanently — that is rarely what you want.
Mark all as read at the end of a working session.
The bell badge is most useful when it represents actual new attention items. Users who let unread notifications accumulate lose the signal-to-noise advantage. The /notifications page has a one-click Mark all as read button for exactly this.
Trust the deep-links in metadata.
Most notifications carry a metadata.link (or equivalent) that routes directly to the source entity — the requirement that was commented on, the proposal awaiting review, the repository that just synced. Following the link is faster than navigating through the entity tree.
Next Steps
- Configure your Settings — Learn how user preferences and profile fields work
- Review Change Proposals — Understand the main source of
proposals_pendingnotifications - Connect a Repository — Trigger
component_syncnotifications when code lands
Pro Tip: If a teammate is missing notifications they should be receiving, check three things in order: (1) their notification_preferences toggles for that type, (2) whether quiet_hours_enabled is active during the event window, and (3) whether email_digest_frequency has rolled the event into a pending digest rather than sending immediately.
Support
- Documentation: Continue exploring core concepts to understand what generates each notification type
- In-App Help: The AI assistant can explain why a specific notification was delivered or suppressed
- Email: support@catalio.ai
- Community: Share notification-tuning patterns with other Catalio users