high priority low complexity infrastructure pending infrastructure specialist Tier 2

Acceptance Criteria

createSignedUrl(storagePath, {int expiresInSeconds = 3600}) returns a non-empty URL string on success
The method calls the Supabase Storage createSignedUrl API with the 'activity-attachments' bucket, the exact storagePath, and the given expiresInSeconds value
When expiresInSeconds is omitted, the default of 3600 is used
The method returns a typed Result<String, StorageSignedUrlError> (or throws a typed exception) on failure
StorageSignedUrlError wraps the underlying Supabase StorageException and does not expose raw SDK types to callers
The returned URL is a valid HTTPS URL (starts with 'https://')
Unit tests verify the correct bucket name ('activity-attachments'), exact path forwarding, and the expiresInSeconds value passed to the SDK
Unit tests cover: successful URL generation, storage error (SDK throws), and default expiry used when parameter omitted

Technical Requirements

frameworks
Flutter
Supabase Dart SDK (supabase_flutter)
apis
Supabase Storage createSignedUrl API (storage.from(bucket).createSignedUrl(path, expiresIn))
data models
SignedUrlResult (url: String, expiresAt: DateTime) — optional value object if needed by callers
performance requirements
Signed URL generation is a single lightweight API call; no caching is implemented at the adapter layer — caching belongs in the service layer (AttachmentSignedUrlService)
security requirements
Signed URLs are time-limited; 3600 seconds (1 hour) is the default — callers should not request expiries beyond 86400 seconds (24 hours)
The adapter must validate that expiresInSeconds is between 1 and 86400 and throw an ArgumentError for out-of-range values
Signed URL strings must not be logged in production (they grant access to private files)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Add `createSignedUrl` as a new method on `SupabaseStorageAdapter` (extends the existing class from task-006). Signature: `Future> createSignedUrl(String storagePath, {int expiresInSeconds = 3600})`. Call: `await supabaseClient.storage.from('activity-attachments').createSignedUrl(storagePath, expiresInSeconds)`. The Supabase SDK returns the URL string directly — wrap it in `Result.ok(url)`.

Validate the expiry range at the top of the method before making the SDK call. Keep `StorageSignedUrlError` in the same sealed error file as `StorageUploadError` and `StorageDeleteError` from task-006 for cohesion. If `SignedUrlResult` (URL + expiry timestamp) is needed by `AttachmentSignedUrlService` (task-002), return it from this method; otherwise a plain `String` is sufficient at the adapter layer.

Testing Requirements

Unit tests using mockito/mocktail to mock `SupabaseStorageFileApi`. Test cases: (1) successful signed URL — verify bucket 'activity-attachments', correct storagePath, and expiresInSeconds are forwarded to SDK, assert returned string equals mock URL; (2) default expiry — call without second argument and verify 3600 is passed to SDK; (3) storage error — SDK throws StorageException, adapter returns/throws StorageSignedUrlError containing the original exception; (4) invalid expiresInSeconds (0 or >86400) — ArgumentError thrown before SDK call. All tests are pure unit tests with no network I/O.

Component
Supabase Storage Adapter
infrastructure low
Epic Risks (3)
high impact medium prob security

Supabase RLS policies may not cover all query paths (e.g., service-role key usage in edge functions), potentially exposing attachment metadata or objects from another organisation to an unauthorised actor, breaching GDPR requirements.

Mitigation & Contingency

Mitigation: Add org_id scoping as an explicit WHERE clause at the Dart repository level as a second line of defence. Document which queries use the anon key versus service-role key, and audit all edge function calls that touch the storage bucket.

Contingency: If a bypass is discovered post-deployment, immediately revoke the affected signed URLs, rotate the service-role key, add the missing org_id filter, and deploy a patch. Notify affected organisations per GDPR breach protocol.

medium impact low prob dependency

Supabase free/pro tier storage quotas may be exceeded earlier than expected if organisations upload large PDFs frequently, causing upload failures with no graceful degradation for users.

Mitigation & Contingency

Mitigation: Configure a 10 MB per-file cap enforced in the upload service (Epic 2), and add a storage usage monitoring alert at 80% of the allocated quota. Document the upgrade path in runbooks.

Contingency: If the quota is hit, temporarily disable new uploads via the org-level feature flag (attachments_enabled) and upgrade the Supabase plan. Communicate clearly to affected coordinators with an estimated restoration time.

high impact low prob integration

The feature documentation specifies a migration order dependency: the activity_attachments table must be created after the activities table and before the Bufdir export join query is updated. Running migrations out of order will cause foreign-key or join failures.

Mitigation & Contingency

Mitigation: Add the migration to the numbered Supabase migration sequence immediately after the activities table migration. Add a CI check that runs migrations in order against a clean schema.

Contingency: If a deployment runs migrations out of order, roll back via the Supabase migration rollback script, reorder, and redeploy. No data loss occurs as attachments do not exist yet at that point.