critical priority medium complexity infrastructure pending infrastructure specialist Tier 0

Acceptance Criteria

A private Supabase Storage bucket named 'receipts' is created with public access disabled
RLS INSERT policy allows authenticated users to upload only to paths starting with receipts/{their_org_id}/{their_user_id}/
RLS SELECT policy allows authenticated users to read only objects where path starts with receipts/{their_org_id}/
RLS DELETE policy allows authenticated users to delete only objects where path starts with receipts/{their_org_id}/{their_user_id}/
No policy allows any authenticated user to read or write paths belonging to a different org_id
Service role (used only by backend functions and test helpers) bypasses RLS as per Supabase default — this is explicitly documented
Bucket is configured with a maximum file size limit of 10 MB per object
Allowed MIME types restricted to image/jpeg and image/png at the bucket policy level
All policies are captured in a version-controlled SQL migration file (supabase/migrations/)
A path convention document (or inline comments in the migration) describes the receipts/{org_id}/{user_id}/{claim_id}/{filename} structure and naming rules for filename (e.g., UUID + extension)

Technical Requirements

frameworks
Supabase
apis
Supabase Storage API
Supabase Auth (for auth.uid() and JWT claims in RLS policies)
data models
ClaimReceiptAttachment
performance requirements
Bucket region must match the Supabase project region to minimise upload latency for Norwegian users (eu-central-1 or eu-west-1)
Signed URL generation must support expiry windows from 60 seconds to 7 days
security requirements
Bucket must be private — no public URL access
org_id used in RLS must be derived from the user's JWT claims or a server-side lookup, never from client-supplied parameters alone
All RLS policies must be tested against a second org's credentials to confirm cross-org isolation
MIME type restriction at bucket level prevents non-image uploads even if client-side validation is bypassed

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

In Supabase RLS for Storage, policies reference the storage.objects table. The path column contains the full object key including bucket name prefix. Use a policy condition like: (storage.foldername(name))[1] = auth.uid()::text is NOT sufficient alone — org scoping requires checking that (storage.foldername(name))[1] matches the user's org_id from their profile or JWT claim. Prefer reading org_id from a server-side user_profiles table (auth.uid() lookup) inside the RLS policy to avoid JWT claim spoofing.

Coordinate with the auth team to ensure org_id is reliably available. For filename, enforce UUID v4 + extension (e.g., 550e8400-e29b-41d4-a716-446655440000.jpg) from the client side to prevent path traversal or conflicting filenames. Document in the migration file that the bucket name 'receipts' is intentionally lowercase and must match exactly in all SDK calls.

Testing Requirements

Manual verification required before any application code is written against this bucket: (1) use the Supabase dashboard Storage explorer to attempt an upload as user-A to user-B's path — confirm it is rejected with a 403; (2) attempt to read user-B's object as user-A — confirm 403; (3) upload a valid JPEG as user-A to their own path — confirm 200; (4) attempt upload of a .pdf file — confirm rejection. Capture screenshots or record the test session as evidence. These manual tests supplement the integration tests in task-015.

Component
Receipt Storage Repository
data medium
Epic Risks (3)
high impact medium prob security

Supabase Storage RLS policies using org/user/claim path scoping may not enforce correctly if claim ownership is not present in the JWT or if path segments are constructed differently at upload vs. read time, leading to data leakage or access denial for legitimate users.

Mitigation & Contingency

Mitigation: Define and test RLS policies in isolation before wiring to app code. Write integration tests that assert cross-org and cross-user access is denied. Use service-role key only in edge functions, never in client code.

Contingency: If client-side RLS proves insufficient, route all storage reads through a Supabase Edge Function that validates ownership before generating signed URLs, adding a controlled server-side enforcement layer.

high impact medium prob technical

Aggressive image compression may reduce receipt legibility below the threshold required for financial auditing, causing claim rejections or compliance failures despite technically successful uploads.

Mitigation & Contingency

Mitigation: Define minimum legibility requirements with HLF finance team before implementation. Set compression targets conservatively (e.g., max 1MB, min 80% JPEG quality) and validate with sample receipt images. Provide compression statistics in verbose/debug mode.

Contingency: If post-compression quality is disputed by auditors, increase the quality floor at the cost of larger file sizes, and add a manual override allowing users to skip compression for PDFs and high-quality scans.

medium impact medium prob dependency

The Flutter image_picker package behaves differently on iOS 17+ (PHPicker) vs older Android (Intent-based), particularly for file types, permission flows, and PDF selection, which may cause platform-specific failures not caught in development.

Mitigation & Contingency

Mitigation: Test image picker integration on physical devices for both platforms early in the sprint. Pin the image_picker package version and review changelogs before updates. Write widget tests using mock file results for each platform branch.

Contingency: If PHPicker or Android Intent differences cause blocking issues, implement separate platform-specific picker delegates behind the unified interface, allowing platform-specific fixes without breaking the shared API.