critical priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

An abstract Dart class `AttachmentSignedUrlService` is defined with at minimum three method signatures: generateSignedUrl, invalidateCacheEntry, and prefetchSignedUrls
generateSignedUrl accepts storagePath and optional expiresInSeconds (default 3600); returns Future<Result<SignedUrlResult, AttachmentSignedUrlError>>
invalidateCacheEntry accepts storagePath and returns void or Future<void> — evicts the cached URL for that path
prefetchSignedUrls accepts a List<String> of storagePaths and returns Future<Map<String, Result<SignedUrlResult, AttachmentSignedUrlError>>> for bulk pre-loading
AttachmentSignedUrlError is a sealed class with at minimum one variant: urlGenerationFailed(String storagePath, Object cause)
SignedUrlResult is an immutable value object with two fields: url (String) and expiresAt (DateTime in UTC)
SignedUrlResult exposes a computed property isExpired that returns true when DateTime.now().toUtc() is after expiresAt
The interface file contains no caching implementation — caching is an implementation detail left to the concrete class
The interface file is placed in the correct domain/service layer directory alongside AttachmentUploadService
All types are exported from the attachments domain barrel file

Technical Requirements

frameworks
Flutter
freezed or Dart 3 sealed classes for error union
equatable or freezed for SignedUrlResult value equality
data models
SignedUrlResult (url: String, expiresAt: DateTime)
AttachmentSignedUrlError (sealed: urlGenerationFailed(storagePath, cause))
performance requirements
prefetchSignedUrls signature accepts a list to enable the implementation to batch requests or parallelise with Future.wait — the interface must not constrain this
SignedUrlResult.isExpired must be a simple DateTime comparison — O(1)
security requirements
urlGenerationFailed must carry the storagePath for logging/debugging but implementations must ensure this is not surfaced to the end user (it may contain org/activity UUIDs)
SignedUrlResult.url must never be stored persistently (no DB, no SharedPreferences) — it is an in-memory cache value only; the interface comment should state this explicitly
expiresAt must be stored and compared in UTC to prevent timezone-related early-expiry bugs on devices with non-UTC local time

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Model the interface signature for prefetchSignedUrls as: `Future>> prefetchSignedUrls(List storagePaths);` — the Map key is storagePath. This allows the concrete implementation to use `Future.wait` over all paths and return per-path results. For SignedUrlResult, use a `const` constructor with final fields and override `==` / `hashCode` (either manually, via equatable, or via freezed) so it works correctly as a map value. Keep `isExpired` as a getter: `bool get isExpired => DateTime.now().toUtc().isAfter(expiresAt);`.

Do NOT add the cache map or any mutable state to the abstract class — those belong in `SupabaseAttachmentSignedUrlService` (the concrete implementation in a later task). Add a doc comment to the abstract class stating that implementations SHOULD cache results in memory for the duration of the signed URL's validity and MUST invalidate on soft-delete.

Testing Requirements

No runtime tests for a pure interface. Write a compile-time exhaustiveness test for the sealed error class in test/ (switch on AttachmentSignedUrlError variants without a default clause). Write a unit test for SignedUrlResult.isExpired: (1) construct a result with expiresAt 1 hour in the future — assert isExpired is false; (2) construct with expiresAt 1 second in the past — assert isExpired is true. These are pure Dart unit tests with no mocking required.

Component
Signed URL Service
service low
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.