Conceptual model
Tenant (Institution)
├── Users (students, teachers, coordinators, institutional admin)
├── Courses → Modules → Lessons
├── ClassGroups
├── MediaAssets
├── Automations
├── EmailTemplates
├── VideoProviderConfig (BBB/Zoom/Teams/Meet)
├── TenantApiKey (own AI keys)
├── TenantSubscription (billing)
└── ...all other entities
Data isolation — 3 layers
Layer 1: Mandatory query filter
All Prisma queries in application code filter by tenantId:
const { tenantId } = requireTenant(user);
const courses = await prisma.course.findMany({
where: { tenantId }, // MANDATORY
});
Layer 2: Supabase RLS
As safety net against bugs, RLS policies in Supabase enforce isolation even on direct queries.
Layer 3: Global admin audit
Global admin impersonation uses HMAC cookie with fixed 1h TTL, signed via IMPERSONATION_SECRET, audited in AdminAuditLog.
Per-tenant API keys
TenantApiKey encrypted AES-256-GCM. Resolution cascade: TenantApiKey → ProviderApiKey global → process.env. Costs go directly to tenant's Anthropic/OpenAI account.
Roles
| Role | Scope |
|---|---|
| student | Own progress |
| parent | Linked children |
| teacher | Own classes |
| coordinator | All classes in tenant |
| pedagogue | All students in tenant |
| institution_admin | Entire tenant |
| admin | Global platform |
Limits per plan
Enforced via checkTenantResourceLimit() at 7 enforcement points (audited 2026-04-11).