critical priority low complexity infrastructure pending infrastructure specialist Tier 0

Acceptance Criteria

Supabase Storage bucket named 'expense-receipts' created with public: false (private bucket — no public URL access)
Bucket file size limit set to 5 MB per file
Allowed MIME types restricted to image/jpeg, image/png, image/webp, application/pdf
RLS INSERT policy: authenticated users can upload only to paths matching '{their_org_id}/{any_claim_id}/*' — org_id verified against JWT claims
RLS SELECT policy: claimants can read their own receipts at '{org_id}/{claim_id}/*' where claim's claimant_id = auth.uid()
RLS SELECT policy: coordinators can read all receipts under their org's path prefix '{org_id}/**'
RLS DELETE policy: claimants can delete receipts only for claims in 'draft' status — requires a security definer function or edge function to check claim status
No RLS UPDATE policy — receipts are immutable after upload; replacement requires delete + re-upload
CORS configuration allows requests from the app's Supabase project URL and localhost:3000 (dev)
Signed URL expiry configured to 3600 seconds (1 hour) as project default
Bucket configuration stored as Supabase CLI seed/migration script (not manual Studio setup) so it is reproducible
Attempting to access a receipt from another org returns 403, verified by test

Technical Requirements

frameworks
Supabase CLI
Supabase Storage
apis
Supabase Storage (bucket policies)
Supabase Auth (auth.uid(), JWT claims for org_id)
data models
claim_event
performance requirements
Signed URL generation must complete in under 200ms
Bucket should support concurrent uploads from up to 50 simultaneous users without throttling
security requirements
Bucket must be private — no public URL access under any circumstances
Receipt images contain PII (names on receipts, amounts) — classified as sensitive personal data under GDPR
Path structure must include org_id prefix to enable org-level RLS without full-table scans
Exif metadata stripping must be enforced at upload time (handled in task-006 adapter, not bucket policy)
Signed URLs expire after 1 hour — never generate permanent URLs for receipts
Service role key used only in edge functions — mobile client uses anon key with RLS

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Define the bucket using Supabase CLI's `supabase/seed.sql` or a dedicated storage migration script so it can be reproduced in staging and production environments without manual Studio clicks. The path convention `{org_id}/{claim_id}/{filename}` is critical — the RLS policies rely on this structure using Supabase Storage's `name` column (which contains the full path) and string prefix matching. For the coordinator read policy, use a helper function `get_user_org_id()` that reads from a user_roles or org_members table to avoid hardcoding org_id in JWT claims. Document the signed URL expiry as a configurable constant (RECEIPT_SIGNED_URL_TTL_SECONDS = 3600) referenced from both the bucket config and the adapter in task-006.

Testing Requirements

Write Supabase Storage RLS tests using the Supabase test helpers or manual curl-based scripts: (1) Verify authenticated claimant can upload to their org/claim path; (2) Verify claimant cannot upload to a different org's path; (3) Verify coordinator can read all files under their org prefix; (4) Verify claimant from org A cannot read receipts from org B; (5) Verify file size limit rejects files over 5 MB; (6) Verify MIME type restriction rejects text/plain uploads. Run these as part of the infrastructure smoke test suite in CI.

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

Row-level security policies for expense claims must correctly scope data to organisation, role (peer mentor sees own claims only, coordinator sees org-wide queue), and claim status. Incorrect RLS can expose claims cross-organisation or prevent coordinators from accessing the attestation queue.

Mitigation & Contingency

Mitigation: Define RLS policies in code-reviewed migration files. Write integration tests that attempt cross-org reads with different JWT roles and assert access denial. Review with a second engineer before merging migrations.

Contingency: If RLS is misconfigured post-deployment, disable the affected policy temporarily and apply a hotfix migration within the same release window. No claim data is exposed publicly due to Supabase project-level auth requirement.

medium impact medium prob technical

The auto-approval Edge Function is triggered server-side on expense insert. Cold-start latency or Edge Function failures can block the submission response and degrade UX, especially on mobile networks.

Mitigation & Contingency

Mitigation: Implement the auto-approval Edge Function client with a timeout and graceful fallback: if no result is received within 5 seconds, treat the claim as 'pending' and poll for the status update via Supabase Realtime. Keep the Edge Function warm with a periodic ping.

Contingency: If Edge Function reliability is unacceptable, move auto-approval evaluation to a database trigger or Postgres function as an interim measure, accepting that threshold configuration changes require a migration rather than a settings update.

medium impact low prob scope

The expense type catalogue and threshold configuration are cached locally for offline use. If an organisation updates their catalogue exclusion rules or thresholds while a peer mentor is offline, the local cache may allow submissions that violate the new policy.

Mitigation & Contingency

Mitigation: Cache entries include a TTL (24 hours). On connectivity restore, refresh cache before allowing new submissions. Server-side validation in the Edge Function and save functions provides a second enforcement layer.

Contingency: If a stale-cache submission passes client validation but fails server validation, surface a clear error message explaining that the expense type rules have been updated and prompt the user to review their selection with the refreshed catalogue.