critical priority medium complexity database pending database specialist Tier 1

Acceptance Criteria

RLS is enabled on bufdir_export_audit_log (ALTER TABLE ... ENABLE ROW LEVEL SECURITY)
INSERT policy: authenticated users may insert rows only where NEW.org_id equals the org_id claim from their JWT (auth.jwt()->'app_metadata'->>'org_id' or equivalent claim path used by the project)
SELECT policy: authenticated users may select only rows where org_id matches their JWT org_id claim — cross-organisation reads return empty result set, not an error
No UPDATE RLS policy exists (relying on the immutability trigger from task-001 for full protection)
No DELETE RLS policy exists (relying on the immutability trigger from task-001 for full protection)
Smoke test: User A (org_id = 'org-nhf') inserts a row and can SELECT it back; User B (org_id = 'org-hlf') cannot SELECT User A's row
Smoke test: User A attempting UPDATE on their own row receives a PostgreSQL exception from the trigger (not silently ignored)
Smoke test: service_role client attempting UPDATE also receives trigger exception (confirming trigger-based immutability is not bypassable via service_role)
RLS policies are created in a migration file that applies after the table creation migration (task-001)
FORCE ROW LEVEL SECURITY is applied so that table owners are also subject to RLS in non-privileged sessions

Technical Requirements

frameworks
Supabase RLS (PostgreSQL Row Level Security)
PostgreSQL 15+
apis
supabase CLI
Supabase Auth JWT claims (app_metadata.org_id or custom claim)
data models
bufdir_export_audit_log
auth.users (Supabase Auth)
performance requirements
RLS policy expressions must be simple equality checks on indexed columns — no subqueries in policy expressions that could cause sequential scans
The (org_id, created_at DESC) index created in task-001 must be leveraged by SELECT queries filtered by the RLS policy
security requirements
Confirm the correct JWT claim path for org_id with the auth setup — using the wrong claim path silently grants access to all rows
Use USING clause for SELECT policy and WITH CHECK clause for INSERT policy — do not conflate them
FORCE ROW LEVEL SECURITY ensures even the table owner role (postgres) is subject to RLS when connecting as a non-superuser
Service_role bypasses RLS by default in Supabase — immutability must be enforced by triggers (already done in task-001), not RLS

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

The critical risk here is using the wrong JWT claim path for org_id — verify with the rest of the codebase how org_id is embedded in the JWT (check existing RLS policies on other tables for the established pattern). Common patterns in Supabase: auth.jwt()->'app_metadata'->>'org_id', or a custom claim added via a Supabase Auth hook. Match the existing convention exactly. The INSERT WITH CHECK policy prevents a malicious client from writing audit records for another organisation — this is important because audit records affect billing and compliance for each organisation.

Do not add an UPDATE policy even as a 'deny all' policy — absence of an UPDATE policy combined with the trigger is cleaner and avoids any risk of a permissive policy accidentally overriding. Apply FORCE ROW LEVEL SECURITY in the same migration file as the policies. If the project uses Supabase Edge Functions for the orchestrator backend, ensure those functions use the user's JWT (not service_role) when inserting audit records, so RLS is exercised in the normal code path.

Testing Requirements

Write a SQL-level integration test or a Dart integration test using two separate Supabase clients authenticated with different org JWTs. Test cases: (1) User A inserts a row with org_id='org-a' — succeeds; (2) User A selects all rows — returns exactly their own row; (3) User B selects all rows — returns empty set; (4) User A attempts INSERT with org_id='org-b' (mismatched claim) — rejected by WITH CHECK policy; (5) User A attempts UPDATE — rejected by trigger; (6) service_role client attempts UPDATE — rejected by trigger. Include these as named test cases in a test file under test/database/ or equivalent. All tests must pass against local Supabase instance in CI.

Component
Export Audit Log Repository
data low
Epic Risks (2)
high impact medium prob security

RLS policies for the audit log and schema config tables must correctly handle multi-chapter membership hierarchies (up to 1,400 local chapters for NHF). Incorrect policies could either over-expose data across organisations or prevent legitimate coordinator access, both of which are serious compliance failures.

Mitigation & Contingency

Mitigation: Design RLS policies using the existing org hierarchy resolver pattern. Write integration tests that verify cross-organisation isolation with representative fixture data covering NHF's multi-level hierarchy before merging.

Contingency: If RLS policies prove too complex to express safely in Postgres, implement a Supabase Edge Function as a data access proxy that enforces isolation in application code, with RLS serving as a secondary defence layer.

medium impact medium prob scope

Bufdir's column schema is expected to evolve as Norse Digital Products negotiates a simplified digital reporting format. If the schema config versioning model is too rigid, applying Bufdir schema updates without a code deployment could fail, forcing emergency releases.

Mitigation & Contingency

Mitigation: Design the schema config table to store the full JSON column mapping as a JSONB field with a version number. Provide an admin API to upsert new versions without any schema migration required.

Contingency: If the versioning model is insufficient for a Bufdir schema change, fall back to a code deployment with the updated default schema, using the database config only for org-specific overrides.