critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

Files exceeding 10 MB (10,485,760 bytes) are rejected before any network call is made, returning AttachmentUploadError.fileTooLarge with the actual byte size included in the error payload
Files with MIME types outside [application/pdf, image/jpeg, image/png] are rejected with AttachmentUploadError.unsupportedMimeType including the detected type in the error payload
MIME type validation reads the actual file header bytes (magic bytes), not just the file extension, to prevent spoofing via renamed files
Empty files (0 bytes) are rejected with AttachmentUploadError.emptyFile
Valid files return Right<ValidatedFile> containing sanitized filename, byte length, and confirmed MIME type
All validation errors return Left<AttachmentUploadError> — no exceptions are thrown or propagated to callers
Validation is synchronous where possible; if async (e.g., reading bytes), it completes before any StorageAdapter call
The allowlist is defined as a compile-time constant, not a runtime-configurable value, to prevent accidental misconfiguration

Technical Requirements

frameworks
Flutter
BLoC
Riverpod
apis
Supabase Storage (not called during validation — validation gates the upload)
data models
activity
performance requirements
File size check must complete in under 50 ms for files up to 10 MB
MIME type detection must read no more than the first 16 bytes of the file to minimize memory allocation
security requirements
Magic byte validation (file header inspection) must be used instead of relying on file extension or Content-Type header provided by the client
Rejected files must not be partially buffered or cached anywhere in the service layer
Validation errors must not leak internal file paths or system metadata in error messages exposed to the UI layer
Receipt images containing PII are handled — ensure ValidatedFile does not store raw bytes in memory longer than the validation pass

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use the `mime` Dart package or a custom magic-byte map to detect MIME from file content. Define an `AttachmentUploadError` sealed class (Dart 3 sealed) with variants: `fileTooLarge`, `unsupportedMimeType`, `emptyFile`, and `unknown`. The `ValidatedFile` value object should be immutable and contain: `originalFilename` (sanitized), `byteLength`, `mimeType`, and `bytes` (Uint8List). Use the `fpdart` or `dartz` package for `Either` if already in the dependency graph; otherwise implement a lightweight local Either.

Do not introduce a new functional programming dependency just for this task if one is not already present — a simple sealed Result class is acceptable. Keep the validation function pure (no side effects) to make it trivially testable.

Testing Requirements

Unit tests (flutter_test) must cover: (1) file exactly at 10 MB boundary is accepted, (2) file one byte over is rejected, (3) each allowlisted MIME type is accepted via magic bytes, (4) each rejected MIME type (GIF, WEBP, HEIC, MP4) returns the correct error variant, (5) empty file returns emptyFile error, (6) file with PDF extension but JPEG magic bytes is classified as image/jpeg (not application/pdf), (7) validateFile returns Right with correct metadata fields populated. No integration tests required for this task — pure unit tests against the validation function are sufficient. Target 100% branch coverage on the validation logic.

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.