critical priority medium complexity backend pending backend specialist Tier 0

Acceptance Criteria

Calling markAsRead(notificationId) immediately emits an optimistic ReadStateUpdate event with is_read=true before awaiting the repository
Calling markAllRead() immediately emits an optimistic ReadStateUpdate covering all currently unread notifications in the caller-provided list
Pre-mutation snapshot of the notification list is captured synchronously before the optimistic emit
When repository returns a read_at timestamp, that UTC value is applied to the notification model; when absent, DateTime.now().toUtc() is used
The service exposes a Stream<ReadStateUpdate> that the BLoC can subscribe to
Service state correctly tracks which notification IDs are pending confirmation (in-flight set)
Concurrent markAsRead calls on different notification IDs are handled independently without race conditions
Service does not persist state itself — it delegates all persistence to the injected repository
Service is injectable via constructor (no singleton pattern) to support testing

Technical Requirements

frameworks
Flutter
BLoC
supabase_flutter
apis
Supabase REST API (PATCH /notifications)
Supabase RLS
data models
NotificationModel
ReadStateUpdate
NotificationRepository
performance requirements
Optimistic emit must occur synchronously on the same microtask as the service call — no await before first emit
markAllRead on a list of 100 notifications must not block the UI thread — mutation call must be fully async
security requirements
read_at timestamp must be sourced from server response where available — client time is fallback only to prevent timestamp manipulation
Mark-read operations must target only notifications owned by the authenticated user (enforced by RLS + repository filter)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Implement `NotificationReadStateService` with a `StreamController` (broadcast). The `markAsRead` method should: (1) add notification ID to `_inFlight` set, (2) synchronously emit optimistic `ReadStateUpdate(id, isRead: true, readAt: null, isOptimistic: true)`, (3) await repository.markRead(id), (4) emit confirmed update with server read_at, (5) remove from `_inFlight`. Use `unawaited` from `dart:async` carefully — instead, launch the async work via `Future.microtask` or simply `async` the method. The snapshot for rollback should be stored in a `Map _snapshot` before mutation.

Keep `ReadStateUpdate` as a simple immutable value class. Expose `Stream get updates` to BLoC.

Testing Requirements

Unit tests (flutter_test) using a mock NotificationRepository. Test: (1) markAsRead emits optimistic update before repository future completes (use Completer to control timing), (2) read_at uses server timestamp when present, (3) read_at falls back to local UTC when server timestamp is null, (4) markAllRead emits optimistic update for all unread items, (5) snapshot is captured before mutation. Use fake_async or manual Completer patterns to verify emit ordering. Verify the in-flight set correctly tracks pending IDs.

Integration test with supabase_flutter test utilities verifying the PATCH call is issued with correct payload.

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.