critical priority medium complexity database pending database specialist Tier 1

Acceptance Criteria

RLS is enabled on all four tables: badge_definitions, earned_badges, recognition_tiers, tier_assignments
A volunteer (peer mentor role) can SELECT only earned_badges where organisation_id matches their JWT claim and user_id matches their own user ID
A coordinator can SELECT all earned_badges within their organisation_id claim
A coordinator can INSERT earned_badges and tier_assignments scoped to their organisation_id
A volunteer cannot INSERT, UPDATE, or DELETE records in badge_definitions or recognition_tiers
An org admin can INSERT, UPDATE earned_badges, badge_definitions, recognition_tiers, and tier_assignments within their organisation
A superadmin role (custom JWT claim) bypasses all RLS policies and can access all rows across all organisations
A volunteer from Organisation A cannot read any rows from Organisation B in any of the four tables — verified by test
UPDATE policy on earned_badges prevents changing the organisation_id or user_id columns
DELETE policy exists only for superadmin role; coordinators and volunteers cannot hard-delete badge records
All policies are named consistently following a convention (e.g., badge_definitions_select_coordinator)
Policy documentation comment is included in the migration script explaining each policy's intent

Technical Requirements

frameworks
Supabase
PostgreSQL RLS
apis
Supabase Auth JWT claims
PostgreSQL auth.uid()
PostgreSQL auth.jwt()
data models
BadgeDefinition
EarnedBadge
RecognitionTier
TierAssignment
performance requirements
RLS policies must use the organisation_id index — policies should filter on organisation_id = (auth.jwt() ->> 'organisation_id')::uuid to leverage existing indexes
Avoid per-row subqueries in policy expressions; use JWT claim extraction which is evaluated once per statement
security requirements
JWT claim 'organisation_id' must be the authoritative source for organisation scoping — never derive from user-submitted data
JWT claim 'role' determines permission level (volunteer, coordinator, org_admin, superadmin)
Superadmin bypass must use a dedicated custom claim (e.g., 'is_superadmin': true) not the role field alone
Policies must not expose data from other organisations even in error messages
RLS bypass (BYPASSRLS) must only be granted to the service_role, never to end-user roles

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use Supabase's `auth.jwt()` function to extract custom claims: `(auth.jwt() ->> 'organisation_id')::uuid` for organisation scoping and `auth.jwt() ->> 'role'` for role checks. Define policies in a separate migration file from the schema (e.g., `supabase migration new badge_rls_policies`) to keep schema and security concerns separated. For the superadmin bypass pattern, use `auth.jwt() ->> 'is_superadmin' = 'true'` as an OR clause in every policy. Document the expected JWT payload shape at the top of the migration file.

Be careful with `USING` vs `WITH CHECK`: `USING` controls which rows are visible for SELECT/UPDATE/DELETE, `WITH CHECK` controls which rows can be written. Both are needed for INSERT and UPDATE policies. Test with `set_config('request.jwt.claims', ...)` in pgTAP to simulate different user contexts.

Testing Requirements

Write Supabase RLS tests using the `supabase/tests` pgTAP framework or a dedicated test Dart file that exercises the Supabase client with different user JWT tokens. Required test scenarios: (1) volunteer from Org A can read own earned badges, (2) volunteer from Org A cannot read earned badges of another user in Org A, (3) volunteer from Org A gets empty result when querying Org B data, (4) coordinator can read all earned badges in their org, (5) coordinator INSERT succeeds for own org, fails for other org, (6) superadmin reads across all orgs successfully, (7) attempt to UPDATE organisation_id on earned_badge is blocked. Each scenario must have a passing assertion. Run these as part of CI against a local Supabase instance.

Component
Badge Repository
data low
Epic Risks (2)
high impact medium prob scope

Badge criteria are stored as structured JSON in badge_definitions. If the JSON schema for criteria (threshold counts, streak lengths, training completion flags) is not well-defined upfront, the evaluation service will be built against a moving target, requiring costly migrations and refactors.

Mitigation & Contingency

Mitigation: Define and document the criteria JSON schema in a shared type file before any repository code is written. Review the schema with all three organisations' badge requirements — especially Blindeforbundet's honorar thresholds — and version the JSON schema using a 'criteria_version' field from day one.

Contingency: If the criteria schema must change after services are built, write a Supabase migration to backfill existing rows and add a migration version column. Keep the evaluation service criteria parser isolated behind an interface so only one function needs updating.

medium impact medium prob dependency

Badge icon assets may not yet exist or may fail WCAG 2.2 AA contrast validation (minimum 3:1 for graphical objects) when rendered over design-token backgrounds. Missing or non-compliant icons could block UI epic delivery for Blindeforbundet, for whom screen reader and visual accessibility is non-negotiable.

Mitigation & Contingency

Mitigation: During this epic, implement the contrast-ratio validator in badge-icon-asset-manager and run it as a Flutter test against all candidate icon assets early. Coordinate with the design team to provide WCAG-compliant SVG icons in both locked and unlocked variants before the UI epic begins.

Contingency: If assets are late or fail contrast checks, ship placeholder icons that are guaranteed compliant (solid design-token colour fills with text labels) and swap in final assets post-QA without requiring a code change.