Ir al contenido

Arquitectura multi-tenant

Cada cliente de CÉNIT es una organización (organizations). Todo dato — players, sesiones GPS, wellness, lesiones, mensajes — está aislado a nivel de fila usando organization_id y RLS de Supabase. Un staff de Nacional jamás puede leer datos de otra org, ni siquiera con el JWT en mano: la base de datos rechaza la query.

Player surface: transparente. El jugador no ve la arquitectura — solo ve su club.

  • Cada user_profiles tiene organization_id (FK a organizations.id). Esa columna define a qué org pertenece el staff.
  • Cada players también tiene organization_id. Un jugador pertenece a una sola org.
  • Las policies RLS de Supabase comparan user_profiles.organization_id del JWT contra <tabla>.organization_id en cada SELECT/INSERT/UPDATE/DELETE.

organizations.plan define la oferta comercial:

PlanAtletasNotas
esencial40Risk Advisor con texto estático
proIlimitadoIA en Risk Advisor
enterpriseIlimitadoHoy idéntico a pro — diferenciador pendiente
expired0Redirige a /upgrade?expired=true

Detalles completos en Planes y features.

Hay tres clientes que el código usa según contexto:

  • lib/supabase/server.ts — cookies de sesión, respeta RLS. Es el cliente por defecto en Server Components y Server Actions.
  • lib/supabase/client.ts — browser, respeta RLS. Para queries reactivas en Client Components.
  • lib/supabase/admin.ts — service role, bypassea RLS. Solo para operaciones privilegiadas (onboarding masivo, /w/[token] sin login, alta de orgs desde superadmin). El caller es responsable de filtrar por organization_id manualmente.

Regla absoluta: si una server action usa el admin client, debe filtrar organization_id explícitamente en cada query. Olvidarse significa filtrar datos de otras orgs.

Campos clave: id, name, slug (kebab-case, único, 2–40 chars), logo_url, primary_color, secondary_color, plan, md_max_window_days (90–360, default 90), visible_player_tabs (JSONB con tabs habilitados), acwr_method (rma | ewma).

Las funciones como get_md_reference_per_player y get_gps_weekly_aggregation (migrations 100 y 105) son SECURITY DEFINER con RLS hardening interno: reciben p_organization_id y filtran ellas mismas. El TS caller siempre pasa el organization_id del staff autenticado.

  • Sistema de planes — el plan vive en organizations.plan y se chequea con canUse(plan, feature) desde lib/plans.ts.
  • White-label — fase 1 detecta el slug de la org desde el subdominio y lo propaga via header x-org-subdomain.
  • Superadmin — el panel interno de CÉNIT crea, lista y configura las orgs.
  • Audit logs por org — no existen. Si un staff borra un jugador, no hay registro de quién y cuándo. Pendiente, candidato a diferenciador Enterprise.
  • Cross-org features — no hay scouting que comparta jugadores entre orgs. Cada org tiene su propio external_players.
  • Soft delete vs hard deleteplayers tiene deleted_at para bulk delete + restore, pero otras tablas borran en cascada. Auditar antes de prometer “papelera” general.