critical priority medium complexity integration pending integration specialist Tier 2

Acceptance Criteria

An uploadDeclaration(tamperEvidentBlob, orgId, declarationId) function exists and uploads the serialized blob to a Supabase Storage path of the form declarations/{orgId}/{declarationId}.enc
The upload uses the Supabase service-role client (server-side only) — never the anon/public client
Content-type metadata is set to application/octet-stream on the uploaded object
Custom metadata headers include: x-org-id, x-declaration-id, x-schema-version, x-uploaded-at (ISO-8601) for auditability
The bucket declarations is configured as private (not public) — no object is accessible without a signed URL
On successful upload, the function returns the full storage object path (e.g. declarations/{orgId}/{declarationId}.enc) as a string
On upload failure (network error, RLS rejection, duplicate path), the function throws a typed StorageUploadError with a code field distinguishing between network_error, rls_denied, and duplicate_path
RLS policy on the declarations bucket prevents any org from writing to another org's path prefix — verified by integration test attempting a cross-org upload with a scoped JWT
The adapter is implemented as a Supabase Edge Function (or called from within one) — no storage client credentials are present in the Flutter mobile app
Unit tests mock the Supabase Storage client and assert correct path construction, metadata headers, and error handling for each error code

Technical Requirements

frameworks
Supabase Edge Functions (Deno)
Supabase Storage SDK (server-side)
apis
Supabase Storage API
Supabase Edge Functions REST API
data models
confidentiality_declaration
performance requirements
Upload of a 50KB encrypted declaration blob must complete within 3 seconds on a standard Supabase region
Adapter must not load the entire blob into memory twice — stream the bytes directly to the storage SDK upload call
security requirements
Supabase service-role key used exclusively server-side in the Edge Function environment — never sent to or stored in the mobile app
Storage bucket declarations set to private; RLS policy: INSERT allowed only when auth.jwt()->'org_id' matches the {orgId} path segment
Object paths must use declarationId (UUID) as filename — never user-provided strings to prevent path traversal
Custom metadata values must be sanitized before inclusion to prevent header injection
Encrypted filenames: declarationId.enc suffix signals encrypted content to any downstream tooling

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use the @supabase/storage-js client within the Deno Edge Function, initializing with the service-role key from environment. Construct the storage path as `declarations/${orgId}/${declarationId}.enc` — validate that both orgId and declarationId match UUID regex before constructing the path to prevent injection. Use the upsert: false option to prevent silently overwriting an existing declaration (throw duplicate_path error instead). Set cacheControl: '0' to prevent any CDN caching of encrypted blobs.

The Flutter Dart layer calls this adapter via an authenticated POST to the Edge Function endpoint, passing the serialized TamperEvidentBlob as a binary request body — it never calls Supabase Storage directly. Log the upload event (org_id, declaration_id, uploaded_at, object_path) to the bufdir_export_audit_log or a dedicated upload_audit_log table for traceability.

Testing Requirements

Unit tests (Deno test) with a mocked Supabase Storage client: assert correct path construction for a given orgId + declarationId, assert content-type and custom metadata are set, assert StorageUploadError with code=rls_denied is thrown when the mock returns a 403, assert StorageUploadError with code=duplicate_path is thrown on a 409. Integration test against a Supabase staging project: upload a real encrypted blob, assert the returned path exists, assert the object is not publicly downloadable (GET without signed URL returns 400/403), assert a cross-org upload attempt (different org JWT) is rejected with RLS error.

Component
Declaration Supabase Storage Adapter
data medium
Epic Risks (4)
high impact medium prob security

Org-scoped encryption key management is complex. If keys are not correctly isolated per organization, a breach in one org's key could expose another org's declarations. Additionally, key rotation is not specified but may be needed for compliance, and the current implementation may not support it.

Mitigation & Contingency

Mitigation: Use Supabase Vault or a dedicated secrets management approach for org-scoped key storage. Define the key derivation strategy (per-org master key) in a security design document reviewed before implementation begins. Include key isolation tests in the test suite.

Contingency: If a full per-org key management system cannot be safely implemented within the sprint, fall back to a single platform-level encryption key with strict RLS isolation as a temporary measure, flagging the key rotation gap as a security debt item with a defined resolution milestone.

medium impact medium prob integration

Push notification delivery to drivers depends on FCM token availability and device connectivity. If a driver has not granted notification permissions or has an expired FCM token, the declaration delivery notification will silently fail, leaving the coordinator unaware and the declaration unacknowledged.

Mitigation & Contingency

Mitigation: Implement delivery status tracking in declaration-notification-service. Fall back to in-app notification and SMS (if configured) when push delivery fails. Expose delivery failure status in the declaration status badge so coordinators can identify and manually follow up.

Contingency: If push delivery proves unreliable, implement a polling-based in-app notification fallback where drivers see pending declarations on next app open, ensuring the workflow can complete even without push notifications.

medium impact medium prob technical

The acknowledgement service is meant to validate that the driver has fully scrolled through the declaration before confirming. Implementing reliable scroll completion detection in Flutter across different screen sizes and font sizes is technically non-trivial and could be bypassed.

Mitigation & Contingency

Mitigation: Implement scroll position tracking using ScrollController with a threshold (e.g., 95% of content height reached) and record the validated state server-side before allowing acknowledgement submission. Document the approach in the legal sign-off checkpoint noted in the feature documentation.

Contingency: If reliable scroll detection cannot be implemented within the sprint, add a mandatory reading delay timer (e.g., estimated reading time based on word count) as an alternative validation mechanism, pending legal review of the approach.

medium impact low prob dependency

The driver assignment service must coordinate with the threshold-based expense approval workflow for fees above configured thresholds. If the expense approval workflow interface changes or is not yet stable, the integration point could break or produce incorrect routing behavior.

Mitigation & Contingency

Mitigation: Define a clear interface contract between driver-assignment-service and the expense approval workflow before implementation. Use dependency injection so the expense workflow client can be mocked in tests. Monitor the expense approval feature for interface changes.

Contingency: If the expense approval workflow interface is not stable, implement a direct database insert to the expense records table as a temporary bypass, with a flag indicating manual review is needed, until the stable interface is available.