Design user_unit_assignments junction table
epic-organizational-hierarchy-management-foundation-task-002 — Create the user_unit_assignments junction table schema with columns for id, user_id, unit_id, is_primary, assigned_at, assigned_by, and revoked_at. Add unique constraints to enforce one primary assignment per user. Include foreign keys to auth.users and organization_units with appropriate cascade behavior.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Use a partial unique index rather than a plain unique index so that a user can have multiple non-primary (is_primary = false) assignments to the same or different units, while only ever having one active primary. The partial index predicate should be: WHERE is_primary = true AND revoked_at IS NULL. Do NOT enforce primary uniqueness via application logic alone — the database constraint is the authoritative guard. For cascade behavior: auth.users deletion should cascade to assignments (user is gone, assignments are irrelevant); organization_unit deletion should be RESTRICT to force explicit cleanup first (prevents accidental data loss).
Use timestamptz for all timestamps to ensure correct handling across Norwegian timezones (CET/CEST). The assigned_by column provides an audit trail required for Norwegian volunteer management compliance. When writing the RLS policy for coordinators, join against user_unit_assignments itself to identify which units the requesting user manages — avoid hardcoding role checks that bypass the hierarchy. Keep the migration file version-prefixed (e.g., 20260329_create_user_unit_assignments.sql) following the existing project migration naming convention.
Testing Requirements
Write Supabase integration tests using the supabase-js client in a test environment with a seeded database. Test scenarios: (1) Insert a valid assignment and confirm it is retrievable. (2) Attempt to insert a second active primary assignment for the same user and confirm the unique partial index raises a constraint violation. (3) Revoke an assignment (set revoked_at) and confirm a new primary assignment can then be created for that user.
(4) Attempt to delete a referenced organization_unit and confirm ON DELETE RESTRICT blocks the operation. (5) Confirm RLS: a user querying the table only sees their own rows. (6) Confirm RLS: a coordinator sees assignments for their managed units but not others. Cover both positive paths and constraint violation paths.
Integration tests should run against a local Supabase instance (supabase start).
Recursive CTE queries for large hierarchies (1,400+ nodes) may exceed Supabase query timeouts or produce unacceptably slow responses, degrading tree load time beyond the 1-second target.
Mitigation & Contingency
Mitigation: Implement Supabase RPC functions for subtree fetches rather than client-side recursive calls. Use materialized path or closure table as a supplemental index for depth-first traversal. Benchmark with realistic NHF data volumes during development.
Contingency: Fall back to a pre-computed flat unit list stored in the hierarchy cache with client-side tree reconstruction, trading freshness for speed. Add a background refresh job to keep the cache warm.
Concurrent writes from multiple admin sessions could cause cache staleness, leading to stale tree views and incorrect ancestor path computations that corrupt aggregation results.
Mitigation & Contingency
Mitigation: Use optimistic versioning on cache entries with a short TTL (5 minutes) as a safety net. Subscribe to Supabase Realtime on the organization_units table to push invalidation events to all connected clients.
Contingency: Provide a manual 'Refresh Hierarchy' action in the admin portal that forces a full cache bust, and display a staleness warning banner when the cache age exceeds the TTL.
Persisting the flat unit list to local storage may expose organization structure data if the device is compromised or the storage is not properly encrypted, violating data protection requirements.
Mitigation & Contingency
Mitigation: Use flutter_secure_storage (AES-256 backed by Keychain/Keystore) for the local unit list cache rather than SharedPreferences. Include only unit IDs, names, and types — no member PII.
Contingency: Disable local-storage persistence entirely and rely on in-memory cache only. Accept the trade-off of no offline hierarchy access for the security guarantee.