critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

On successful upload and metadata persist, the method returns Right<AttachmentMetadata> containing the storage path, activity ID, file size, MIME type, and created_at timestamp
If StorageAdapter.upload fails, the method returns Left<AttachmentUploadError.storageFailure> and no metadata record is written
If ActivityAttachmentRepository.save fails after a successful upload, StorageAdapter.delete is called with the just-uploaded path before returning Left<AttachmentUploadError.metadataPersistFailure>
If the rollback delete itself fails (e.g., network error during cleanup), the error is logged with the orphaned storage path and the original metadata error is still returned to the caller — the rollback failure must not mask the primary error
Storage path is org-scoped: format is `{org_id}/activities/{activity_id}/{uuid}.{ext}` to enforce tenant isolation
The upload method accepts a `ValidatedFile` (output of task-003) — raw `File` or `Uint8List` are not accepted directly
No orphaned storage objects exist after any failure scenario (verified by integration test)
The method is idempotent for the same validated file and activity ID combination when retried after a transient failure

Technical Requirements

frameworks
Flutter
BLoC
Riverpod
apis
Supabase Storage SDK (upload, delete)
Supabase PostgreSQL 15 (via ActivityAttachmentRepository)
data models
activity
performance requirements
Upload progress must be trackable via a Stream<double> emitted by the service so the BLoC layer can relay progress to the UI
Rollback delete must be attempted within the same call stack — no deferred/background cleanup jobs
security requirements
Storage bucket must be private — signed URLs required for retrieval (not public access)
Storage path includes org_id prefix to enforce Row-Level Security and prevent cross-org file access
Service role key is never used client-side — all Supabase Storage calls use the authenticated user JWT via the Supabase Flutter SDK
Exif metadata must be stripped from JPEG images before upload (use image package or equivalent)
Receipt images containing PII stored in private Supabase Storage bucket per security policy

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Structure the orchestration as a single async method `uploadAttachment(ValidatedFile file, String activityId, String orgId)`. Generate the storage path deterministically using `uuid` package for the file ID segment. Use Supabase Flutter SDK's `storage.from(bucket).uploadBinary()` which supports progress callbacks. The rollback pattern: assign `String?

uploadedPath` before the try block, set it after upload succeeds, check it in the catch block before calling delete. Log the orphaned path using the app's structured logger (not print). The `ActivityAttachmentRepository.save` call should use a Supabase insert with `returning: MinimalReturningOption.minimal` for efficiency. Ensure the bucket name is not hardcoded — inject it via the Riverpod provider configuration.

Testing Requirements

Integration tests (flutter_test with mocked StorageAdapter and Repository): (1) happy path — upload succeeds, metadata saved, Right returned, (2) storage failure — upload throws, metadata not called, Left(storageFailure) returned, (3) metadata failure after upload — delete called with correct path, Left(metadataPersistFailure) returned, (4) metadata failure + rollback delete failure — original error returned, orphan path logged, (5) org_id prefix present in all generated storage paths. Use `mockito` or `mocktail` to mock StorageAdapter. Verify delete is called exactly once on metadata failure using `verify()`. Unit test the path-generation logic independently.

Do not call real Supabase in CI tests.

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

The storage upload succeeds but the subsequent metadata insert fails. The rollback delete call to Supabase Storage could itself fail (network error, transient timeout), leaving an orphaned object in the bucket with no database record pointing to it — a cost and compliance risk that also breaks delete-on-cascade logic.

Mitigation & Contingency

Mitigation: Wrap the rollback delete in a retry loop (3 attempts, exponential back-off). Log orphaned-object incidents to a dedicated structured log stream for periodic audit. Consider a scheduled Supabase Edge Function that reconciles storage objects against database records and flags orphans.

Contingency: If orphaned objects accumulate, run the reconciliation edge function manually to identify and purge them. Add a monitoring alert for metadata insert failures after successful uploads so the issue is caught within minutes.

medium impact medium prob scope

If the signed URL TTL is set too short, users browsing the attachment preview modal on slow connections will receive expired URLs before the content loads, causing a broken experience. If set too long, a URL shared outside the app (e.g., pasted into a chat) remains valid beyond the intended access window.

Mitigation & Contingency

Mitigation: Default TTL to 60 minutes, configurable via a named constant. The in-memory cache TTL should be set to TTL minus 5 minutes to ensure cached URLs are refreshed before they expire. Document the trade-off in code comments.

Contingency: If users report broken previews, shorten the cache TTL hotfix. If a URL leak is reported, rotate the Supabase storage signing secret to invalidate all outstanding signed URLs immediately.

medium impact medium prob technical

The multi-attachment user story requires parallel uploads with individual progress indicators. Managing concurrent BLoC events for 3–5 simultaneous uploads risks state collisions, progress indicator mixups, or partial rollbacks that are difficult to reason about.

Mitigation & Contingency

Mitigation: Design the BLoC to maintain a per-attachment upload state map keyed by a client-generated UUID. Each upload runs as an isolated Future with its own result emitted as a typed event. Write integration tests for 3-concurrent-upload scenarios.

Contingency: If state collisions occur in production, fall back to sequential upload processing (one at a time) gated behind a feature flag until the concurrent model is stabilised.