Implement SupabaseActivityClient with typed queries
epic-quick-activity-registration-data-infrastructure-task-003 — Implement the SupabaseActivityClient wrapping the Supabase Flutter SDK. Provide typed methods: insertActivity(ActivityRecord) → Future<ActivityRecord>, fetchActivities({required String peerId}) → Future<List<ActivityRecord>>, and deleteActivity(String id). Use PostgREST query construction against the 'activities' table. All methods must be fully typed — no dynamic maps in public interfaces. Handle Supabase errors by throwing domain exceptions.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Use supabase.from('activities').insert(payload).select().single() for insertActivity to get the server row back in one call. Before insert, replace any 'local_' prefixed id with null (or omit the id field entirely) so PostgreSQL generates the real UUID server-side. After a successful insert, set syncStatus to SyncStatus.synced on the returned record. Wrap the entire PostgREST chain in a try/catch for PostgrestException and rethrow as ActivityNetworkException(message: e.message, code: e.code).
Define ActivityNetworkException as a simple domain exception class in lib/core/exceptions/. Avoid chaining too many PostgREST modifiers in a single expression — break into local variables for readability and easier debugging.
Testing Requirements
Unit tests with a mocked SupabaseClient using mocktail. Test insertActivity: stub the PostgREST chain to return a JSON map, assert the returned ActivityRecord has syncStatus == synced and the server-assigned id (no local_ prefix). Test fetchActivities: stub select response with a list of JSON maps, assert correct deserialization into List
Test error handling: stub any method to throw PostgrestException, assert ActivityNetworkException is thrown. Integration tests against a local Supabase instance (optional, CI-gated): verify actual insert/fetch/delete round-trips with RLS enabled.
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.
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.
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.