Modelo conceptual
Tenant (Institución)
├── Users (alumnos, profesores, coordinadores, admin institucional)
├── Courses → Modules → Lessons
├── ClassGroups (grupos de clase)
├── MediaAssets (biblioteca de medios)
├── Automations
├── EmailTemplates
├── VideoProviderConfig (BBB/Zoom/Teams/Meet)
├── TenantApiKey (claves propias de IA)
├── TenantSubscription (billing)
├── ...todas las demás entidades
Los usuarios sin tenantId son B2C (plataforma directa). Los usuarios con tenantId pertenecen a una institución.
Aislamiento de datos — 3 capas
Capa 1: Filtro obligatorio en queries
Toda query Prisma en el código de la aplicación filtra por tenantId:
const { tenantId } = requireTenant(user);
const courses = await prisma.course.findMany({
where: { tenantId }, // OBLIGATORIO
});
requireTenant() en apps/web/lib/tenant.ts devuelve NextResponse 403 si el usuario no tiene tenant. Sin esta llamada, es imposible acceder a datos B2B.
Capa 2: RLS en Supabase
Como safety net frente a bugs, las políticas RLS en Supabase refuerzan el aislamiento incluso en queries directas. Si un código olvida el filtro tenantId, la base de datos lo rechaza.
Capa 3: Auditoría de admin global
Cuando un admin global necesita acceder a los datos de un tenant (soporte, depuración), utiliza impersonación mediante cookie HMAC con:
- TTL fijo de 1h (no prorrogable)
- Firma HMAC-SHA256 con
IMPERSONATION_SECRET - Auditoría en
AdminAuditLog(acciónimpersonate.start, IP, user-agent) getUserProfile()devuelveisImpersonating: true+ overlay en memoria- Supabase auth NUNCA modificado (solo overlay)
- Banner fijo en /institution/ alerta al admin durante la sesión
El panel admin SaaS global incluye impersonacion para soporte.
API keys por tenant
Por defecto, todas las llamadas LLM usan las claves globales (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) de Studeia. Los costos se imputan al tenant mediante metering.
Sin embargo, las instituciones que ya tienen cuentas en Anthropic/OpenAI/Google pueden usar sus propias claves (TenantApiKey, cifrada AES-256-GCM en la base de datos):
- Los costos van DIRECTAMENTE a la cuenta del tenant en Anthropic/OpenAI
- Studeia no cobra margen de IA — solo la suscripción mensual
- Resolución automática:
TenantApiKey→ProviderApiKeyglobal →process.env(cascada enapps/web/lib/api-key-resolver.ts)
NUNCA se asigna process.env en runtime (generaría conflictos entre tenants) — la key se pasa mediante las opciones del SDK.
White-label completo
Cada tenant puede personalizar:
| Aspecto | Cómo |
|---|---|
| Logo, favicon | Subida en ajustes |
| Colores (primary, accent, background) | Editor con vista previa |
| Fuente (Google Fonts) | Desplegable |
| Tema visual (de 9 opciones) | Toggle por usuario o predeterminado del tenant |
| CSS personalizado | Saneado, máx. 10KB |
| Dominio personalizado | DNS CNAME + TXT _studeia-verify + TLS automático Caddy (on-demand) |
| Logo en email | Por plantilla |
| Email de envío (SMTP/Resend/SendGrid) | TenantEmailConfig |
| Ocultar marca Studeia | Sí (plan enterprise) |
Roles y permisos
| Rol | Ámbito | Puede hacer |
|---|---|---|
student | Propio progreso | Chat tutor, cursos, simulacros, gamificación |
parent | Hijos vinculados | Ver progreso, alertas, informes |
teacher | Propios grupos/cursos | Crear cursos, subir material, ver alumnos de los grupos |
coordinator | Todos los grupos del tenant | Gestionar grupos, ver todos los alumnos |
pedagogue | Todos los alumnos del tenant | Orientación educativa, informes |
institution_admin | Todo el tenant | Config IA, API keys, white-label, usuarios |
admin | Global (plataforma) | Todo + gestionar tenants |
Límites por plan
Límites aplicados mediante checkTenantResourceLimit(tenantId, resource) en apps/web/lib/plan-limits.ts:
| Plan | Alumnos máx. | Profesores | Cursos | IA |
|---|---|---|---|---|
| Demo | 1 | 1 | 1 | Solo Haiku, 10 msgs/día |
| Mini | 10 | Ilimitado | Ilimitado | Todos los proveedores |
| Crescimento | 50 | Ilimitado | Ilimitado | Todos los proveedores |
| Escala | 100 | Ilimitado | Ilimitado | Todos los proveedores |
| Enterprise | Custom (maxStudentsOverride) | Ilimitado | Ilimitado | Todos los proveedores |
7 puntos de aplicación (todos auditados el 2026-04-11):
POST /api/courses/[courseId]/enroll— auto-inscripción del alumnoPOST /api/institution/courses/[id]/clone— clonación de cursoPOST /api/institution/courses/import— importación IMS CCexecuteEnrollUser()en automatizaciónPOST /api/scim/v2/Users— aprovisionamiento SCIMPOST /api/institution/users— vincular usuario existentePATCH /api/institution/users/[id]— ascender rol
Cómo solicitar override enterprise
Los tenants enterprise pueden tener Tenant.maxStudentsOverride: Int? ajustado por el admin global. Sin override (null), enterprise = ilimitado. Contacto comercial mediante contact@studeia.com.
Limitaciones
- Un User pertenece a UN tenant. Para profesores que atienden múltiples escuelas, crear usuarios separados.
- El intercambio de cursos entre tenants no es nativo (cada tenant tiene su CMS aislado). Roadmap: marketplace de cursos con licenciamiento.
- La migración de un tenant a otro plan es instantánea, pero el downgrade que viola los límites actuales queda bloqueado hasta que el tenant ajuste los recursos.