medium priority medium complexity infrastructure pending infrastructure specialist Tier 2

Acceptance Criteria

Bufdir API credentials (API key or OAuth client credentials) are stored in a dedicated integration_credentials table in Supabase with columns: org_id, integration_type ('bufdir'), credential_payload (encrypted jsonb), rotated_at, created_by
credential_payload is encrypted at rest using Supabase Vault (pgsodium) or equivalent — never stored as plaintext
RLS policy on integration_credentials restricts reads to service role only — no mobile client can read credentials regardless of JWT
Supabase Edge Function `functions/bufdir-submit` reads credentials from the vault at invocation time (not cached longer than the function lifetime)
Credential rotation is achieved by inserting a new row with updated credential_payload and rotated_at timestamp — no service restart required
Edge Function selects the most recently rotated credential row for the given org_id on each invocation
Org-level isolation is enforced: credential lookup is scoped by org_id extracted from JWT claims, never from client-supplied parameters
No Bufdir credentials appear in: source code, environment variable files committed to git, Flutter app bundle, application logs, or error messages surfaced to users
A rotation procedure document (markdown) is committed to the repository under `docs/operations/bufdir-credential-rotation.md` covering: when to rotate, how to insert new credentials, how to verify the new credentials are active, and rollback steps
Automated test verifies that calling the Edge Function with an org_id that has no credentials returns a structured error (not a 500 crash)

Technical Requirements

frameworks
Dart
Flutter
Supabase Edge Functions (Deno)
apis
Supabase PostgreSQL 15
Supabase Edge Functions (Deno)
Bufdir Reporting API
data models
bufdir_export_audit_log
performance requirements
Credential lookup from vault must add no more than 20ms to Edge Function cold start
Vault decryption must complete within 5ms for a single credential payload
security requirements
Supabase Vault (pgsodium) used for credential encryption — never roll custom encryption
Service role key available only in Edge Function environment variables, never in Flutter app
Credential_payload must include an expiry field; Edge Function warns (logs) if credentials are within 14 days of expiry
All credential access (read, rotate) logged to bufdir_export_audit_log with actor_id and timestamp
Old credential rows retained for 90 days for audit purposes before deletion
GDPR: Bufdir credentials are classified as org-level operational secrets, not personal data — no DPA required for the credentials themselves, but handling must meet organisational security policy
Rotation procedure must include revoking the old credential with Bufdir before deleting the row

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Integration Task

Handles integration between different epics or system components. Requires coordination across multiple development streams.

Implementation Notes

Create the integration_credentials table via a Supabase migration using `vault.create_secret()` or store the encrypted payload in a jsonb column protected by pgsodium `vault.secrets`. The Edge Function pattern: `const cred = await supabase.from('integration_credentials').select('credential_payload').eq('org_id', orgId).eq('integration_type', 'bufdir').order('rotated_at', ascending: false).limit(1).single()` — then decrypt using `vault.decrypted_secrets` view or pgsodium `crypto_secretbox_open`. For the rotation procedure document, include a SQL snippet for inserting new credentials and a verification curl command against the Bufdir sandbox endpoint. Pin the Deno Edge Function runtime version in `deno.json` to prevent unexpected upgrades that could affect credential decryption behaviour.

The vault key management key (KMS key) must be documented in the ops runbook and stored outside Supabase.

Testing Requirements

Infrastructure/integration tests: (1) Edge Function with valid credentials for org_id returns success, (2) Edge Function with no credential row for org_id returns structured BufdirCredentialNotFoundError, (3) Edge Function with expired credential logs a warning and still attempts submission (credentials may still work after expiry window), (4) RLS test verifies a mobile client JWT cannot SELECT from integration_credentials. Security review checklist: grep codebase for any hardcoded credential strings, verify no credentials in Flutter asset bundle, verify Supabase Dashboard shows Vault encryption active. No flutter_test unit tests needed for this infrastructure task.

Component
Bufdir API Client
service high
Epic Risks (2)
medium impact high prob dependency

Norse Digital Products has not yet completed API negotiations with Bufdir. If negotiations stall or Bufdir's API design diverges significantly from expectations, the API client may need substantial rework, or the epic may be blocked indefinitely.

Mitigation & Contingency

Mitigation: Implement the client against a locally defined stub of the expected Bufdir API schema. Isolate all Bufdir-specific schema mapping in a single adapter class so that changes to the actual API schema require changes in only one place. Keep the epic in 'interface-ready' status until real API credentials are available for integration testing.

Contingency: If API negotiations are not completed within the planned window, defer this epic without impact on any other epic — the PDF/CSV fallback path from Epics 1–4 delivers full standalone value. Mark the epic as blocked and resurface when negotiations conclude.

high impact low prob security

Bufdir API credentials stored in the application or edge function environment could be exposed through misconfigured secrets management, log leakage, or a compromised deployment pipeline, allowing unauthorised Bufdir submissions on behalf of the organisation.

Mitigation & Contingency

Mitigation: Store all Bufdir API credentials exclusively in Supabase Vault (or the integration credential vault component), never in client-side code or environment variables accessible to the Flutter app. Transmit credentials only from within the edge function, not from the Flutter client. Implement credential rotation support from the outset.

Contingency: If a credential leak is detected, immediately revoke and rotate the affected API credentials through Bufdir's credential management portal, audit submission logs for any unauthorised calls, and notify Bufdir's technical contact per the API agreement's security incident clause.