critical priority medium complexity infrastructure pending infrastructure specialist Tier 3

Acceptance Criteria

RealtimeApprovalSubscription exposes a Stream<ClaimStatusEvent> that emits on every UPDATE to expense_claim_status rows matching the current user's claims
connect() establishes the Supabase Realtime channel and subscribes — returns a Future that completes when channel state is SUBSCRIBED
reconnect() implements exponential backoff starting at 1s, doubling each attempt up to a maximum of 60s, with jitter to prevent thundering herd
dispose() unsubscribes the channel and closes the StreamController — safe to call multiple times without error
ClaimStatusEvent contains: claimId (String), newStatus (ClaimStatus), previousStatus (ClaimStatus?), updatedAt (DateTime)
RLS policy enforcement verified — subscription only receives events for claims belonging to the authenticated user's organization (confirmed by test with two org users)
Stream emits ClaimStatusEvent.reconnecting when connection drops and ClaimStatusEvent.reconnected when restored
No duplicate channel registrations — calling connect() while already connected is a no-op with log warning
Channel is scoped to organization_id from JWT claims — never receives cross-org events
Memory leak test: dispose() followed by GC shows no retained RealtimeChannel references

Technical Requirements

frameworks
Dart async (StreamController, Stream)
BLoC (ApprovalBLoC consumes the stream)
apis
Supabase Realtime — channel subscription on expense_claim_status table
Supabase Auth — JWT claims for organization scoping
data models
claim_event (status change events)
assignment (organization_id scoping)
performance requirements
Realtime event delivery latency < 500ms from database commit to Stream emission
Reconnection attempt must not block the UI thread — all retry logic in isolate-safe async code
Maximum 1 active channel per claim context — no unbounded channel creation
security requirements
RLS policies enforced on Realtime subscriptions — JWT validated on every channel subscription
No sensitive PII transmitted via Realtime payloads — only row IDs and status fields (per integration spec)
Channel filter includes eq('organization_id', orgId) to enforce org isolation at subscription level

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use Supabase Flutter SDK's RealtimeChannel with .onPostgresChanges() filtering on schema='public', table='expense_claim_status', event=PostgresChangeEvent.update. Expose the stream as a broadcast stream so multiple BLoC instances can subscribe without each owning a channel. Implement the exponential backoff using a recursive async function with await Future.delayed() — avoid Timer-based approaches that complicate testing. Use StreamController.broadcast() so late subscribers don't miss events from before their subscription.

Place the class in lib/infrastructure/realtime/ to clearly indicate it is an infrastructure adapter. Document that the stream never completes normally — consumers must cancel their subscription explicitly.

Testing Requirements

Unit tests using flutter_test with a mock RealtimeChannel. Test cases: (1) connect() transitions to SUBSCRIBED state and emits first event, (2) simulated disconnect triggers reconnect() with correct backoff intervals verified by fake async timer, (3) dispose() closes stream and subsequent events are silently dropped, (4) duplicate connect() calls do not create additional channels, (5) ClaimStatusEvent fields correctly mapped from Realtime payload JSON. Integration test (optional, requires Supabase test instance): verify cross-org isolation by subscribing with org-A credentials and confirming org-B updates do not appear.

Component
Supabase Realtime Approval Subscription
infrastructure medium
Epic Risks (3)
medium impact medium prob technical

Optimistic locking in ExpenseClaimStatusRepository may produce excessive concurrency exceptions in high-volume coordinator sessions where multiple coordinators process the same queue simultaneously, causing confusing UI errors and coordinator frustration.

Mitigation & Contingency

Mitigation: Design the locking strategy with a short retry window (1-2 automatic retries with 200ms back-off) before surfacing the error to the UI. Document the concurrency model clearly so the UI layer can display a contextual 'claim was already actioned' message rather than a generic error.

Contingency: If contention remains high under load testing, switch to a last-writer-wins update with a conflict notification rather than a hard block, and log all concurrent edits for audit purposes.

medium impact medium prob integration

FCM device tokens stored for peer mentors may be stale (app reinstalled, token rotated) causing push notifications for claim status changes to silently fail, leaving submitters unaware their claim was approved or rejected.

Mitigation & Contingency

Mitigation: Implement token refresh on every app launch and store updated tokens in Supabase. ApprovalNotificationService should fall back to in-app Realtime delivery when FCM returns an invalid-token error and should queue a token refresh request.

Contingency: If FCM delivery rates fall below acceptable thresholds in production monitoring, add a polling fallback in the peer mentor claim list screen that checks status on foreground resume.

high impact low prob dependency

Supabase Realtime has per-project channel and connection limits. If many coordinators and peer mentors are simultaneously subscribed across multiple screens, the project may hit quota limits causing subscription failures.

Mitigation & Contingency

Mitigation: Design RealtimeApprovalSubscription to use a single shared channel per user session rather than per-screen subscriptions. Implement subscription reference counting so channels are only opened once and reused across screens.

Contingency: Upgrade the Supabase plan tier if limits are reached, and implement graceful degradation to polling with a 30-second interval when Realtime is unavailable.