critical priority high complexity backend pending backend specialist Tier 3

Acceptance Criteria

Supabase Storage upload executes in a background isolate — main isolate frame rate does not drop below 55 fps during upload of a 500 KB file
UploadProgress.uploading(percentage) events are emitted at minimum every 5% increment
On transient network error (SocketException, timeout), the upload is retried up to 3 times with exponential backoff: 1 s, 2 s, 4 s delays
On permanent error (HTTP 4xx from Supabase) or after exhausting retries, the stream emits UploadProgress.failed with the specific failure reason
Calling cancelUpload(claimId) stops the background isolate's upload, emits UploadProgress.cancelled, and cleans up any partially uploaded data from Supabase Storage
The expense form remains fully interactive (text fields, dropdowns, submit button) during an ongoing upload
Form submission proceeds even if upload is still in progress — the claim is saved with receipt_status: 'pending' and updated to 'attached' when upload completes
If the app is backgrounded during upload, the isolate continues and completes (within OS background execution time limits)
Retry attempt number is included in the UploadProgress.retrying(attempt, maxAttempts) event so the UI can display 'Retrying 2/3'
All retry and cancellation paths have unit test coverage

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Flutter SDK
apis
Supabase Storage API (multipart upload with progress callback)
Dart Isolate API or Flutter compute()
data models
UploadProgress (extended sealed: compressing | uploading(int) | retrying(int, int) | completed(String) | failed(Failure) | cancelled)
RetryPolicy (maxAttempts: int, baseDelayMs: int)
UploadCancellationToken
performance requirements
Main thread must remain unblocked — all I/O in isolate or async gaps only
Backoff delays must use Future.delayed and not block any isolate's event loop
Memory: isolate must release compressed bytes after upload — no lingering large allocations
security requirements
Cancellation must issue a DELETE to Supabase Storage to remove the partial upload — do not leave orphaned objects
Background isolate must not retain a reference to the user's auth token beyond the upload operation — pass it as a message parameter, do not store globally
Network calls must use HTTPS exclusively (enforced by Supabase SDK)

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Flutter isolates cannot share objects — you must pass the compressed Uint8List and auth token as primitive-compatible messages to the isolate via SendPort/ReceivePort. The Supabase Flutter SDK's storage uploadBinary method accepts an onUploadProgress callback — call this from the isolate and send progress integers back via the ReceivePort. For retry logic, implement a RetryPolicy value object with maxAttempts and baseDelayMs; the exponential delay is baseDelayMs * 2^(attempt-1). Distinguish transient errors (SocketException, TimeoutException, HTTP 503/429) from permanent ones (HTTP 400/401/403/404) before deciding to retry.

For cancellation, use an IsolateCancel flag: the isolate checks it between retries and during the progress callback; on cancel, it deletes the partial upload and sends a cancelled message. Note that Supabase Storage does not support true resumable uploads via the Flutter SDK as of 2025 — cancellation deletes and the next call starts fresh. Expose cancelUpload(claimId) from the service class, keyed by claimId so concurrent uploads can be cancelled independently.

Testing Requirements

Unit tests (flutter_test): mock the Supabase Storage client within the isolate by injecting a factory. Test retry logic: simulate SocketException on attempt 1 and 2, success on attempt 3 — assert three upload calls and correct backoff delays using fake async. Test exhausted retries: three consecutive SocketExceptions — assert UploadProgress.failed. Test cancellation: cancel mid-upload — assert cancelled event and StorageRepository.delete called.

Test permanent error (403): assert no retry, immediate failed. Integration test: upload a real 400 KB JPEG to a Supabase test bucket via the service, assert progress events received, file present in bucket, and claim_receipts row linked. Use flutter_test's FakeAsync to test backoff timing without real delays.

Component
Receipt Attachment Service
service high
Epic Risks (3)
high impact medium prob technical

Non-blocking upload creates a race condition: if the claim record is submitted and saved before the upload completes, the storage path may never be written to the claim_receipts table, leaving the claim with a missing receipt that was nonetheless required.

Mitigation & Contingency

Mitigation: Design the attachment service to queue a completion callback that writes the storage path to the claim record upon upload completion, even after the claim form has submitted. Use a local task queue with persistence to survive app backgrounding. Test the race condition explicitly with simulated slow uploads.

Contingency: If the async path association proves unreliable, fall back to blocking upload before claim submission with a clear progress indicator, accepting the UX trade-off in exchange for data integrity.

high impact medium prob scope

The offline capture requirement (cache locally, sync when connected) significantly increases state management complexity. If the offline queue is not durable, receipts captured without connectivity may be lost when the app is killed, causing claim submission failures users are not aware of.

Mitigation & Contingency

Mitigation: Persist the offline upload queue to local storage (e.g., Hive or SQLite) on every state transition. Implement background sync using WorkManager (Android) and BGTaskScheduler (iOS). Scope the initial delivery to online-only flow if offline sync cannot be adequately tested before release.

Contingency: Ship without offline support in the first release, displaying a clear 'Upload requires connection' message. Add offline sync as a follow-on task once the core online flow is validated in production.

medium impact low prob integration

The inline bottom sheet presentation within a multi-step wizard can conflict with existing modal navigation and back-button handling, particularly if the expense wizard itself uses nested navigation or custom route management.

Mitigation & Contingency

Mitigation: Review the expense wizard navigation architecture before implementation. Use showModalBottomSheet with barrier dismissal disabled to prevent accidental dismissal. Coordinate with the expense wizard team on modal stacking behavior and ensure the camera sheet does not interfere with wizard step transitions.

Contingency: If modal stacking causes navigation issues, present the camera sheet as a full-screen dialog using PageRouteBuilder with a transparent barrier, preserving wizard state via the existing Bloc while still appearing inline.