critical priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

NotificationRealtimeInsert event handler deserialises the raw Supabase RealtimeInsertPayload into a NotificationModel using the model's fromJson factory
Deserialised notification is passed through RoleAwareNotificationFilterService before any state mutation
If the filter returns false (not visible for current role), the notification is silently ignored — no state change, no error
If the filter returns true, the notification is prepended to the existing list in the current NotificationLoaded state
Unread count in state is incremented by 1 (atomically as part of the same state emit) when a new notification is prepended
Before prepending, the notification ID is checked against the existing state list — if already present, the event is a no-op
If the current state is not NotificationLoaded (e.g., NotificationError), the INSERT event is queued or dropped gracefully without throwing
Deserialisation failure (malformed payload) emits a non-fatal NotificationRealtimeParseError and does not crash the bloc
State emission after INSERT is a complete immutable copy of the previous state with the new notification prepended — no list mutation

Technical Requirements

frameworks
Flutter
BLoC
apis
Supabase Realtime payload format
data models
NotificationModel
NotificationLoaded
NotificationRealtimeInsert
UserRole
performance requirements
INSERT event handling including filter check and state emit must complete within 16ms (one frame) to avoid jank
Duplicate check must use a Set<String> of existing IDs maintained in state, not a linear list scan, to keep O(1) lookup
security requirements
Raw Realtime payload must be validated before deserialisation — reject payloads missing required fields (id, org_id, recipient_id)
org_id in the incoming payload must be verified against the current user's org_id before inserting — defence against Realtime subscription misconfiguration

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Add a `Set notificationIds` field to `NotificationLoaded` state alongside the list — this enables O(1) duplicate detection. In the `on` handler: (1) guard `if (state is! NotificationLoaded) return;`, (2) try/catch deserialise with `NotificationModel.fromJson(event.payload)`, on failure add `NotificationRealtimeParseError` event, (3) validate `model.orgId == currentUser.orgId`, (4) call filter service, (5) check `(state as NotificationLoaded).notificationIds.contains(model.id)`, (6) emit new state with `[model, ...currentState.notifications]` and incremented unreadCount and updated `notificationIds` set. Use `copyWith` on `NotificationLoaded` state for clean immutable updates.

Apply `transformer: sequential()` on this event handler to prevent concurrent INSERT processing from producing race conditions on the state list.

Testing Requirements

Unit tests (flutter_test + bloc_test) for NotificationRealtimeInsert handler: (1) valid payload for current role is prepended and unread count increments, (2) valid payload filtered out by role filter produces no state change, (3) duplicate payload (same ID already in state) produces no state change, (4) malformed JSON payload emits NotificationRealtimeParseError without crashing, (5) INSERT event while in NotificationError state is handled without throwing, (6) org_id mismatch in payload is silently rejected. Use bloc_test `seed` to establish a pre-loaded state before each test. Verify state immutability: assert that the original list reference in state is not the same object as the new list after insert.

Component
Notification BLoC
service high
Epic Risks (3)
medium impact medium prob technical

A Realtime INSERT event arriving during an in-flight mark-all-read operation can cause the new notification to be incorrectly marked read in the optimistic state update, silently hiding it from the user.

Mitigation & Contingency

Mitigation: Process Realtime events sequentially in the BLoC event queue using bloc_concurrency's sequential transformer. The mark-all-read event should only affect notifications whose IDs were fetched before the operation started.

Contingency: Add a reconciliation step after mark-all-read that re-fetches the unread count from the repository and corrects the BLoC state if it diverges from the server value.

medium impact medium prob technical

A coordinator assigned to many peer mentors may trigger a query returning hundreds or thousands of notifications. Without pagination and query optimisation, the initial load will be slow and memory-heavy.

Mitigation & Contingency

Mitigation: Enforce server-side pagination (50 items per page) in the Role-Aware Filter's query predicates. Add a composite index on (org_id, user_id, created_at DESC) and profile query plans before shipping.

Contingency: If query performance is insufficient for large coordinator scopes, introduce a server-side RPC function that pre-aggregates visible notification IDs and returns only the first page, deferring full scope resolution to lazy-load.

medium impact low prob scope

If the read-state optimistic update rolls back frequently due to intermittent connectivity, users will observe notifications toggling between read and unread, creating confusion and distrust of the feature.

Mitigation & Contingency

Mitigation: Queue failed mutations in a local retry store and re-attempt on next connectivity event using a connectivity-aware retry service. Show a non-intrusive banner if offline rather than applying optimistic updates.

Contingency: Disable optimistic updates for mark-as-read in low-connectivity scenarios detected by the connectivity provider, instead showing a loading indicator until server confirmation.