Skip to content

Notifications Module

Location: apps/api/src/modules/notifications/

Aggregate roots: Notification, NotificationPreference, PushToken

The Notifications context is purely a listener — it never calls other module services directly. It subscribes to domain events and creates Notification rows, publishes real-time messages via Centrifugo, and delivers Expo push notifications. A daily digest email is sent at 09:00 UTC.


Prisma Models

ModelTableNotes
NotificationnotificationsIn-app inbox row
NotificationPreferencenotification_preferencesPer-user channel + kind toggles
PushTokenpush_tokensExpoPushToken per device

Notification Fields

FieldTypeNotes
idUUID PK
userIdUUID FKRecipient
kindStringSee kind list below
titleStringHuman-readable title
bodyString?Optional detail line
linkString?Deep link path
payloadJson?Structured context
readAtDateTime?Null = unread
createdAtDateTime

Rollup logic: same (kind, link) within 1 hour collapses into a single row with count incremented and title updated to "N people reacted to…".

NotificationPreference Fields

FieldTypeNotes
userIdUUID PK
channelsJson{ push: bool, email: bool, inApp: bool } — default all true
kindsJsonPer-kind toggles { "post.replied": false } — default empty (all on)

Notification Kinds

KindTriggerDescription
user.welcomeidentity.user.registeredWelcome message to new user
invite.usedidentity.invite_code.usedInviter notified when code used
circle.invitedsocial.invitation.sentInvitee receives circle invite
circle.removedsocial.member.removedMember notified of removal
message.postedmessaging.message.postedNew message in conversation
message.startedmessaging.member.addedAdded to group conversation
post.repliedposts.repliedReply on your post
post.reactedposts.reactedReaction on your post
post.repostedposts.repostedYour post was reposted
post.bookmarkedposts.bookmarkedYour post was saved
post.mentionedmention.detected (post)Mentioned in a post
reply.mentionedmention.detected (reply)Mentioned in a reply
post.publishedposts.scheduled.publishedScheduled post went live
post.hiddenpost.hiddenYour post was moderated
post.restoredpost.unhiddenYour post is visible again
post.milestoneInternal (reaction count)Post hit 5 / 15 / 50 unique reactors
ai.session.completedai.session.completedReflection session ready
reputation.changedreputation.milestoneLevel-up notification
reputation.votedreputation.vote.castPeer voted on you
account.restoreduser.unbannedAccount ban lifted
account.suspendeduser.bannedAccount suspended
social.followedsocial.user.followedSomeone followed you
interest.new_postposts.createdNew post in followed interest
system.broadcastadmin.broadcast.sentAdmin broadcast message
economy.payment.receivedeconomy.payment.sentMana received
economy.post.boostedeconomy.post.boostedPost boost active
series.new_partseries.new_partNew part in followed series
wiki.proposal.approvedwiki.proposal.approvedYour wiki edit approved
posts.coauthor.invitedposts.coauthor.invitedCo-author invitation

HTTP Endpoints

All under /api/v1/notifications/. Require JwtAuthGuard.

MethodPathDescription
GET/notificationsList my notifications (?limit=50&onlyUnread=true)
GET/notifications/unread-countReturns { count }
POST/notifications/:id/readMark one notification read
POST/notifications/read-allMark all as read
GET/notifications/preferencesGet channel + kind prefs
PATCH/notifications/preferencesUpdate prefs { channels?, kinds? }

Push Delivery (PushService)

  • Uses Expo Push API in batches of 100 tokens.
  • On DeviceNotRegistered error, the token row is deleted.
  • sendToUsers(userIds, message) fans out to all registered tokens for those users.
  • Badge count is passed for iOS (incremented per unread count).
  • Push is best-effort: errors are logged, not thrown.

Daily Digest (09:00 UTC)

A @Cron(EVERY_DAY_AT_9AM) job:

  1. Groups users with ≥1 unread notification in the past 24h.
  2. Skips users who opted out of email channel.
  3. Sends a branded HTML email listing up to 5 notifications with deep links.
  4. Skips deleted accounts (email.endsWith('@deleted.rgls')).

Regulus — invite-only social-knowledge platform