high priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

When repository.markRead throws any exception, the service emits a ReadStateRollback event restoring the pre-mutation is_read=false state for that notification
When repository.markAllRead fails, all optimistically updated notifications are rolled back to their pre-mutation state in a single emit
After a rollback, calling markAsRead on the same notification ID again produces a fresh optimistic update and a new repository call without stale state
Rollback is idempotent: calling it multiple times on an already-rolled-back item has no observable effect on state
Rollback emits a ReadStateFailure event containing a human-readable English error message (network error description, timeout message, or permission denied text)
Rollback correctly handles the case where the snapshot is missing (e.g., snapshot was cleared prematurely) — emits failure without crashing
In-flight set is cleared for affected notification IDs after rollback
Rollback does not affect notification IDs that were not part of the failed mutation

Technical Requirements

frameworks
Flutter
BLoC
apis
Supabase REST API error responses
data models
NotificationModel
ReadStateUpdate
ReadStateRollback
ReadStateFailure
performance requirements
Rollback emit must occur synchronously in the catch block — no additional async hops
Bulk rollback of up to 200 notifications must complete state restoration in under 2ms
security requirements
Error messages exposed to the UI must not include raw Supabase error details or SQL messages that could leak schema information
Distinguish RLS rejection (403-equivalent) from network errors and present appropriate user-facing messages

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

In the catch block of both `markAsRead` and `markAllRead`, retrieve the stored snapshot from `_snapshot[id]` (or the bulk snapshot map) and emit `ReadStateRollback(ids: [...], restoredStates: {...})` followed by `ReadStateFailure(message: _humanReadableError(e))`. Implement `_humanReadableError` as a switch on exception type: `SocketException` → 'No internet connection', `TimeoutException` → 'Request timed out, please retry', `PostgrestException` with code 42501 → 'You do not have permission to update this notification', default → 'Something went wrong, please try again'. Always remove IDs from `_inFlight` in a `finally` block to prevent permanent stuck state. Clear snapshot entries after use (both success and failure) to avoid memory growth.

For idempotency: check if notification is already in un-read state before applying rollback — if already rolled back, skip silently.

Testing Requirements

Unit tests (flutter_test) using mock repository that throws configurable exceptions. Test scenarios: (1) network error on single markAsRead triggers rollback of that item only, (2) timeout on markAllRead triggers bulk rollback of all optimistically updated items, (3) RLS rejection produces 'permission denied' human-readable message, (4) retry after rollback produces fresh optimistic update (verify snapshot is cleared and re-captured), (5) rollback with missing snapshot emits failure without throwing, (6) rollback is idempotent (call rollback handler twice, assert state unchanged on second call). Use fake_async to simulate timeouts. Verify in-flight set is empty after both success and failure paths.

Component
Notification Read State Service
service medium
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.