critical priority medium complexity infrastructure pending infrastructure specialist Tier 0

Acceptance Criteria

accounting_credentials table exists in Supabase with columns: credential_id, org_id, target_system, encrypted_payload (TEXT), created_at, updated_at — encrypted using pgsodium (Supabase Vault) or pgp_sym_encrypt
RLS on accounting_credentials denies ALL access from the anon and authenticated roles — only the service role (Edge Functions) can read rows
Supabase Edge Function store-accounting-credentials accepts POST with {orgId, targetSystem, credentials: {apiKey, clientId, clientSecret}} from an authenticated coordinator, validates the coordinator role, encrypts credentials, and upserts the row
Supabase Edge Function get-accounting-credentials (internal-only, not exposed to Flutter) returns decrypted credentials for a given orgId+targetSystem — callable only by other Edge Functions via service role key
Flutter AccountingCredentialsVault class exposes only submitCredentials(orgId, targetSystem, rawCredentials) which calls the store Edge Function — it never reads back credentials
A has-credentials-configured(orgId, targetSystem) check returns a boolean without exposing credential content — the coordinator UI uses this to show 'configured'/'not configured' status
Submitting new credentials for an already-configured org+system upserts (overwrites) the existing entry
Edge Function returns HTTP 403 if the calling user is not a coordinator or org_admin for the specified orgId
Credential payload is never logged, never included in error responses, and never written to Supabase logs

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Edge Functions (Deno/TypeScript)
pgsodium or Supabase Vault
apis
Supabase Edge Functions (invoke)
Supabase Vault / pgsodium for encryption
Xledger API (credentials will authenticate to this)
Microsoft Dynamics API (credentials will authenticate to this)
data models
accounting_credentials
AccountingCredentialStatus (Flutter model — no secrets, just boolean flags)
performance requirements
Credential retrieval within Edge Functions must complete in under 200ms — use connection pooling (Supabase pg pool) not cold connections
Credential setup is a rare operation (once per org per system) — latency is acceptable up to 3s
security requirements
CRITICAL: Flutter client must NEVER receive decrypted credentials under any code path
Use Supabase Vault (vault.secrets) or pgsodium.crypto_secretbox for encryption at rest — do not implement custom encryption
Edge Function must verify JWT from Flutter request using Supabase's built-in JWT verification before processing
Credentials must be transmitted over HTTPS only (enforced by Supabase Edge Function transport)
Implement rate limiting on the store endpoint to prevent credential brute-force or enumeration: max 10 requests per minute per org
Audit log: write a row to a credential_audit_log table on every store/update event with timestamp and coordinator user_id (no credential content)
ui components
CredentialSetupForm widget — text fields for apiKey/clientId/clientSecret with obscured input, submit button, and success/error state
CredentialStatusBadge widget — shows 'Configured' (green) or 'Not Configured' (amber) based on hasCredentialsConfigured result

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Place Flutter code in lib/features/accounting/infrastructure/accounting_credentials_vault.dart and Edge Functions in supabase/functions/store-accounting-credentials/ and supabase/functions/get-accounting-credentials/. Use Supabase Vault (vault.create_secret / vault.read_secret) if available on the project's Supabase plan — it provides key rotation and audit logging out of the box. If Vault is not available, use pgsodium with a key stored in Supabase secrets (not hardcoded). The get-accounting-credentials Edge Function should only be invokable internally (via service role key from other Edge Functions, not from Flutter) — enforce this by checking that the Authorization header is the service role key, not a user JWT.

Coordinate with Xledger and Dynamics to understand the credential format (API key only vs. OAuth client_id/secret) before finalizing the encrypted_payload schema.

Testing Requirements

Unit tests (Flutter): Test submitCredentials invokes the correct Edge Function with correct payload shape. Test that the Flutter class has no method that returns credential values. Test CredentialStatusBadge renders 'Configured' when hasCredentials = true and 'Not Configured' when false. Edge Function tests (Deno): Test store-accounting-credentials with valid coordinator JWT — verify upsert is called.

Test with non-coordinator JWT — verify 403 response. Test with missing fields in body — verify 400 response with message. Test that credential_payload does not appear in any response body. Integration test: Call store then verify hasCredentialsConfigured returns true for same org+system and false for a different org.

Component
Accounting Credentials Vault
infrastructure medium
Epic Risks (3)
high impact medium prob technical

Adding exported_at and export_run_id columns to expense_claims requires a live migration on a table shared with the approval workflow. A poorly timed migration could lock the table and block claim submissions or approvals.

Mitigation & Contingency

Mitigation: Use non-blocking ADD COLUMN with a DEFAULT of NULL (no backfill needed) executed during a low-traffic window. Test migration rollback on a staging replica before production deployment.

Contingency: If migration causes table lock contention, roll back and reschedule for a maintenance window. Use a feature flag to gate the export UI until the migration completes successfully.

medium impact high prob scope

Chart of accounts mapping configurations for Xledger and Dynamics may not be fully specified by stakeholders at development time, leaving the mapper with incomplete data and causing validation failures for unmapped expense categories.

Mitigation & Contingency

Mitigation: Implement the mapper to return a structured validation error (not a crash) for any unmapped field, and surface these errors clearly in the export confirmation dialog. Request full mapping tables from Blindeforbundet and HLF stakeholders as a pre-condition for this epic.

Contingency: If mappings arrive incomplete, ship the mapper with the available subset and mark unmapped categories as excluded (skipped with reason). Coordinators see which categories are skipped and can manually submit those records.

medium impact medium prob dependency

Supabase Vault configuration for storing per-org accounting credentials may require infra permissions or environment secrets not yet provisioned in staging or production, blocking development and testing of credential retrieval.

Mitigation & Contingency

Mitigation: Provision Vault secrets and environment configuration in staging as the first task of this epic. Document the exact secret naming convention and rotation procedure before implementation begins.

Contingency: If Vault is unavailable, use environment variables scoped to the Edge Function as a temporary fallback for development. Block production deployment until Vault-based storage is confirmed operational.