critical priority low complexity infrastructure pending infrastructure specialist Tier 0

Acceptance Criteria

A `bufdir-exports` bucket exists in Supabase Storage and is set to private (not public)
Storage RLS policy allows SELECT (download) only when the first path segment matches the user's org_id from user_roles: `(storage.foldername(name))[1] = (SELECT org_id::text FROM user_roles WHERE user_id = auth.uid())`
Storage RLS policy allows INSERT only for the service role — no user-facing upload policy exists (uploads happen exclusively via edge function with service role key)
Storage RLS policy allows DELETE only for the service role
Signed URLs generated with a 7-day expiry (604800 seconds) provide time-limited access for re-download without requiring the user to be authenticated at download time
The bucket and all RLS policies are defined in a migration file at `supabase/migrations/{timestamp}_create_bufdir_exports_bucket.sql` that applies cleanly from scratch
Path convention `{org_id}/{report_id}.{csv|pdf}` is documented in a `supabase/storage/BUFDIR_EXPORTS_BUCKET.md` file
A security review checklist is completed and signed off confirming: bucket is private, cross-org access is blocked, service-role-only writes, signed URL TTL is 7 days
Attempting to access a file from a different org's folder with a valid JWT returns a 403 — verified manually

Technical Requirements

frameworks
Supabase CLI (supabase storage)
PostgreSQL RLS (storage.objects table policies)
apis
Supabase Storage API (bucket management, signed URL generation)
data models
storage.objects table (Supabase internal)
user_roles table (referenced in RLS policy for org_id lookup)
performance requirements
Signed URL generation must complete within 500ms
Storage bucket must support files up to 50MB (large org CSV/PDF exports)
security requirements
Bucket must be private — no anonymous or public read access
All user downloads must go through signed URLs, never direct object URLs
Service role key used for writes must never be exposed to Flutter client code
RLS policy must use the server-side org_id lookup via user_roles — do not rely on client-supplied org_id claims
Signed URL TTL of 7 days is the maximum — do not issue non-expiring URLs

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

In Supabase, storage RLS policies are written against the `storage.objects` table. The `name` column contains the full path including bucket name in some versions — use `storage.foldername(name)` helper to extract path segments reliably. The `(storage.foldername(name))[1]` expression extracts the first folder (org_id in the path convention). The user_roles lookup in the RLS policy will execute once per row evaluation — ensure the user_roles table has an index on `user_id` (it likely already exists from auth setup).

Do NOT make the bucket public even for signed URLs — signed URLs work on private buckets via Supabase's signed URL mechanism which bypasses RLS using a time-limited token. Define the 7-day TTL as a constant in the migration SQL comment and reference it in the Flutter adapter (task-004) to keep them in sync. If the Supabase project uses a custom schema for user_roles (not public), adjust the RLS policy schema prefix accordingly.

Testing Requirements

Manual verification steps documented as a checklist in the PR: (1) Upload a test file as service role to `{test_org_id}/{test_report_id}.csv`, (2) Authenticate as a user from test_org and verify GET succeeds, (3) Authenticate as a user from a different org and verify GET returns 403, (4) Attempt INSERT as authenticated user (not service role) and verify 403, (5) Generate a signed URL and verify it works without auth headers, (6) Verify the signed URL is rejected after manual expiry (or use a short test TTL). No automated flutter_test coverage required for this infrastructure task — manual checklist is the acceptance gate.

Component
Export Storage Bucket
infrastructure low
Epic Risks (3)
high impact medium prob technical

NHF's three-level hierarchy (national / region / chapter) with 1,400 chapters may have edge cases such as chapters belonging to multiple regions, orphaned nodes, or missing parent links in the database. Incorrect scope expansion would silently under- or over-report activities, which could invalidate a Bufdir submission.

Mitigation & Contingency

Mitigation: Obtain a full hierarchy fixture export from NHF before implementation begins. Write exhaustive unit tests covering boundary cases: single chapter, full national roll-up, chapters with no activities, and chapters assigned to multiple regions. Validate resolver output against a known-good manual count.

Contingency: If hierarchy data quality is too poor for automated resolution at launch, implement a manual scope override in the coordinator UI that allows the coordinator to explicitly select org units from a tree picker, bypassing the resolver.

medium impact high prob dependency

The activity_type_configuration table may not cover all activity types currently in use, leaving a subset unmapped at launch. Bufdir submissions with unmapped categories will be incomplete and may be rejected by Bufdir.

Mitigation & Contingency

Mitigation: Run a query against production activity data before implementation to enumerate all distinct activity type IDs. Cross-reference with Bufdir's published category schema (request from Norse Digital Products). Flag every gap as a known issue and build the warning surface into the preview panel.

Contingency: Implement a fallback 'Other' category bucket for unmapped types and surface a prominent warning in the export preview requiring coordinator acknowledgement before proceeding. Log unmapped types for post-launch cleanup.

high impact low prob security

Supabase RLS policies on generated_reports and the storage bucket must enforce strict org isolation. A misconfigured policy could allow a coordinator from one organisation to read another organisation's export files, creating a serious data breach with GDPR implications.

Mitigation & Contingency

Mitigation: Write RLS integration tests that attempt cross-org reads with explicitly different JWT tokens and assert that all attempts return empty sets or 403 errors. Include RLS policy review in the pull request checklist. Use Supabase's built-in policy tester during development.

Contingency: If a policy gap is discovered post-deployment, immediately revoke all signed URLs for affected exports, audit the access log for unauthorised reads, and issue a coordinated disclosure to affected organisations per GDPR breach notification requirements.