critical priority medium complexity database pending database specialist Tier 0

Acceptance Criteria

Migration file is named with a timestamp prefix following Supabase migration naming convention (e.g., 20250226120000_create_activity_types.sql)
Table activity_types is created with column id (UUID, primary key, default gen_random_uuid())
Column org_id (UUID, NOT NULL) is present and references the organizations table with ON DELETE CASCADE
Column name (TEXT, NOT NULL) is present with a CHECK constraint ensuring name is not empty string
Column is_active (BOOLEAN, NOT NULL, DEFAULT true) is present
Column metadata (JSONB, NOT NULL, DEFAULT '{}') is present
Column created_at (TIMESTAMPTZ, NOT NULL, DEFAULT now()) is present
Column updated_at (TIMESTAMPTZ, NOT NULL, DEFAULT now()) is present
Index idx_activity_types_org_id on (org_id) is created
Index idx_activity_types_org_id_is_active on (org_id, is_active) is created for the common filtered query pattern
A trigger or Supabase function updates updated_at automatically on row update
Migration applies cleanly on a fresh Supabase project with supabase db reset
Migration is idempotent — running it twice does not cause an error (use CREATE TABLE IF NOT EXISTS or check migration system state)

Technical Requirements

frameworks
Supabase
apis
Supabase Database API
data models
ActivityType
Organization
performance requirements
The composite index on (org_id, is_active) must be used by the primary list query as verified by EXPLAIN ANALYZE
Table creation migration must execute in under 5 seconds on a standard Supabase project
security requirements
Table must be created in the public schema with RLS enabled by default (ALTER TABLE activity_types ENABLE ROW LEVEL SECURITY) — RLS policies are added in the next task but enabling must be part of this migration
Default privileges must not grant public SELECT access — RLS will control access

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use gen_random_uuid() for the id default — do not use uuid_generate_v4() as it requires the uuid-ossp extension which may not always be enabled. For the updated_at trigger, create a reusable moddatetime trigger function (CREATE OR REPLACE FUNCTION handle_updated_at()) if one does not already exist in the project's shared migrations, then attach it with CREATE TRIGGER set_updated_at BEFORE UPDATE ON activity_types. Store the metadata JSONB schema definition in a comment block in the migration file to document expected shape (triggersReimbursementWorkflow: bool, isTravelExpenseEligible: bool, labelKey: string). Place the migration in the supabase/migrations/ directory.

Coordinate with the team to ensure the organizations table migration has already been applied, as the foreign key depends on it.

Testing Requirements

Verify migration with supabase db reset followed by supabase db push in a local Supabase environment. Write a SQL test script that: inserts a row with all required columns, queries by org_id and is_active, verifies updated_at changes on UPDATE, and verifies ON DELETE CASCADE removes activity_types when the parent org is deleted. Confirm indexes exist using SELECT * FROM pg_indexes WHERE tablename = 'activity_types'. Verify EXPLAIN ANALYZE uses the composite index for WHERE org_id = $1 AND is_active = true queries.

Component
Supabase Client
infrastructure low
Epic Risks (3)
high impact medium prob technical

The JSONB metadata column has no enforced schema at the database level. If the Dart model and the stored JSON diverge (e.g., a field is renamed or a new required flag is added without a migration), the metadata resolver will silently return null or throw at parse time, breaking conditional wizard logic for all organisations.

Mitigation & Contingency

Mitigation: Define a versioned Dart Freezed model for ActivityTypeMetadata and add a Supabase check constraint or trigger that validates the JSONB structure on write. Document the canonical metadata schema in a shared constants file and require schema review for any metadata field additions.

Contingency: Implement a lenient parse path in ActivityTypeMetadataResolver that returns safe defaults for missing fields and logs a structured warning to Supabase edge logs, allowing the app to degrade gracefully rather than crash.

high impact low prob security

If RLS policies on the activity_types table are misconfigured, a coordinator from one organisation could read or mutate activity types belonging to another organisation, violating data isolation guarantees required by all three client organisations.

Mitigation & Contingency

Mitigation: Write integration tests against the Supabase local emulator that explicitly assert cross-org isolation: a token scoped to org A must receive zero rows when querying org B activity types, and upsert attempts must return permission-denied errors.

Contingency: Apply an emergency RLS policy patch via Supabase dashboard without a code deploy. Audit all activity_type rows for cross-org contamination and restore from backup if any data leakage is confirmed.

medium impact medium prob integration

If the cache invalidation call in ActivityTypeService is not reliably triggered after an admin creates, edits, or archives an activity type, peer mentors on the same device will see stale data in the registration wizard until the next app restart, leading to confusion and potential misregistrations.

Mitigation & Contingency

Mitigation: Enforce a strict pattern: ActivityTypeService always calls cacheProvider.invalidate() inside the same try block as the successful Supabase mutation, before returning to the caller. Write a widget test that verifies the cache notifier emits an updated list after a service mutation.

Contingency: Add a background Supabase Realtime subscription on the activity_types table that triggers cache invalidation automatically, providing an independent safety net independent of the service call path.