high priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

On first request for a path, a signed URL is generated via Supabase Storage SDK and the result is cached with an expiry of `now + ttl`
On subsequent requests for the same path within TTL, the cached URL is returned and Supabase Storage SDK is NOT called (verifiable by mock call count assertions)
On request for a path after TTL expiry, a new signed URL is generated, the cache entry is replaced, and the new URL is returned
The TTL defaults to 55 minutes (3300 seconds) but is injectable at construction time for testing (e.g., passing 1 second TTL in tests)
Cache entries are stored keyed by exact storage path string — paths are case-sensitive
If Supabase returns an error generating a signed URL, Left<SignedUrlError> is returned and no cache entry is written for the failed path
Cache has a maximum entry limit (default 200 entries) to prevent unbounded memory growth in long-lived sessions; LRU eviction removes the least-recently-used entry when limit is reached
Cache is not shared across org boundaries — the service is scoped per org session (enforced by Riverpod provider scoping in task-008)

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase Storage SDK (createSignedUrl)
data models
activity
performance requirements
Cache lookup must be O(1) — use a HashMap for storage with a doubly-linked list for LRU ordering, or use an existing LRU package
Cache hit must return in under 1 ms (pure in-memory operation)
No disk persistence for the cache — in-memory only, cache is lost on app restart (acceptable: URLs regenerated on next request)
security requirements
Signed URLs expire after 60 minutes on Supabase side; cache TTL of 55 minutes ensures clients never receive an expired URL
Cache entries must not be accessible across org sessions — provider scoping (task-008) is the enforcement mechanism
Signed URLs must not be logged in any form — they provide time-limited access to private files

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use a `LinkedHashMap` where `_CacheEntry` holds `{String signedUrl, DateTime expiresAt}`. For LRU, maintain insertion/access order by removing and re-inserting on each access. Alternatively, use the `lru_map` pub.dev package if already in the dependency graph. Inject the clock as `DateTime Function() clock = DateTime.now` for testability — this avoids `await Future.delayed` in tests.

The `getSignedUrl(String path)` method signature should return `Future>`. The max-entry cap (200) should be a constructor parameter with a sensible default. Document that the cache is intentionally not persisted across hot restarts — this is by design given signed URL expiry semantics.

Testing Requirements

Unit tests (flutter_test): (1) cold cache generates URL and populates cache, (2) warm cache hit returns same URL without calling storage SDK — assert mock called once total across two requests, (3) expired cache entry triggers new URL generation — use injected clock or TTL=1s + await delay, (4) failed URL generation returns Left and does not pollute cache, (5) LRU eviction: fill cache to 200 entries, add one more, verify oldest entry is evicted, (6) TTL boundary: entry at exactly TTL-1ms is a hit, entry at exactly TTL+1ms is a miss. Use a fake/mock clock (pass `DateTime Function() clock` to constructor) to make TTL expiry tests deterministic without real delays.

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.