critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

insertActivity emits exactly two items on the returned Stream for the success path: first with 'local_<uuid>' id and SyncStatus.pending, second with the server-assigned id and SyncStatus.synced
insertActivity emits exactly two items on the failure path: first with 'local_<uuid>' id and SyncStatus.pending, second with the original local id retained and SyncStatus.failed
The Stream completes (closes) after emitting the second event in both success and failure paths
getActivities(peerId) returns a Future<List<ActivityRecord>> containing only records belonging to the given peerId
deleteActivity(id) calls through to SupabaseActivityClient and propagates exceptions as domain exceptions
The repository does NOT hold state between calls — it is stateless and each insertActivity call produces an independent stream
ActivityRepository depends on SupabaseActivityClient via constructor injection (not hard-coded)
No Supabase SDK types leak through the repository's public interface — all return types use domain models

Technical Requirements

frameworks
Flutter
Riverpod
Dart async/Stream
apis
Supabase REST (via SupabaseActivityClient)
data models
ActivityRecord
SyncStatus
performance requirements
First stream emission (optimistic local record) must occur synchronously before any async I/O, ensuring UI responsiveness within a single frame
Supabase insert should not block the UI thread — use async/await inside StreamController
security requirements
Repository must not log or expose the full ActivityRecord payload in non-verbose mode (may contain peer notes)
Domain exceptions wrapping Supabase errors must not include raw Supabase error messages in their user-facing message property

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use a StreamController with async processing. The pattern is: (1) create controller, (2) synchronously add the optimistic record, (3) kick off the async Supabase call without awaiting it before returning the stream, (4) in the async continuation add the server result or failure record and close the controller. The local ID should be generated as 'local_${const Uuid().v4()}' — import the `uuid` package if not already available, or use Dart's built-in random. Do NOT use a broadcast stream here; the repository is consumed by a single BLoC listener per call.

Keep SyncStatus as an enum defined in the domain layer, not in this file. For getActivities and deleteActivity, simple Future-returning methods are correct — the optimistic pattern applies only to inserts because reads and deletes are idempotent from a UI perspective. Error wrapping: catch PostgrestException and rethrow as a domain-level ActivityRepositoryException.

Testing Requirements

Unit tests using flutter_test with mocked SupabaseActivityClient (mocktail preferred). Required test cases: (1) success path stream emits pending then synced with correct IDs, (2) failure path stream emits pending then failed and retains local ID, (3) stream closes after second emission in both paths, (4) getActivities delegates to client with correct peerId, (5) deleteActivity delegates to client and re-throws domain exception on error. Use StreamMatcher (emitsInOrder, emitsDone) for stream assertions. Aim for 100% branch coverage on ActivityRepository.

No integration tests in this task — those belong to a separate integration test suite.

Component
Activity Repository
data medium
Epic Risks (3)
high impact medium prob technical

The optimistic insert pattern requires reconciling temporary local IDs with server-assigned IDs after the async Supabase write completes. If reconciliation logic is incorrect, the UI may display stale records, duplicate entries may appear, or subsequent operations (edit, delete) may target the wrong record ID, corrupting data integrity.

Mitigation & Contingency

Mitigation: Define a clear contract for temporary ID generation (e.g., UUID prefixed with 'local-') and implement a dedicated reconciliation method in ActivityRepository that atomically swaps the temporary ID. Write integration tests that simulate the full optimistic → confirm cycle.

Contingency: If reconciliation proves too complex, fall back to a simpler non-optimistic insert with a loading spinner for the network round-trip. The UX degrades slightly but correctness is preserved. Re-introduce optimistic behaviour once the pattern is stable.

high impact medium prob integration

Supabase row-level security policies on the activities table may not be configured to match the access patterns required by the client. If RLS blocks inserts or selects for the authenticated peer mentor session, all activity registration operations will silently fail or return empty results, which is difficult to diagnose in production.

Mitigation & Contingency

Mitigation: Define and test RLS policies in a dedicated Supabase migration script as part of this epic. Create integration tests that execute against a local Supabase instance with RLS enabled, covering insert, select by peer mentor ID, and denial of cross-mentor access.

Contingency: Maintain a fallback service-role client path (server-side only) that can be activated via a feature flag if client-side RLS is blocking legitimate operations while policies are corrected.

medium impact low prob technical

SharedPreferences on Flutter can become corrupted if the app crashes mid-write or if the device runs out of storage. A corrupted last-used activity type preference would cause the defaults manager to return null or an invalid ID, breaking the zero-interaction happy path.

Mitigation & Contingency

Mitigation: Wrap all LocalStorageAdapter reads in try/catch with typed safe defaults. Validate the retrieved activity type ID against the known list before returning it. Use atomic write operations where the platform supports them.

Contingency: If the preference store is corrupted, silently reset to the hardcoded default (first activity type alphabetically or 'general') and log a warning. The user loses their last-used preference but the app remains functional.