critical priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

An abstract Dart class `AttachmentUploadService` is defined with at minimum three method signatures: uploadAttachment, deleteAttachment, and validateAttachment (or equivalent names matching the domain)
uploadAttachment accepts orgId, activityId, fileName, bytes (Uint8List), and mimeType; returns Future<Result<AttachmentUploadResult, AttachmentUploadError>>
deleteAttachment accepts storagePath (and optionally attachmentId); returns Future<Result<void, AttachmentUploadError>>
validateAttachment (or inline validation) checks MIME type and file size before attempting upload
AttachmentUploadError is a sealed class (freezed or Dart 3 sealed) with at minimum three variants: fileTooLarge(maxBytes, actualBytes), invalidMimeType(mimeType), uploadFailed(Object cause)
The sealed class allows exhaustive pattern-matching in Dart switch expressions without a default branch
AttachmentUploadResult is a value object containing at minimum: storagePath, attachmentId, fileName
The interface file contains no implementation logic — it is a pure contract definition
The file is placed in the correct domain/service layer directory (e.g., lib/features/attachments/domain/ or lib/services/attachments/)
A corresponding barrel export or part file is created if the project uses them

Technical Requirements

frameworks
Flutter
freezed (for sealed error union)
dart_mappable or built_value as alternative if freezed is not used
data models
AttachmentUploadResult (storagePath, attachmentId, fileName, mimeType, fileSizeBytes)
AttachmentUploadError (sealed: fileTooLarge, invalidMimeType, uploadFailed)
performance requirements
Interface definition only — no performance constraints at this layer
security requirements
fileTooLarge variant must carry maxBytes and actualBytes so the UI can display a meaningful error without hardcoding limits
invalidMimeType variant must carry the rejected mimeType string so the UI can name the rejected format
uploadFailed must wrap the original cause (Object) to preserve the error chain for logging, but callers must NOT surface the raw cause to end users

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use Dart 3 `sealed class AttachmentUploadError` syntax (no freezed required if targeting Dart 3+): `sealed class AttachmentUploadError {}`, then `final class FileTooLargeError extends AttachmentUploadError { final int maxBytes; final int actualBytes; ... }`, etc. Alternatively use freezed with `@freezed class AttachmentUploadError with _$AttachmentUploadError` for JSON serialisation support if errors are ever logged remotely. For the allowed MIME types list, define a `const Set kAllowedAttachmentMimeTypes` in a constants file (e.g., `{'application/pdf', 'image/jpeg', 'image/png', 'image/heic', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'}`) so both the interface and future UI validation share the same source of truth.

Define `const int kMaxAttachmentBytes = 10 * 1024 * 1024` (10 MB) as a named constant. Keep the abstract class free of imports from the infrastructure layer — it should only depend on Dart core and the domain models.

Testing Requirements

No runtime tests for a pure interface definition. However, verify the sealed class is exhaustive by writing a compile-time test (a standalone Dart file in test/ that switch-matches all variants without a default clause — if it compiles, exhaustiveness is confirmed).

Optionally write a unit test for any static validation helper (e.g., isAllowedMimeType) defined alongside the interface.

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.