0%
balqalam Logo
balqalam
FeaturesCommunityPricingDocumentation
Login
  • Introduction
  • Pitch
  • Hogwarts
  • Live Demo
  • MVP
  • Roadmap
  • Launch Sprint
  • PRD
  • Get Started
  • Localhost
  • Architecture
  • Structure
  • Pattern
  • Page
  • Layout
  • Content
  • Types
  • Config
  • Actions
  • Queries
  • Authorization
  • Validation
  • Form
  • Table
  • Detail
  • Card
  • Util
  • Hooks
  • List Params
  • Views
  • README.md
  • ISSUE.md
  • Technology Stack
  • Database
  • File
  • CDN Assets
  • Entry Points
  • Dashboard
  • Authentication
  • Credentials
  • OAuth
  • Flow Diagrams
  • Multi-Tenancy
  • Offline
  • Onboarding
  • Onboarding Videos
  • Add Values
  • Admission
  • Application
  • Attendance
  • Compliance
  • Profile
  • Exams
  • Exam Wizard
  • Timetable
  • Classrooms
  • Notifications
  • Conference
  • LMS (Lumos)
Finance
  • Finance
  • Fee Management
  • Invoice
  • Wallet
  • Salary
  • Payroll
  • Timesheet
  • Expenses
  • Budget
  • Receipt
  • Accounts
  • Banking
  • Reports
  • Dashboard
  • Permissions
  • Messages
  • Integration Flow
  • Provision
  • AI Document Processing
  • Document Intelligence
  • Internationalization
  • Translation
  • Translation Guide
  • Icons
  • Docs Factory
  • Inspiration
  • Listings
  • Teachers
  • Students
  • Catalog
  • Library
  • Contributing
  • Code of conduct
  • GitHub Workflow
  • Database Seeds
  • Database Safety
  • Test Accounts
  • Playwright
  • Prettier
  • Block Rebound
Sales & GTM
  • Marketing Brief
  • Sales
  • Go-to-market
  • Marketing
  • Admission — Feature Spotlight
  • Pilot Program
  • Leads
  • Proposal
  • Outreach Templates
  • Case Study
  • Competitors
  • Landing-page teardown
  • Competitor FAQ
  • Business model
  • Shared economy
  • Traction
Fundraising & Ecosystem
  • Get Support
  • Investor Deck
  • Data Room
  • Investors
  • Accelerators
  • Incubators
  • Grants
  • Sponsors
  • Partners
  • Competitions & Hackathons
  • Universities & Training Centers

Notifications

PreviousNext

Multi-channel delivery (in-app, email, push, SMS) with per-user preferences, RBAC, batch operations, and delivery tracking.

The notifications module delivers across four channels with per-user preferences, RBAC, batch operations, delivery tracking, and a polling fallback when Socket.IO is unavailable. Every domain dispatcher routes through dispatchNotification() so preference checking and expiration are uniform.

Routes

RouteRolesPurpose
/notificationsallNotification center with list and filters
/notifications/preferencesallPer-type per-channel toggle matrix

Role × type matrix

Cross-tab of which roles receive which notification groups. Y = role receives that group; — = not dispatched. STUDENT defaults email: false (in-app only unless opted in); STAFF defaults email: false; DEVELOPER can receive anything (in-app by default).

Notification groupDEVELOPERADMINTEACHERACCOUNTANTSTAFFSTUDENTGUARDIAN
attendance_alertYYY———Y
absence_intention(_decision)Y—Y———Y
assignment_*Y————Y—
grade_posted, report_readyY————YY
fee_due / fee_paid / fee_overdueY——Y—YY
class_rescheduled / class_cancelledY—Y————
account_createdYY———Y—
system_alertYYYYYY—
announcement, event_reminderYYYYY—Y
message, message_mentionYYYYYYY

Priority varies by event: most are normal; attendance_alert, assignment_due, fee_due, report_ready are high; fee_overdue and critical system_alert are urgent. SMS is reserved for attendance_alert to GUARDIAN. Email follows per-role defaults below.

Notification types (23)

GroupTypes (with default expiration)
Messagingmessage (30 d), message_mention (30 d)
Academicsassignment_created (60 d), assignment_due (7 d), assignment_graded (60 d), grade_posted (90 d)
Attendanceattendance_marked (30 d), attendance_alert (7 d), absence_intention (14 d), absence_intention_decision (14 d)
Financefee_due (30 d), fee_overdue (30 d), fee_paid (90 d)
Eventsannouncement (90 d), event_reminder (7 d), class_cancelled (7 d), class_rescheduled (7 d)
Systemsystem_alert (30 d), account_created (90 d), password_reset (1 d), login_alert (7 d), document_shared (60 d), report_ready (30 d)
PriorityUse case
lowInformational (document shared, report ready)
normalStandard (assignments, grades, attendance)
highRequires attention (fee due, attendance alert)
urgentImmediate action (fee overdue, system alert)

Recipient map

Source: src/components/school-dashboard/notifications/recipient-map.ts. Highlights:

  • attendance_alert — multiple trigger points (mark absent, threshold cron, excuse submit / decide, teacher unmarked cron). Recipients vary.
  • grade_posted, report_ready — students + guardians.
  • fee_due / fee_paid / fee_overdue — students + guardians; cron drives overdue.
  • system_alert — catch-all for membership lifecycle, payroll, exam scheduling, substitutions, application status.
  • account_created — onboarding to admins; membership approvals + enrollments to the user.
  • message, message_mention — conversation participants and mentioned users.

A handful of types are defined but not yet dispatched: attendance_marked, password_reset, login_alert, document_shared.

Architecture

src/lib/dispatch-notification.ts is the single entry point.

// single user — used by domain dispatchers
await dispatchNotification({
  userId,
  schoolId,
  type,
  priority,
  title,
  body,
  metadata,
  actorId,
})
 
// bulk by audience — school / class / role scoped
await dispatchNotificationsToAudience({
  schoolId,
  type,
  priority,
  title,
  body,
  scope: { type: "school" }, // or { type: "class", classId } or { type: "role", role }
})

dispatchNotification() checks NotificationPreference, picks channels, sets expiresAt from config, then writes the row. dispatchNotificationsToAudience() filters by preference and uses createMany({ skipDuplicates: true }).

Domain dispatchers all call dispatchNotification(). No domain code touches db.notification.create directly.

Real-time delivery uses Socket.IO with rooms keyed by user:${userId}. The client singleton is src/lib/websocket/socket-service.ts. The Socket.IO server is external to this repo — without it, the client falls back to 30 s polling against GET /api/notifications/bell (a route handler on purpose: auth() rotates the session cookie inside server-action requests, so an action-based poll would ship a full RSC page re-render every 30 s; the route returns ~2 KB of JSON). Polling is visibility-aware — hidden tabs skip the round-trip and catch up when foregrounded — and concurrent hook instances share one in-flight request.

One hard-won gotcha: when no socket server is configured, socketService.connect() resolves without a socket. Connected-state must be read from socketService.isConnected() after the promise settles — trusting the resolution disabled the polling fallback and froze the production bell for three weeks (2026-07-19 → 2026-08-11).

RBAC

ActionDEVELOPERADMINTEACHER (own classes)ACCOUNTANT (fee types)STAFF (limited)STUDENTGUARDIAN
Read ownYYYYYYY
Mark as readYYYYYYY
CreateYall typesacademic onlyfee typesdoc / event——
Send batchYYown classesY———
Update preferencesYYYYYYY

Channels

ChannelEnabledProviderNotes
in_appyesinternalBell, center, DB-backed
emailyesResend SDKHTML with RTL (Rubik font), delivery logging
pushnoWeb PushService worker exists; no VAPID keys
smsnoTwilioE.164 validation, +966 default; SMS_ENABLED=false

Default channels: every role gets in_app; STUDENT and STAFF default email: false; all other roles default email: true. push and sms are off by default for everyone (provider keys missing).

Email delivery uses Resend, renders HTML + plain-text fallback, respects per-user quiet hours, batches via cron/process-email-notifications. Dev sends to delivered@resend.dev; production sends from noreply@school.databayt.org.

Preferences

NotificationPreference is unique on [userId, type, channel]. Fields: enabled, quietHoursStart/quietHoursEnd (0–23), digestEnabled, digestFrequency (daily | weekly).

The preferences page renders one row per notification type × four channel columns plus quiet-hours and digest controls. Missing preference falls back to role defaults.

Server actions and queries

Actions in actions.ts:

ActionRolesPurpose
createNotification()staff, type-scopedCreate single
markAsRead()all (own only)Mark single as read
markAllAsRead()all (own only)Mark all as read
deleteNotification()all (own only)Delete
createBatch()admin, teacher, acctBatch by role / class / users
updatePreferences()all (own only)Update per-channel preference
subscribe() / unsubscribe()allEntity subscriptions

Queries (queries.ts): getNotifications (paginated), getNotificationsCursor (infinite scroll), getUnreadCount, getNotificationStats, getRecentNotifications, getByType, getExpired, getPreferences, getSubscriptions, getPendingEmailQueue.

Schema

model Notification {
  id        String                 @id @default(cuid())
  schoolId  String
  userId    String
  type      NotificationType
  priority  NotificationPriority   @default(normal)
  title     String
  body      String                 @db.Text
  metadata  Json?
  actorId   String?
  read      Boolean                @default(false)
  readAt    DateTime?
  channels  NotificationChannel[]
  expiresAt DateTime?
 
  emailSent Boolean? emailSentAt DateTime? emailError String?
  pushSent  Boolean? pushSentAt  DateTime? pushError  String?
  smsSent   Boolean? smsSentAt   DateTime? smsError   String?
 
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
 
  @@index([schoolId, userId, read, createdAt])
  @@index([schoolId, type])
  @@index([schoolId, createdAt])
  @@index([schoolId, emailSent])
  @@index([schoolId, pushSent])
  @@index([schoolId, smsSent])
  @@index([expiresAt])
}

Supporting models: NotificationBatch is fully wired — sendBroadcast (school/communication) creates one per broadcast, processNotificationBatch fans it out, and /api/cron/process-broadcast-batches (every 5 min, added 2026-06-12) sweeps scheduled batches whose scheduledFor has arrived plus any stuck pending batch past a 10-minute grace (previously a scheduled broadcast stayed pending forever). NotificationTemplate, NotificationSubscription, NotificationSummary remain schema only. Source: prisma/models/notifications.prisma.

Enums: NotificationType, NotificationPriority, NotificationChannel.

Cron jobs

Cron routeSchedulePurpose
/api/cron/process-email-notificationshourlyDrain pending email queue
/api/cron/process-broadcast-batchesevery 5 minFire scheduled / stuck broadcast batches
/api/cron/cleanup-notificationsdaily 2 AMDelete read+expired, anything > 90 days
/api/cron/event-remindersdaily 8 AMevent_reminder for events in next 24 h
/api/cron/assignment-remindersdaily 8 AMassignment_due
/api/cron/fee-overduedaily 9 AMfee_overdue
/api/cron/attendance-policiesdaily 1 AMattendance_alert to admins
/api/cron/teacher-remindershourly 8–16Reminder to teachers to mark attendance
/api/cron/scheduled-reportsevery 6 hreport_ready
/api/cron/publish-announcementsmidnightPublish scheduled announcements
/api/cron/expire-announcementsmidnightExpire old announcements

UI

bell-icon.tsx (badge + recent popover), card.tsx (per-item rendering), list.tsx (tabbed all/unread, date grouping), notification-center-client.tsx (SSR + socket merge), and preferences-form.tsx (per-type per-channel toggle matrix).

Hooks expose the data — dual-channel (Socket.IO when available, 30 s polling fallback), with optimistic updates. An initial fetch always runs regardless of transport (a live socket only pushes new events), and polled snapshots merge through poll-merge.ts — forward-only read-state sync, so a notification read in another tab clears here without fighting optimistic updates:

const {
  isConnected,
  unreadCount,
  recentNotifications,
  markAsRead,
  markAllAsRead,
} = useNotifications({
  autoConnect: true,
  showToast: true,
  pollInterval: 30000,
})
 
useNotificationBell() // count + 5 recent, no toast
useNotificationCenter() // all recent + toasts

See also

  • Messages — message + mention dispatch
  • Fees — fee_due / fee_paid / fee_overdue
  • Attendance — attendance_alert triggers
ClassroomsConference

On This Page

RoutesRole × type matrixNotification types (23)Recipient mapArchitectureRBACChannelsPreferencesServer actions and queriesSchemaCron jobsUISee also

Built by Databayt ·

Welcome to balqalam.

A great journey is about to begin.