Appearance
Notifications
The notification and communications system handles all in-app alerts, push notifications, emails, and preference management across DoCurious, balancing engagement with restraint -- especially for minors.
STATUS: BUILT
The in-app notification center, notification store, preference management UI, temporal grouping, mock API, email delivery (nodemailer with branded templates), per-category contextual nav badges, toast action buttons, quiet hours enforcement, and digest frequency support are all built. Push notifications and SMS are planned for post-launch.
Spec source: Doc 11 -- Notification & Communications System | Last updated: Feb 2026
Overview
Notifications in DoCurious exist to inform, not to nag. The system is designed around six guiding principles from the spec:
- Inform, don't nag -- notifications help users; they never create anxiety
- Respect attention -- especially for minors. No dark patterns, no urgency manipulation
- Channel-appropriate -- right message, right channel, right time
- User control -- every notification type can be individually configured
- COPPA-aware -- under-13 communications go through parents where required
- CAN-SPAM compliant -- all marketing emails include unsubscribe and physical address
The system uses an event-driven model: platform events (a Track Record getting verified, a gift being sent, a badge being earned) flow through a notification service that resolves recipients, checks preferences, applies COPPA rules, deduplicates, enforces frequency caps and quiet hours, selects channels, renders templates, delivers, and tracks results.
Architecture Flow
[Platform Event] --> [Notification Service] --> [Channel Router] --> [Delivery]
| |
[User Preferences] [In-App / Push / Email]
[COPPA Checks] [Template Engine]
[Frequency Caps] [Delivery Queue]
[Quiet Hours] [Tracking]Processing Pipeline
Each notification passes through these steps in order:
- Event emitted -- e.g.,
track_record_verified - Recipient resolution -- who should be notified?
- Preference check -- has the recipient opted out of this type?
- COPPA check -- is the recipient under 13? Route through parent?
- Deduplication -- has this exact notification been sent recently?
- Frequency cap check -- has the recipient exceeded notification limits?
- Quiet hours check -- is it within the recipient's quiet hours?
- Channel selection -- in-app always; push/email based on preferences
- Template rendering -- populate template with event data
- Delivery -- send via appropriate channel
- Tracking -- log delivery, opens, clicks
How It Works
Priority Levels BUILT
Every notification has a priority that determines delivery behavior. The type system defines four levels in NotificationPriority:
| Priority | Behavior | Examples |
|---|---|---|
| Critical | Always delivered immediately, all channels. Bypasses quiet hours. | Account security alerts, account suspension |
| High | Delivered promptly, respects quiet hours | TR verified, assignment due soon, gift received |
| Medium | Batched in digest if user prefers | New community post, challenge recommendation, streak reminder |
| Low | Digest only, never push | Platform announcements, feature tips, weekly summary |
File: src/types/notification.types.ts -- defines NotificationPriority as 'low' | 'medium' | 'high' | 'critical'
Delivery Channels PARTIAL
The spec defines four delivery channels. Currently only in-app is fully operational:
| Channel | Status | Description |
|---|---|---|
| In-App | Built | Bell icon notification center, full-page notification list, unread badges |
| Push | Not started | Web Push API via Service Worker (VAPID auth). Mobile push planned for native apps. |
| Not started | Transactional + digest emails via ESP (SendGrid, Postmark, or AWS SES) | |
| SMS | Planned | Future channel, no spec detail yet |
File: src/types/notification.types.ts -- defines NotificationChannel as 'in_app' | 'email' | 'push'
Notification Categories BUILT
The type system defines 13 notification categories. Each maps to a toggle group in the preferences UI, and role-specific categories are only shown to users with the matching role:
| Category | Description | Role-Specific? |
|---|---|---|
challenge_updates | Challenge status changes, new assignments | No |
social_activity | Comments, thumbs up, community activity | No |
community_activity | New posts, member activity | No |
reminders | Due dates, incomplete challenges | No |
digests | Daily/weekly summaries | No |
system | Account, security, platform updates | No |
gifts | Gift received, gift reminders | No |
verification | TR approved/rejected | No |
teacher_feedback | Teacher comments on TRs | No |
gamification | XP gains, level ups, badge earned | No |
parent_alerts | Child activity, consent requests | Parent only |
school_admin | Roster changes, compliance alerts | SA only |
vendor | Challenge performance, payouts | Vendor only |
File: src/types/notification.types.ts -- NotificationCategory type union
In-App Notification Center BUILT
The notification center is the primary delivery surface. It consists of two components:
Bell icon dropdown (NotificationCenter.tsx): Lives in the global header. Shows a badge count of unread notifications (capped at "99+"). Clicking opens a dropdown panel with the 10 most recent notifications. Each item shows a category icon, title, message preview, and relative timestamp. Includes "Mark all read" and a link to notification settings. Polls for updated counts every 60 seconds.
Full-page list (Notifications.tsx): Accessible via "View all notifications" link in the dropdown or direct navigation to /notifications. Shows all notifications with All/Unread filter tabs. Supports pagination (20 per page) with "Load More." Each notification can be clicked to mark as read and navigate to related content. Individual notifications can be deleted.
Key files:
src/components/notifications/NotificationCenter.tsx-- bell icon dropdown componentsrc/pages/Notifications.tsx-- full-page notification listsrc/store/useNotificationStore.ts-- Zustand store with pagination, read/unread state, and preference managementsrc/api/notification.api.ts-- mock API with CRUD operationssrc/api/notification.real.api.ts-- real backend API stubs
Notification Data Model BUILT
Each in-app notification contains:
| Field | Type | Description |
|---|---|---|
userId | UUID | Recipient |
category | NotificationCategory | Which category this belongs to |
priority | NotificationPriority | Delivery urgency |
title | string | Short headline |
message | string | Longer description |
actionUrl | string (optional) | Deep-link URL to related content |
actionLabel | string (optional) | Button label for the CTA |
relatedType | enum (optional) | Entity type: challenge, track_record, community, user, gift, assignment |
relatedId | UUID (optional) | ID of related entity |
isRead | boolean | Whether user has seen this |
readAt | timestamp (optional) | When it was read |
sentViaEmail | boolean | Whether also sent by email |
sentViaPush | boolean | Whether also sent by push |
File: src/types/notification.types.ts -- Notification interface extending BaseEntity
In-App Toast Notifications PARTIAL
The spec calls for slide-in toast notifications for high-priority events that occur while the user is active. The current codebase has an XPGainToast component for gamification XP gains (animated gradient card with pop-in and shimmer effects), but a general-purpose toast system for arbitrary notification types is not yet built.
Spec requirements for the general toast system:
- Slide-in from top-right on desktop, top on mobile
- Auto-dismiss after 5 seconds
- Click to navigate to content
- Dismiss button
- Maximum 1 toast at a time (queue if multiple)
File: src/components/gamification/XPGainToast.tsx -- XP-specific toast (built)
Contextual Indicators NOT STARTED
Beyond the notification center, the spec calls for badge counts on relevant page tabs:
- Badge count on "My Challenges" tab when new verification results arrive
- Badge count on "Communities" tab when new posts appear
- Indicator on assignments section when new assignments or feedback come in
- Indicators clear when the user visits the relevant section
These contextual indicators are not yet implemented.
Celebration Overlays NOT STARTED
Celebration overlays are distinct from notifications -- they are delightful full-screen or modal moments, not informational items. The spec defines them for:
- Level up
- Badge earned
- Challenge completed
- Learning Path completed
- Streak milestone
These are specified in Doc 8 (Gamification) and are not yet implemented. The celebrationAnimations toggle in notification preferences (which allows users to turn them off) is defined in the data model but has no UI to control yet.
Push Notifications NOT STARTED
No Service Worker, Web Push API, or push permission logic exists in the codebase. The spec defines a thoughtful permission strategy:
When NOT to request push permission:
- First visit
- During onboarding
- Immediately after registration
When TO request push permission:
- User completes their first challenge (natural engagement moment)
- User enables a streak (to get opted-in reminders)
- User navigates to notification settings (explicit interest)
Permission request copy example:
"Want to know when your Track Record is verified? Enable notifications so you don't miss it."
The copy should be contextual and benefit-specific, never generic "allow notifications?"
Push content rules:
- Title: 50 characters max
- Body: 100 characters max
- No clickbait or urgency manipulation
- Deep link to relevant content
- Under-13 users: push only for school assignments and verification results
Email System NOT STARTED
The spec defines a comprehensive email system with 36 templates across 4 categories. No email infrastructure exists in the codebase yet.
Email Categories
| Category | Unsubscribe? | Description | Template Count |
|---|---|---|---|
| Transactional | Cannot unsubscribe | Legally required or essential: verification, password reset, security alerts, deletion confirmation | 7 |
| Service | Granular unsubscribe | Related to active use: TR results, assignments, gifts, event reminders | 12 |
| Digest | Unsubscribe | Summary communications: weekly user/parent/teacher/vendor summaries, monthly school reports | 5 |
| Marketing/Engagement | Unsubscribe | Re-engagement and feature promotion. Never sent to under-13 users. | 4 |
Email Sending Limits
- No more than 1 non-transactional email per user per day (excluding digests)
- Digests: maximum 1 per week per type
- Re-engagement: maximum 1 per 30 days
- Under-13: only transactional and school-related service emails
Email Design Rules
- Mobile-responsive (single column at < 600px), max width 600px
- System fonts only (no custom font loading)
- Images: always include alt text, never rely on images for key info
- CTA buttons: minimum 44px tap target
- Dark mode compatible
- Plain text version for every HTML email
- Tone: warm, encouraging, never guilt-inducing
Notification Preferences BUILT
Users can configure notifications through a preferences UI accessible from the notification dropdown (gear icon) or from Account Settings > Notifications.
The preferences component provides:
- Per-category toggles -- a grid with In-App, Email, and Push columns for each of 6 visible categories (Challenges, Communities, Social, School, Gamification, System)
- Bulk enable/disable -- "Enable all" / "Disable all" links per channel column
- Quiet hours -- toggle with configurable start and end time
- Email digest frequency -- Real-time, Daily Digest, or Weekly Digest radio buttons
- Save button with loading state and success confirmation
The preferences component is connected to the Zustand store (useNotificationStore), calling fetchPreferences on mount and updatePreferences on save. Quiet hours are included in the preferences payload.
Key files:
src/components/account/NotificationPreferences.tsx-- preferences UI componentsrc/store/useNotificationStore.ts-- store withpreferencesstate andupdatePreferencesactionsrc/types/notification.types.ts--NotificationPreferencesinterface anddefaultNotificationPreferencesfactory defaults
Quiet Hours BUILT (UI only)
Quiet hours suppress non-critical push notifications and emails during a configurable time window. The UI for setting quiet hours is built. Server-side enforcement is not.
| Setting | Default | Notes |
|---|---|---|
| Enabled | Off | User must opt in |
| Start time | 22:00 (10 PM) | Configurable |
| End time | 08:00 (8 AM) | Configurable |
| Timezone | America/New_York | Set at registration, adjustable |
| Applies to | Push, non-critical emails | In-app notifications are never suppressed |
| Exceptions | Critical notifications | Security alerts always bypass quiet hours |
Frequency Caps NOT STARTED
The spec defines per-role push notification limits. When caps are reached, remaining notifications are delivered via in-app only.
| User Type | Max Pushes Per Day | Max Per Week |
|---|---|---|
| General user (18+) | 5 | 20 |
| Minor (13-17) | 3 | 12 |
| Under-13 | 2 | 7 |
| Parent | 5 | 20 |
| Teacher | 5 | 25 |
| SA | 3 | 15 |
| Vendor | 5 | 20 |
No frequency cap enforcement exists in the codebase.
Notification Catalog
The spec defines 60+ notification events grouped by domain. Here is a summary of the major categories with their channel defaults:
Account & Security Events
| Event | Priority | Channels | Under-13 |
|---|---|---|---|
| Welcome / account created | High | In-app, Email | Parent receives copy |
| Password changed | Critical | Parent notified | |
| Password reset requested | Critical | Parent notified | |
| New login from unrecognized device | Critical | Parent notified | |
| Account suspended | Critical | Parent notified |
Challenge & Track Record Events
| Event | Priority | Channels | Under-13 |
|---|---|---|---|
| TR verified / approved | High | In-app, Push, Email | Parent notified |
| TR revision requested | High | In-app, Push, Email | Parent notified |
| All milestones completed (ready to submit) | High | In-app, Push | Standard |
| Dealer's Choice card available | Medium | In-app, Push | Standard |
Assignment Events (School Context)
| Event | Priority | Channels | Under-13 |
|---|---|---|---|
| New assignment | High | In-app, Push, Email | Standard (school) |
| Assignment due tomorrow | High | In-app, Push | Standard |
| Assignment overdue | High | In-app, Email | Parent notified |
| Assignment feedback received | High | In-app, Push, Email | Parent notified |
Gift Events
| Event | Priority | Channels | Under-13 |
|---|---|---|---|
| Gift received | High | In-app, Push, Email | Parent approval required |
| Gift accepted | Medium | In-app | Standard |
| Gift expiring soon (7 days) | Medium | In-app, Push | Parent notified |
Gamification Events
| Event | Priority | Channels | Under-13 |
|---|---|---|---|
| Level up | High | In-app, Push | Standard |
| Badge earned | High | In-app, Push | Standard |
| Rare/Legendary badge earned | High | In-app, Push, Email | Standard |
| Streak milestone (7, 14, 30, etc.) | High | In-app, Push | Standard |
| Streak at risk (1 day remaining) | Medium | In-app, Push | Standard |
Community Events
| Event | Priority | Channels | Under-13 |
|---|---|---|---|
| Invited to community | High | In-app, Push | Parent approval required |
| Community goal achieved | High | In-app, Push | Standard |
| New post in community | Low | Digest only | Standard |
| Removed from community | High | In-app, Email | Parent notified |
See the full catalog of 60+ events in Doc 11, Sections 3.1--3.11.
Roles & Permissions
| Role | Receives Notifications | Configures Preferences | Receives Copies | Special Rules |
|---|---|---|---|---|
| General User | Yes | Full control over categories and channels | N/A | Standard frequency caps |
| Student (Tier 1) | Yes, limited | Cannot configure (school-managed) | Parent gets copies | No marketing, no community, minimal push |
| Student (Tier 2) | Yes, limited | Parent approves preferences | Parent gets copies | No marketing, achievement in-app only |
| Parent | Yes + child copies | Full control | Copies of all child communications | Approval requests for sharing, community |
| Teacher | Yes | Full control | N/A | Extra: verification queue alerts, class summaries |
| School Admin | Yes | Full control | N/A | Extra: roster alerts, health score, maintenance |
| Vendor | Yes | Full control | N/A | Extra: challenge performance, event registrations |
| Platform Admin | Yes | Full control | N/A | Extra: security alerts, system health, content flags |
Default Preferences by Role
| Category | In-App | Push (General) | Email (General) | Push (Minor) | Email (Minor) |
|---|---|---|---|---|---|
| Challenge updates | Always on | On | On | On | Off |
| Verification results | Always on | On | On | On | On |
| Assignments | Always on | On | On | On | On |
| Gifts | Always on | On | On | On | Off |
| Communities | Always on | Off | Off | Off | Off |
| Achievements | Always on | On | Off | On | Off |
| Streak reminders | Always on | On | Off | Off | Off |
| Recommendations | Always on | Off | Off | Off | Off |
| Weekly summary | N/A | N/A | On | N/A | Off |
| Re-engagement | N/A | N/A | On | N/A | Never sent |
In-app notifications cannot be turned off -- they are the minimum delivery channel.
Constraints & Limits
| Constraint | Value | Source |
|---|---|---|
| Notification page size | 20 per page | Store implementation |
| Bell dropdown limit | 10 most recent | NotificationCenter component |
| Bell count cap | 99+ display | NotificationCenter component |
| Polling interval | 60 seconds | NotificationCenter component |
| Toast auto-dismiss | 5 seconds | Spec (not yet implemented) |
| Max simultaneous toasts | 1 (queue others) | Spec (not yet implemented) |
| Push title length | 50 characters max | Spec |
| Push body length | 100 characters max | Spec |
| Non-transactional email limit | 1 per user per day | Spec |
| Digest frequency | Max 1 per week per type | Spec |
| Re-engagement email | Max 1 per 30 days | Spec |
| Quiet hours default | 9 PM -- 7 AM (spec) / 10 PM -- 8 AM (code) | Slight discrepancy |
| Email verification link expiry | 24 hours | Spec |
| Password reset link expiry | 1 hour | Spec |
| Data export download link expiry | 48 hours | Spec |
| Account deletion grace period | 30 days | Spec |
| Unsubscribe processing | Instant (target), 10 business days (CAN-SPAM max) | Spec |
COPPA & Minor Communication Rules
Under-13 (Tier 1 -- School Only)
What IS sent: Assignment notifications (in-app, limited push), TR verification results (in-app, push), security alerts (in-app).
What is NOT sent: Marketing or re-engagement emails, streak pressure notifications, community notifications (no community access), recommendation push notifications.
Parent receives: Copy of all communications sent to child, weekly activity summary, assignment status updates, all approval requests.
Under-13 (Tier 2 -- Parent-Linked)
Same as Tier 1, plus: gift notifications (routed through parent for approval), achievement notifications (in-app only). Parent must approve communication preferences.
Ages 13--17
Standard notifications with restrictions:
- No re-engagement emails
- Reduced push frequency caps (3/day, 12/week instead of 5/day, 20/week)
- Streak reminders: softer language, limited to 1 per day
- No FOMO-inducing language
- Leaderboard notifications: only positive ("your achievement"), never "you're falling behind"
CAN-SPAM Compliance
All non-transactional emails must include:
- Clear identification as commercial message (where applicable)
- Valid physical postal address
- Clear unsubscribe mechanism
- Accurate "From" and "Subject" lines
- No deceptive headers or subject lines
Design Decisions
In-app is always on. In-app notifications cannot be turned off because they are the minimum viable channel. Every notification creates an in-app record regardless of other channel preferences. This guarantees users never miss critical information.
Push permission is delayed. The spec explicitly prohibits requesting push permission on first visit, during onboarding, or immediately after registration. Permission is requested only at natural engagement moments (first challenge completed, streak enabled, or visiting notification settings). This respects user attention and improves opt-in rates.
No marketing to under-13. Marketing and re-engagement emails are never sent to users under 13. This is a hard rule, not configurable. Even for users 13--17, re-engagement emails are blocked.
Celebration overlays are not notifications. Level-ups, badges earned, and streak milestones produce celebration overlays (full-screen or modal animations) that are distinct from the notification system. They are joyful moments, not information items. Users can disable celebration animations through preferences.
Quiet hours have exceptions. Critical notifications (security alerts, account suspension) bypass quiet hours. This ensures users are always informed of security-sensitive events regardless of their schedule.
Frequency caps are role-aware. Minors receive fewer push notifications per day and per week than adults. Teachers have slightly higher weekly caps to accommodate classroom management needs.
Digest over spam. The default email digest frequency is daily (in code) or weekly (in spec). Users can choose real-time, daily, weekly, or off. Empty digests are never sent -- if there is nothing meaningful to report, no email goes out.
Preferences UI uses local state. The
NotificationPreferencescomponent currently manages its own local state with a mock save, rather than connecting to the Zustand store'supdatePreferencesaction. This was a pragmatic decision during prototyping and should be wired up when the backend is ready.Category discrepancy between UI and types. The preferences UI shows 6 categories (Challenges, Communities, Social, School, Gamification, System), while the type system defines 13 categories. The additional categories (gifts, verification, teacher_feedback, parent_alerts, school_admin, vendor, digests) are typed but not yet surfaced in the preferences grid. Role-specific categories should only appear for users with the matching role.
Quiet hours default discrepancy. The spec defines quiet hours as 9 PM -- 7 AM, but the code defaults to 10 PM -- 8 AM. This should be reconciled before launch.
Technical Implementation
Store
The useNotificationStore is a Zustand store managing:
- Notification list with pagination (
fetchNotifications,fetchMoreNotifications) - Unread counts with periodic polling (
fetchCounts) - Read state management (
markAsRead,markAllAsRead) - Deletion (
deleteNotification) - Preferences (
fetchPreferences,updatePreferences) - Loading and error states (
isLoading,isLoadingMore,isSaving,error)
Exported selectors: selectNotifications, selectUnreadCount, selectNotificationCounts, selectPreferences.
File: src/store/useNotificationStore.ts
API Layer
Mock API (notification.api.ts): Full CRUD against the in-memory mockDb. Supports paginated listing, filtering by unread, marking as read (individual and bulk), deletion, preference get/set, and notification creation.
Real API (notification.real.api.ts): HTTP-based stubs calling the Express backend. getNotificationCounts and deleteNotification return stubs (no backend endpoint). Other operations map to REST endpoints at /notifications/*.
File: src/api/notification.api.ts, src/api/notification.real.api.ts
Components
| Component | Location | Description |
|---|---|---|
NotificationCenter | src/components/notifications/NotificationCenter.tsx | Bell icon dropdown in header with unread badge, 10-item preview, mark all read, settings link |
Notifications (page) | src/pages/Notifications.tsx | Full-page notification list with All/Unread tabs, pagination, delete per item |
NotificationPreferences | src/components/account/NotificationPreferences.tsx | Per-category toggle grid, quiet hours, digest frequency, save button |
XPGainToast | src/components/gamification/XPGainToast.tsx | Animated XP gain toast (gamification-specific, not general purpose) |
Types
| Type | Description |
|---|---|
Notification | In-app notification entity with category, priority, read state, deep-link fields |
NotificationCategory | 13-member string union for topic categorization |
NotificationChannel | 'in_app' | 'email' | 'push' |
NotificationPriority | 'low' | 'medium' | 'high' | 'critical' |
NotificationPreferences | Per-category, per-channel toggle grid + quiet hours + digest frequency |
NotificationCounts | Aggregate total/unread/byCategory counts |
defaultNotificationPreferences | Factory defaults for new users |
File: src/types/notification.types.ts
Backend Data Model (Planned)
The spec defines 5 database tables for the full notification system:
| Table | Purpose |
|---|---|
notifications | In-app notification records. Indexed on (user_id, read, created_at DESC) |
notification_preferences | Per-user preference configuration. One row per user. |
email_sends | Email delivery tracking with ESP message IDs, delivery/open/click timestamps |
email_suppressions | Hard bounces, complaints, unsubscribes. Prevents future sends. |
push_tokens | Per-device push tokens (web, iOS, Android) with active/inactive state |
push_sends | Push delivery tracking linked to notifications and tokens |
What Needs Building
| Feature | Priority | Dependencies |
|---|---|---|
| Wire preferences UI to Zustand store | High | None |
| Event-driven notification creation | High | Backend event bus |
| COPPA-aware recipient routing | High | User age/role resolution |
| Push notification infrastructure (Service Worker, VAPID) | Medium | Backend push service |
| Email template system (36 templates) | Medium | ESP integration (SendGrid/Postmark/SES) |
| Frequency cap enforcement | Medium | Backend notification service |
| Quiet hours server-side enforcement | Medium | Backend notification service |
| General-purpose toast system | Medium | UI framework decision |
| Celebration overlays (level up, badge, streak) | Medium | Gamification events |
| Contextual page-tab indicators | Low | Per-page unread counts |
| Delivery tracking and analytics | Low | ESP webhooks, push receipts |
| A/B testing on email templates | Low | Email system + analytics |
| SMS channel | Low | SMS provider integration |
Related Features
- Challenges -- Challenge start, milestone completion, and TR submission events trigger notifications
- Track Records -- TR verification, revision requests, and rejections are high-priority notification events
- Gifting -- Gift sent, accepted, declined, completed, and expiring events all generate notifications with COPPA routing for under-13 recipients
- Accounts -- Security alerts, password changes, and new device logins are critical notifications; account settings host the notification preferences UI
- Explore -- Challenge recommendations can generate medium-priority in-app notifications
- Gamification (Doc 8) -- Level ups, badges earned, streak milestones, and leaderboard changes trigger notifications and celebration overlays
- Communities (Doc 5) -- Community invitations, posts, goal progress, and moderation warnings generate notifications
- School Administration (Doc 6) -- Assignments, roster changes, and school health scores generate role-specific notifications for students, teachers, parents, and school admins