Date: February 17, 2026
Status: RESOLVED — Gamification is fully tenant-scoped (migration 20260217030000)
The gamification system is fully tenant-scoped. All 7 data tables now have tenant_id NOT NULL, and the 2 definition tables (gamification_achievements, gamification_store_items) have optional tenant_id for school-specific customization. Features are gated by plan tier to create an upsell path.
What this means:
- A student in "Code Academy Pro" and "Default School" has separate XP, levels, streaks, and coin balances
- Leaderboards only show students from the current school
- Achievements are tracked independently per tenant
- Advanced features (leaderboard, achievements, store) are locked behind plan tiers
| Feature | DB | UI | Logic | Tenant-Aware | Plan Gate |
|---|---|---|---|---|---|
| XP Awards (6 triggers) | Yes | Yes | Yes | YES | All plans |
| 20 Levels (auto-progression) | Yes | Yes | Yes | Global (OK) | All plans |
| Streaks (daily activity) | Yes | Yes | Yes | YES | All plans |
| 21 Achievements (5 tiers) | Yes | Yes | Yes | YES | Basic+ |
| Coins + Store (6 items) | Yes | Yes | Yes | YES | Professional+ |
| Leaderboard (mini view) | Yes | Yes | Yes | YES | Basic+ |
| Challenges | Schema only | No | No | YES | Professional+ |
| Power-Up Rewards | Schema only | No | Partial | YES | Professional+ |
- XP triggers fire automatically on lesson completion (100 XP), exam submission (200+ XP), exercise (50 XP), comments (10 XP), reviews (50 XP)
- Level progression through 20 levels (Newcomer → Transcendent) with icons/titles
- Streak tracking with freeze power-ups purchasable from store
- Achievement system with lazy checking via edge function, 21 pre-seeded achievements across 5 categories
- Store with 6 items, purchase validation, max limits, coin balance management
- Gamification header in student dashboard shows level, XP bar, streak, coins
- Challenges — table exists but no data, rules, or UI
- Power-up UI — rewards are tracked in DB but no interface to view/use active buffs
- Full leaderboard page — only mini top-5 view exists
- Course-specific leaderboard — backend supports it, no frontend
- Level perks — JSONB field reserved but unused
| Table | tenant_id | Constraint | Status |
|---|---|---|---|
gamification_profiles |
NOT NULL |
UNIQUE(user_id, tenant_id) |
FIXED |
gamification_xp_transactions |
NOT NULL |
Indexed | FIXED |
gamification_leaderboard_cache |
NOT NULL |
UNIQUE(user_id, tenant_id) |
FIXED |
gamification_user_achievements |
NOT NULL |
UNIQUE(user_id, achievement_id, tenant_id) |
FIXED |
gamification_redemptions |
NOT NULL |
Indexed | FIXED |
gamification_user_rewards |
NOT NULL |
Indexed | FIXED |
gamification_challenge_participants |
NOT NULL |
Indexed | FIXED |
gamification_store_items |
NULLABLE |
NULL = global, UUID = school-specific | FIXED |
gamification_achievements |
NULLABLE |
NULL = global, UUID = school-specific | FIXED |
gamification_levels |
None | Global definitions (intentional) | OK |
| Table | Risk | Note |
|---|---|---|
exam_questions |
HIGH | Relies on exam→course→tenant join chain |
question_options |
HIGH | Same — no direct tenant filter |
comments |
Medium | Join-based isolation only |
reviews |
Medium | Join-based isolation only |
notifications |
Medium | No direct tenant scope |
All gamification RLS policies now use get_tenant_id() for tenant filtering:
- Data tables:
USING (tenant_id = get_tenant_id())for reads,WITH CHECK (tenant_id = get_tenant_id())for writes - Definition tables (achievements, store items):
USING (tenant_id IS NULL OR tenant_id = get_tenant_id())to show global + school-specific items - Leaderboard cache: tenant-scoped public read
For School Owners (your paying customers):
-
School Identity & Branding — Each school wants THEIR leaderboard, THEIR achievements, THEIR community. A school with 50 students doesn't want to compete against a school with 5,000.
-
Custom Store Items — Schools should sell their own power-ups, merchandise credits, or special access. A coding school might sell "Code Review Token" while a language school sells "Conversation Session."
-
School-Specific Achievements — "Complete all React courses" only makes sense in a coding school. Achievement definitions should be customizable per tenant.
-
Competitive Advantage — Schools can market "Our gamified learning platform" as a unique selling point. Global gamification dilutes this.
-
Revenue Opportunity — Premium gamification features (custom achievements, branded store, challenge creation) become upsell opportunities for higher-tier plans.
| Plan | Gamification Features |
|---|---|
| Free | XP + levels + streaks (basic engagement) |
| Basic | + School leaderboard, achievements |
| Professional | + Store, custom achievements, custom store items |
| Enterprise | + All features, analytics, white-label |
- Schools with gamification see 2-3x higher student retention (Duolingo model)
- Custom gamification is a top-3 requested feature in EdTech SaaS
- Could justify $20-50/month premium per school for advanced gamification
Phase 1 — Core Isolation:
- Added
tenant_id NOT NULLto 7 data tables (nullable first, backfilled with default tenant, then NOT NULL) - Updated unique constraints to composite keys:
(user_id, tenant_id)etc. - Dropped old RLS policies, created tenant-scoped replacements using
get_tenant_id()
Phase 2 — Functions & Edge Functions:
award_xp()— New_tenant_id UUID DEFAULT NULLparam, UPSERT on(user_id, tenant_id)for lazy profile creation- Removed
handle_new_gamification_profiletrigger (profiles now created lazily byaward_xp()) - All 5 XP trigger functions rewritten to resolve
tenant_idfrom source tables refresh_leaderboard_cache()— UsesPARTITION BY tenant_idfor per-school rankings- 4 edge functions (
get-gamification-summary,get-leaderboard,check-achievements,spend-points) extracttenant_idfrom JWT and filter all queries
Phase 3 — Plan-Gated Features:
get_gamification_features(_tenant_id UUID)RPC returns JSONB feature flags based ontenants.plan- Optional
tenant_idadded togamification_achievementsandgamification_store_itemsfor custom school content - Frontend components show upgrade prompts when features are locked
- Edge functions return 403 with
feature_locked: truefor gated features
| File | Change |
|---|---|
supabase/migrations/20260217030000_tenant_scope_gamification.sql |
NEW — Full migration |
supabase/functions/get-gamification-summary/index.ts |
Tenant filter + features response |
supabase/functions/get-leaderboard/index.ts |
Tenant filter + plan check |
supabase/functions/check-achievements/index.ts |
Tenant filter + plan check |
supabase/functions/spend-points/index.ts |
Tenant filter + plan check + bug fixes |
lib/hooks/use-gamification.ts |
Added GamificationFeatures interface |
components/gamification/gamification-header-card.tsx |
Conditional trophy icon |
components/gamification/mini-leaderboard.tsx |
Upgrade prompt |
components/gamification/achievement-grid.tsx |
Upgrade prompt |
components/gamification/store-section.tsx |
Upgrade prompt |
messages/en.json |
Upgrade i18n keys |
messages/es.json |
Upgrade i18n keys |
| Bug | Fix |
|---|---|
spend-points used is_active |
Changed to is_available (correct column name) |
spend-points used item.title |
Changed to item.name (correct column name) |
These tables lack tenant_id and rely on join chains (question → exam → course → tenant). While RLS on exams prevents direct cross-tenant access, adding tenant_id would:
- Enable direct RLS policies
- Improve query performance (no join needed)
- Prevent edge cases where orphaned questions become visible
Should have tenant_id to scope notification templates and delivery per school.
- Course isolation between tenants
- Enrollment isolation
- Payment flow isolation
- Role-based access per tenant
- Auth flow with tenant context
Tenant isolation verified:
- Same user (
student@e2etest.com) has separate profiles: 310 XP/Level 3 in Default School vs 25 XP/Level 1 in test-academy - Leaderboard only shows students from current school
award_xp()correctly scopes XP to the target tenant
Plan gating verified (all 3 tiers):
| Plan | XP/Level/Streak | Leaderboard | Achievements | Store |
|---|---|---|---|---|
| free | Shown | Locked (upgrade prompt) | Locked (no trophy) | Locked (upgrade prompt) |
| basic | Shown | Unlocked (rankings) | Unlocked (trophy icon) | Locked (upgrade prompt) |
| professional | Shown | Unlocked | Unlocked | Unlocked (6 items, balance) |
| Area | Status | Priority |
|---|---|---|
| Core LMS (courses, enrollments, payments) | ISOLATED | Done |
| Certificates | ISOLATED | Done |
| Gamification | ISOLATED + PLAN-GATED | Done |
| Exam questions | Partially isolated (via joins) | P1 |
| Notifications | Not isolated | P2 |