Supabase Auth con Row Level Security es la opción correcta para CRM a medida multi-tenant en LATAM. Cada usuario ve solo lo que su tenant permite, la separación está en la capa base de datos (no en código aplicación), y cero riesgo de leak entre tenants. Catalizadora construyó plataforma para 100 franquicias internacionales con este patrón. Sin retainers, sin licencias atadas, código a tu nombre.
¿Qué es multi-tenant en CRM y por qué es difícil?
Multi-tenant significa: un solo deploy del sistema sirve a N organizaciones (tenants) que no pueden ver datos entre sí. Cada tenant tiene sus usuarios, sus clientes, sus deals, sus reportes. Cero leak entre tenants es requisito no negociable.
El error clásico es manejar tenancy en código aplicación: cada query agrega "WHERE tenant_id = X" según el usuario. Funciona hasta que hay un bug, un endpoint que olvida agregar el filtro, o un developer que ejecuta query sin pensar. Resultado: tenant A ve datos de tenant B. Disaster comercial y legal.
La solución correcta es manejar tenancy en la capa base de datos con Row Level Security (RLS). Las políticas filtran automáticamente cada query según el JWT del usuario. Aunque el código aplicación tenga bug, la base no devuelve filas no autorizadas.
¿Cómo funciona RLS con Supabase?
Supabase es PostgreSQL gestionado. RLS es feature nativa de PostgreSQL desde 2017. El flujo:
- Usuario se autentica vía Supabase Auth (email password, magic link, OAuth, OTP)
- Supabase emite JWT con campos estándar (id, email, role) más custom claims (tenant_id, oficina_id, rol_custom)
- App envía requests con el JWT en el header Authorization
- Supabase API ejecuta las queries pasando el JWT a PostgreSQL
- PostgreSQL aplica RLS policies que leen el JWT y filtran filas
El developer escribe el código como si fuera single-tenant. La RLS hace el filtrado por debajo. Cero código tenant-specific en la aplicación.
El caso real: 100 franquicias internacionales con 7 roles RBAC
Catalizadora construyó la plataforma multi-tenant para una distribuidora con 100 franquicias internacionales:
- 7 roles RBAC definidos (tni_admin, tni_regional, franchise_owner, franchise_admin, franchise_manager, franchise_user, auditor)
- RLS por oficina usando JWT custom claims (auth.user_oficina_id())
- DENY UPDATE/DELETE en audit log (append-only)
- Schemas silver y gold protegidos por RLS
- Session mode connection pooler (no transaction mode)
- 3.6 millones de filas migradas a Supabase con RLS activo desde día uno
Resultado: cada franquicia ve solo sus datos. La arquitectura escala a 100 a 200 inmobiliarias bajo el mismo deploy si fuera SaaS inmobiliario.
Estructura típica de tabla multi-tenant
Una tabla de CRM con tenant_id como columna y RLS policy:
create table public.deals (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id),
owner_id uuid not null references auth.users(id),
title text not null,
amount numeric,
stage text not null,
created_at timestamptz default now()
);
create index deals_tenant_idx on public.deals(tenant_id);
alter table public.deals enable row level security;
create policy "deals_read_own_tenant" on public.deals
for select
using (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid);
create policy "deals_write_own_tenant" on public.deals
for insert with check (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid);
Cada query del usuario lee solo deals de su tenant. Sin código aplicación tenant-specific. RLS hace el trabajo.
JWT custom claims: cómo agregarlos
Supabase Auth permite agregar claims custom al JWT vía:
- Auth Hook (Auth Hook Send Email, Auth Hook Custom Access Token): función serverless que enriquece el JWT en cada login
- raw_app_meta_data en auth.users: campo JSON que se incluye automáticamente en cada JWT emitido
- Trigger before login que populates raw_app_meta_data
Patrón típico: cuando se crea un usuario, un trigger asigna tenant_id y rol en raw_app_meta_data. Cada login refresca el JWT con esos claims. Las policies leen el campo tenant_id desde el JWT con el operador JSON de PostgreSQL.
Roles RBAC jerárquicos
Para un CRM multi-tenant serio, los roles típicos en LATAM:
| Rol | Permisos |
|---|---|
| super_admin (Catalizadora) | Todo en todos los tenants. Solo para soporte y migración |
| tenant_admin | Todo dentro de su tenant. CRUD usuarios, configuración, billing |
| manager | CRUD deals, reportes de su equipo. No usuarios ni billing |
| sales_rep | CRUD deals propios. Lectura de equipo |
| viewer | Solo lectura de su tenant |
| auditor | Solo lectura más audit log completo. Para compliance |
Las policies RLS implementan esta jerarquía: super_admin bypassea RLS, tenant_admin lee todo su tenant, manager filtra por equipo, sales_rep filtra por owner_id propio.
Audit log inmutable con SHA-256
Para CRM multi-tenant con compliance (financiero, legal, salud), el audit log debe ser append-only verificable. Catalizadora implementa:
- Schema audit aparte con RLS DENY UPDATE/DELETE forzado
- Trigger en cada INSERT que calcula SHA-256 hash del registro anterior más el nuevo (hash chain)
- Función audit.verify_chain_integrity() que recalcula la cadena y detecta manipulaciones
- Cada evento tiene user_id, ip, action, table, row_id en JSON metadata
Si alguien intenta modificar audit log directamente en base, falla por RLS. Si modifica via super_admin, la hash chain se rompe en la próxima verificación.
¿Qué pasa con storage de archivos multi-tenant?
Supabase Storage tiene RLS análoga para buckets y objects. Patrón típico:
- Bucket por tipo (avatars, documents, attachments)
- Estructura de carpetas:
/[tenant_id]/[user_id]/[filename] - Policy: solo permitir acceso a objects donde el primer segmento del path coincide con tenant_id del JWT
Cada tenant ve solo sus archivos. Cero leak. Sin lógica tenant-specific en código aplicación.
Conexión pooler: session mode vs transaction mode
Para CRM serio con RLS activo, usar session mode pooler (puerto 5432 directo o pooler en port 6543 con session mode), no transaction mode (port 6543 transaction). Razón:
- Transaction mode reinicia la sesión entre queries. El JWT context se pierde.
- Session mode mantiene la sesión durante la duración de la conexión. RLS funciona correctamente.
Catalizadora usa session mode para todas las plataformas multi-tenant sobre Supabase.
Escalabilidad: ¿hasta cuántos tenants y usuarios?
Supabase Pro plan base maneja sin problemas:
- 50,000 a 100,000 usuarios activos por instance
- 100 a 1,000 tenants por instance
- 5 a 50 millones de filas con RLS activo (con índices correctos)
Para escalas mayores, Supabase ofrece compute upgrade lineal y eventualmente self-hosted con replicación. La distribuidora con 100 franquicias internacionales que Catalizadora construyó corre cómoda sobre Supabase Pro compute Micro.
Stack recomendado para CRM multi-tenant 2026
| Capa | Tecnología | Razón |
|---|---|---|
| Frontend | Next.js 15 + Tailwind + shadcn/ui | RSC, app router maduro |
| Backend API | Direct Supabase + Edge Functions | Reduce hop, RLS automático |
| Auth | Supabase Auth con custom claims | Multi-tenant nativo |
| Database | Supabase Pro PostgreSQL | RLS, JSONB, pgvector |
| Storage | Supabase Storage con RLS | Archivos por tenant |
| Realtime | Supabase Realtime | Subscriptions con RLS |
| Observabilidad | Sentry + Better Stack | Errors, logs por tenant |
Próximos pasos
Si vas a construir CRM multi-tenant serio en LATAM, Supabase Auth con RLS es la opción correcta para 90 por ciento de casos. La excepción son escalas extremas (más de 200,000 usuarios concurrentes) o requisitos regulatorios on-prem específicos.
MAGIA Forge construye el CRM multi-tenant completo en 12 semanas con CI/CD, hardening, observabilidad y auditoría compliance por 20,000 USD pago único. Para CRM single-tenant más simple, MAGIA Core cubre por 15,000 USD. Código a tu nombre, sin retainers, sin licencias atadas.