critical priority high complexity backend pending backend specialist Tier 3

Acceptance Criteria

NotificationBloc accepts NotificationRepository, RoleAwareNotificationFilterService, NotificationReadStateService, and SupabaseRealtimeSubscriptionService via constructor injection
On NotificationInitialised event, the bloc fetches the first page of notifications from the repository and emits NotificationLoaded with the paginated result
On NotificationInitialised, the bloc subscribes to the Realtime INSERT stream using emit.forEach binding stream lifecycle to bloc lifecycle
When the bloc is closed, the Realtime subscription is automatically cancelled (no orphaned subscriptions)
Emitted NotificationLoaded state includes the notification list, current page, hasMore flag, and unread count
If the initial page load fails, the bloc emits NotificationError with a human-readable message
If the Realtime subscription fails to connect, the bloc emits a non-fatal NotificationRealtimeError and continues with the loaded list
BLoC state transitions follow the sequence: NotificationInitial → NotificationLoading → NotificationLoaded
All injected dependencies are accessed through abstract interfaces (not concrete Supabase classes) to enable testing

Technical Requirements

frameworks
Flutter
BLoC
supabase_flutter
apis
Supabase Realtime WebSocket
Supabase PostgREST (initial page load)
data models
NotificationModel
NotificationPage
UserRole
NotificationQueryContext
performance requirements
Initial page load must complete within 3 seconds on a 4G connection; emit NotificationLoading immediately to show skeleton UI
Realtime subscription setup must not block the initial page load — run concurrently using Future.wait or separate async operations
BLoC must not retain more than 500 notifications in memory; implement list trimming if exceeded
security requirements
Realtime channel must be scoped to the authenticated user's org and role — never subscribe to a global notifications channel
Supabase JWT must be refreshed before subscribing to Realtime to avoid mid-session auth failures
BLoC must handle auth expiry events from Supabase and unsubscribe + re-subscribe with fresh credentials

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use `emit.forEach(stream, onData: (event) => add(NotificationRealtimeInsert(event)))` inside the `NotificationInitialised` handler to bind stream lifetime to bloc lifetime — this is the idiomatic BLoC pattern and automatically cancels on bloc close. Separate the initial load and subscription setup: `await Future.wait([_loadInitialPage(emit), _subscribeRealtime(emit)])` so neither blocks the other. Define abstract interfaces `INotificationRepository`, `IRealtimeSubscriptionService`, `INotificationReadStateService`, and `IRoleAwareNotificationFilterService` — BLoC depends only on these. Use `on` with `transformer: droppable()` to prevent duplicate initialisation if the event is fired more than once.

Emit unread count by filtering `notifications.where((n) => !n.isRead).length` on the initial load result.

Testing Requirements

Unit tests (flutter_test + bloc_test) covering: (1) NotificationInitialised triggers repository fetch and emits Loading then Loaded, (2) repository failure emits NotificationError, (3) Realtime subscription is set up after successful init (verify mock subscription service called), (4) bloc.close() triggers subscription cancellation (verify mock dispose called), (5) NotificationLoaded state contains correct unread count from initial fetch. Use bloc_test's `expect` DSL for state sequence assertions. Mock all dependencies with mockito or manual test doubles. Avoid using real Supabase clients in unit tests.

Add an integration smoke test verifying the bloc can be instantiated with real Supabase test credentials and emits at least one NotificationLoaded state.

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.