In-App Notification System #18

Open
opened 2026-05-07 20:11:03 +02:00 by ben · 0 comments
Owner

Overview

Implement an in-app notification system that keeps users informed of relevant changes — new dishes and partner favorites — in a bundled, non-annoying way. Notifications appear either as a summary when opening the app, or live when the app is already open.

Goal

Users should never be overwhelmed by individual notifications. Instead, changes are grouped into single summary notifications.

Example: If the partner swipes right on 10 dishes in the Tinder page, the user sees one notification: "vanda hat 10 Gerichte gelikt" — not 10 separate ones.

Delivery Modes

Mode Trigger Behavior
Live App is open Realtime banner/notification triggered by SSE events (debounced/bundled)
On-Open App was closed Notification summary shown on next app open for changes since last session

Architecture

Backend (PocketBase)

New collection: notification_events

Field Type Description
created DateTime When event occurred
type Select new_dish, partner_fav, partner_fav_removed
dish_id Relation → dishes Affected dish (nullable)
by_user_id Relation → users Who triggered the event
delivered Boolean Whether user has been notified

PocketBase JS hooks (pb_hooks/):

  • dishes.afterCreate → inserts a new_dish event for the linked partner
  • users.afterUpdate → detects changes to the favorites field on a partner record, inserts partner_fav events (comparing old vs new favorites to determine which dishes were added/removed)

Client — New Files

  1. lib/data/models/notification_event.dart — Isar model mirroring remote events locally
  2. lib/data/services/notification_service.dart — Pulls remote events, upserts locally, marks delivered, clears old events
  3. lib/logic/providers.dart — Riverpod providers:
    • notificationEventsProvider — Isar stream of unread events
    • notificationStateProviderStateNotifier that batches events into NotificationGroup
    • notificationCountProvider — unread count for badge display
  4. lib/ui/screens/notification_page.dart — Notification list/history page
  5. lib/ui/widgets/notification_banner.dart — Live toast/snackbar for immediate notifications
  6. pb_hooks/notifications.pb.js — PocketBase hook logic
  7. pb_hooks/notification_utils.js — Shared hook utilities

Client — Modified Files

  • lib/logic/providers.dart — Add notification providers + integrate into RealtimeSyncNotifier and performFullSync()
  • lib/ui/screens/home_page.dart — Add notification bell icon with badge counter to app bar
  • lib/data/services/sync_service.dart — Add notification pull into delta sync flow
  • lib/ui/widgets/dish_card.dart — May need UI adjustments for notification badge

Bundling Logic (NotificationStateNotifier)

On SSE event or periodic pull (every 30s):
  1. Pull new remote events since lastNotificationPullTime
  2. Batch into groups:
     - All "new_dish" events → 1 group: "{count} neue Gerichte"
     - All "partner_fav" events → 1 group: "{name} hat {count} Gerichte gelikt"
  3. Update notification state → UI rebuilds

Integration Points

Existing Component Integration
RealtimeSyncNotifier._onEvent SSE events for dishes trigger notification pull
RealtimeSyncNotifier._periodicTimer (30s) Also triggers lightweight notification pull
performFullSync() Pulls notifications on app start / pull-to-refresh
TinderPage No changes needed — partner favorites change fires users.afterUpdate hook
DishRepository No changes needed — new dish creation fires dishes.afterCreate hook

Implementation Order

  1. Backend: Create notification_events collection + JS hooks on VPS
  2. Client models: NotificationEvent Isar model with .g.dart generation
  3. Client service: NotificationService — pull, mark delivered, cleanup
  4. Client state: NotificationStateNotifier + Riverpod providers
  5. Integration: Hook into RealtimeSyncNotifier and performFullSync()
  6. UI: Badge on home page, notification list page, live banner widget
  7. Testing: Manual testing with Tinder swipes and dish creation

Edge Cases

  • App offline: Events accumulate in backend, delivered on next sync
  • Multiple devices: delivered flag is server-side → cross-device consistent
  • Partner unlinked: No partner fav events generated (hook checks partner_id)
  • User logs out: Clear local notification state
  • Dish deleted: Remove related events or mark stale
  • 7-day retention: Auto-clean old delivered events during pull

Design Decisions

Decision Choice Rationale
Server-side event tracking Server-side Reliable, works even if app is offline — events accumulate in DB
SSE for live + periodic polling Both SSE for live, 30s periodic pull catches missed during brief offline
Bundling window Per session Reset when user opens notification list; otherwise accumulate
Keep or delete delivered events? Keep for 7 days Allows viewing history, auto-cleaned during pull
Dish creation trigger dishes.afterCreate hook Simple, reliable
Partner favorite trigger users.afterUpdate hook Detects favorites field changes by comparing old vs new
## Overview Implement an in-app notification system that keeps users informed of relevant changes — new dishes and partner favorites — in a **bundled, non-annoying** way. Notifications appear either as a summary when opening the app, or live when the app is already open. ## Goal Users should never be overwhelmed by individual notifications. Instead, changes are **grouped into single summary notifications**. **Example**: If the partner swipes right on 10 dishes in the Tinder page, the user sees **one** notification: *"vanda hat 10 Gerichte gelikt"* — not 10 separate ones. ## Delivery Modes | Mode | Trigger | Behavior | |---|---|---| | **Live** | App is open | Realtime banner/notification triggered by SSE events (debounced/bundled) | | **On-Open** | App was closed | Notification summary shown on next app open for changes since last session | ## Architecture ### Backend (PocketBase) **New collection: `notification_events`** | Field | Type | Description | |---|---|---| | `created` | DateTime | When event occurred | | `type` | Select | `new_dish`, `partner_fav`, `partner_fav_removed` | | `dish_id` | Relation → dishes | Affected dish (nullable) | | `by_user_id` | Relation → users | Who triggered the event | | `delivered` | Boolean | Whether user has been notified | **PocketBase JS hooks** (`pb_hooks/`): - `dishes.afterCreate` → inserts a `new_dish` event for the linked partner - `users.afterUpdate` → detects changes to the `favorites` field on a partner record, inserts `partner_fav` events (comparing old vs new favorites to determine which dishes were added/removed) ### Client — New Files 1. **`lib/data/models/notification_event.dart`** — Isar model mirroring remote events locally 2. **`lib/data/services/notification_service.dart`** — Pulls remote events, upserts locally, marks delivered, clears old events 3. **`lib/logic/providers.dart`** — Riverpod providers: - `notificationEventsProvider` — Isar stream of unread events - `notificationStateProvider` — `StateNotifier` that batches events into `NotificationGroup` - `notificationCountProvider` — unread count for badge display 4. **`lib/ui/screens/notification_page.dart`** — Notification list/history page 5. **`lib/ui/widgets/notification_banner.dart`** — Live toast/snackbar for immediate notifications 6. **`pb_hooks/notifications.pb.js`** — PocketBase hook logic 7. **`pb_hooks/notification_utils.js`** — Shared hook utilities ### Client — Modified Files - **`lib/logic/providers.dart`** — Add notification providers + integrate into `RealtimeSyncNotifier` and `performFullSync()` - **`lib/ui/screens/home_page.dart`** — Add notification bell icon with badge counter to app bar - **`lib/data/services/sync_service.dart`** — Add notification pull into delta sync flow - **`lib/ui/widgets/dish_card.dart`** — May need UI adjustments for notification badge ### Bundling Logic (`NotificationStateNotifier`) ``` On SSE event or periodic pull (every 30s): 1. Pull new remote events since lastNotificationPullTime 2. Batch into groups: - All "new_dish" events → 1 group: "{count} neue Gerichte" - All "partner_fav" events → 1 group: "{name} hat {count} Gerichte gelikt" 3. Update notification state → UI rebuilds ``` ### Integration Points | Existing Component | Integration | |---|---| | `RealtimeSyncNotifier._onEvent` | SSE events for `dishes` trigger notification pull | | `RealtimeSyncNotifier._periodicTimer` (30s) | Also triggers lightweight notification pull | | `performFullSync()` | Pulls notifications on app start / pull-to-refresh | | `TinderPage` | No changes needed — partner favorites change fires `users.afterUpdate` hook | | `DishRepository` | No changes needed — new dish creation fires `dishes.afterCreate` hook | ## Implementation Order 1. **Backend**: Create `notification_events` collection + JS hooks on VPS 2. **Client models**: `NotificationEvent` Isar model with `.g.dart` generation 3. **Client service**: `NotificationService` — pull, mark delivered, cleanup 4. **Client state**: `NotificationStateNotifier` + Riverpod providers 5. **Integration**: Hook into `RealtimeSyncNotifier` and `performFullSync()` 6. **UI**: Badge on home page, notification list page, live banner widget 7. **Testing**: Manual testing with Tinder swipes and dish creation ## Edge Cases - **App offline**: Events accumulate in backend, delivered on next sync - **Multiple devices**: `delivered` flag is server-side → cross-device consistent - **Partner unlinked**: No partner fav events generated (hook checks `partner_id`) - **User logs out**: Clear local notification state - **Dish deleted**: Remove related events or mark stale - **7-day retention**: Auto-clean old delivered events during pull ## Design Decisions | Decision | Choice | Rationale | |---|---|---| | Server-side event tracking | **Server-side** | Reliable, works even if app is offline — events accumulate in DB | | SSE for live + periodic polling | **Both** | SSE for live, 30s periodic pull catches missed during brief offline | | Bundling window | **Per session** | Reset when user opens notification list; otherwise accumulate | | Keep or delete delivered events? | **Keep for 7 days** | Allows viewing history, auto-cleaned during pull | | Dish creation trigger | `dishes.afterCreate` hook | Simple, reliable | | Partner favorite trigger | `users.afterUpdate` hook | Detects favorites field changes by comparing old vs new |
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ben/LovePlate#18
No description provided.