Skip to content

Notification System: AtramNotification and Slack Ops Alerts

Yaya Engine has two independent notification systems that share nothing but the word "notification." AtramNotification is the base class for every user-facing message the product sends outside the normal conversational reply — task reminders, verification results, product announcements, account issues — and its job is to pick the single best channel a given user can actually be reached on. SlackNotificationService is unrelated: it is how the backend tells the operations team, in Slack, that something needs attention (a payout request, a failed batch, an AI outage). Confusing the two is easy from the name alone, so this page treats them as the separate systems they are.

AtramNotification's channel ladder

AtramNotification is an abstract Illuminate\Notifications\Notification subclass, ShouldQueue by default, whose via() method is declared final [@atram-notification]. That matters: subclasses cannot override routing logic at all, only narrow which channels they support through supportedChannels(), which defaults to all four — Telegram, Pwa, WhatsApp, Sms [@atram-notification]. via() delegates the actual decision to NotificationChannelResolver::resolve(), which walks a fixed ladder — Telegram, then PWA push, then WhatsApp, then SMS — and returns the first channel in that ladder the user is reachable on, intersected with what the notification itself supports [@atram-design]. Reachability is a simple existence check per channel: a channel_accounts row for Telegram or WhatsApp, at least one push_subscriptions row for PWA, and for SMS either a channel_accounts row or a non-empty phone_number [@atram-design]. This is a deliberately separate resolver from the alert-focused ChannelResolver used by Messaging Pipeline's batch sends — that resolver carries alert-specific concerns like a preferred-channel hint and an allow-fallback flag that don't belong on a general notification base class, even though both share ChannelTypeEnum [@atram-design].

A resolved channel maps to a concrete Laravel notification-channel class through CHANNEL_CLASS_MAP: telegram to App\Notifications\Channels\TelegramChannel, pwa to the existing NotificationChannels\Fcm\FcmChannel package, whatsapp to App\Notifications\Channels\WhatsAppChannel, and sms to App\Notifications\Channels\SmsChannel [@atram-notification]. Each of the three custom channel classes is a thin translator: it calls the notification's to{Channel}() method to get a channel-specific message DTO, converts that into the input the existing app/Services/Messaging/Channels/* service expects, and delegates the actual send to that service — so AtramNotification reuses the same WhatsApp/Telegram/SMS sending code the conversational pipeline uses, rather than duplicating it [@atram-design].

Subclasses and what they're for

PushNotification is the one concrete, non-typology notification: it takes title/body/URL/tag/image at construction time rather than pulling copy from a lang file, and it supports [Pwa, Telegram] — used by an ad-hoc Filament test button and one-off broadcasts where the message isn't part of a fixed typology [@push-notification]. Every other notification subclass is grouped by concern and brings its own translation-file copy per channel: Engagement (ActionDropoffReminder, PreAlertReengagement, SeasonalPreparednessKickoff, TaskReminder, TaskWindowClosing) [@notification-groups], Verification (MaxResubmissionsReached, ResubmissionReceived, VerificationApproved, VerificationRejected) [@notification-groups-verification], Product (AppUpdateAvailable, NewFeatureAnnouncement) [@notification-groups-product], and Account (AccountIssue) [@notification-groups-account] — all extending AtramNotification directly and inheriting its FCM-delivery-tracking trait [@push-notification].

One listener for delivery bookkeeping

Before AtramNotification existed, delivery logging was ad hoc per channel. Now a single RecordNotificationDelivery listener on Laravel's NotificationSent/NotificationFailed events writes a Message row and a MessageDeliveryLog row for any notification that is an instance of AtramNotification, filtering out everything else — Slack ops notifications, Laravel's own password-reset notifications — so those don't pollute the same delivery tables alerts use [@atram-design]. The schema change that made this possible loosened messages.conversation_id to nullable (notifications, unlike conversational replies and alerts, don't belong to a conversation) and added notification_type and notification_id columns so ops tooling can query "did user Y receive notification X" the same way it already queries alert delivery [@atram-design]. The reasoning behind this whole redesign — replacing what used to be per-channel, ad-hoc notification logic — is recorded in AtramNotification Routing.

Slack: a separate, ops-only system

SlackNotificationService::send($notification, $channelKey) is the single entry point for internal alerts: it checks config('slack.notifications.enabled') (a global off switch), resolves $channelKey against config('slack.notifications.channels.*'), and if the notification implements ThrottledSlackNotification, checks a RateLimiter keyed on the notification's own throttle key before finally dispatching through Notification::route('slack', $channel)->notify(...) [@slack-service]. This is entirely separate infrastructure from AtramNotification: Slack notifications are not routed by user reachability at all, they are addressed directly to a named Slack channel key, and they are never written to Message/MessageDeliveryLog because RecordNotificationDelivery explicitly filters them out.

The configured destination channels are operational, not user-facing: #yaya-verifications for evidence uploads awaiting review, #yaya-payouts for payout requests, #yaya-alerts for alert dispatch lifecycle and batch failures, #yaya-system-health for AI and job failures, #yaya-operations for the daily digest and expired-action sweeps, and #yaya-onboarding for new-user celebrations [@slack-docs]. Concrete notification classes include ActionsExpiredNotification, AiProcessingFailedNotification (throttled — this is the one Messaging Pipeline's ProcessIncomingMessage fires on a Dify failure), AlertDispatchedNotification, BatchCompletedWithFailuresNotification, BatchJobFailedNotification, EvidenceUploadedNotification, PayoutRequestedNotification, UserOnboardedNotification, and DailyDigestNotification [@slack-docs]. Disabling the whole system is one environment variable (SLACK_NOTIFICATIONS_ENABLED=false); there is no per-notification-type toggle — it is all or nothing [@slack-docs].