critical priority low complexity infrastructure pending infrastructure specialist Tier 2

Acceptance Criteria

Bucket named `bufdir-reports` exists in Supabase Storage and is set to private (not public)
Bucket is configured with a 5-year retention policy (1825 days) using Supabase Storage lifecycle rules or documented manual retention process if lifecycle rules are unavailable in the current Supabase tier
MIME type allowlist enforced: only application/pdf, text/csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet are accepted
Maximum file size is set to 52428800 bytes (50 MB) at the bucket level
RLS SELECT policy: authenticated users can only access files under the path prefix matching their organization_id JWT claim (e.g., `{orgId}/...`)
RLS INSERT policy: only coordinator and admin roles can upload to `{orgId}/{reportId}/` paths
RLS DELETE policy: only admin role can delete files
An authenticated user from org A cannot download a file stored under org B's path prefix
Attempting to upload a .docx or .png file returns a rejection error
Attempting to upload a 51 MB PDF returns a size limit error
Bucket configuration is fully documented in `docs/infrastructure/storage-buckets.md` including retention policy, MIME allowlist, size limits, and RLS policy descriptions

Technical Requirements

frameworks
Supabase Storage
apis
Supabase Storage API
Supabase Storage RLS policies
data models
bufdir_report_history (file_path references storage paths)
performance requirements
Signed URLs generated for downloads must have configurable expiry (default 60 minutes) to minimize re-request overhead
Upload paths must be deterministic and organization-scoped to enable efficient listing: `{orgId}/{reportId}/{filename}`
Bucket must support resumable uploads for files approaching the 50 MB limit
security requirements
Bucket must be private — no public URL access
File path structure must include organization_id as the first path segment to enable RLS path-prefix matching
Signed URLs must be short-lived (max 60 minutes) to prevent link sharing outside the application
MIME type validation must occur server-side (Supabase Storage level), not only client-side
Checksum (SHA-256) of uploaded files must be stored in bufdir_report_history.checksum for integrity verification

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Create the bucket via Supabase SQL migration or the Supabase CLI (`supabase storage create`) so configuration is version-controlled. The SQL approach uses `INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types) VALUES ('bufdir-reports', 'bufdir-reports', false, 52428800, ARRAY['application/pdf','text/csv','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'])`. Storage RLS policies in Supabase are written against the `storage.objects` table. Use `(storage.foldername(name))[1] = (auth.jwt() ->> 'organization_id')` to enforce org-scoped path prefixes.

Note that Supabase Storage retention/lifecycle policies may not be available on all tiers — if unavailable, document a manual deletion schedule and set a reminder. The 5-year retention aligns with Norwegian public record-keeping requirements for Bufdir grant documentation. Enforce organization-scoped paths in the ReportFileStorageClient (task-006) as a second layer of defense.

Testing Requirements

Write infrastructure integration tests: (1) Upload a valid PDF under org A's path as a coordinator — assert 200 response and file appears in storage; (2) Upload a .docx file — assert rejection with MIME error; (3) Upload a 51 MB file — assert rejection with size error; (4) Authenticate as org B user, attempt to download org A's file via direct path — assert 403; (5) Generate a signed URL for a file, wait for expiry, attempt to use it — assert 403 after expiry; (6) Attempt upload as peer_mentor role — assert permission denied. Use Supabase local dev environment for all tests. Document test results in the infrastructure notes file.

Component
Report File Storage Client
data low
Epic Risks (3)
high impact medium prob security

Incorrectly authored RLS policies could silently allow cross-organization data reads, exposing sensitive report history of one organization to coordinators of another in a multi-tenant environment.

Mitigation & Contingency

Mitigation: Write integration tests that explicitly authenticate as a user from organization A and assert zero rows are returned for organization B's history records. Use Supabase's built-in RLS testing utilities and review policies with a second developer.

Contingency: If a cross-tenant leak is discovered post-deployment, immediately revoke all active sessions for affected organizations, audit query logs for unauthorized access, and patch the RLS policy in a hotfix migration before re-enabling access.

medium impact medium prob technical

The 5-year retention policy for report files may conflict with Supabase Storage's lack of native lifecycle rules, requiring a custom pg_cron job that could fail silently and either delete files prematurely or never clean up.

Mitigation & Contingency

Mitigation: Implement the retention cleanup as a documented pg_cron job with explicit logging to a separate audit_jobs table. Add a Supabase Edge Function health check that verifies the cron job ran within the last 25 hours.

Contingency: If the cron job fails, files accumulate in storage (non-critical for compliance — over-retention is safer than under-retention). Alert the ops team via monitoring and manually trigger the cleanup function once the cron issue is resolved.

medium impact low prob integration

Signed URL generation depends on the requesting user's Supabase session being valid at the time of the call. If sessions expire during a long screen interaction, URL generation will fail with an authorization error and confuse the coordinator.

Mitigation & Contingency

Mitigation: Wrap signed URL generation in the service layer with a session-refresh check before calling Supabase Storage. Generate URLs on demand (tap-to-download) rather than pre-generating them for all list items on screen load.

Contingency: If a URL generation fails due to session expiry, surface a clear error message prompting the coordinator to re-authenticate, then automatically retry URL generation after session refresh completes.