Multi-Tenancy
Estrategia de Aislamiento
La plataforma Imagy usa aislamiento logico con Row Level Security (RLS) de PostgreSQL. Todos los tenants comparten la misma infraestructura y esquema de base de datos, pero cada query se filtra automaticamente por tenant_id.
Jerarquia de Entidades
| Nivel | Descripcion | Ejemplo |
|---|---|---|
| Plataforma | Reimagine Tech LLC opera la plataforma | Platform admin gestiona flujos, proveedores |
| Tenant | Cliente B2B que contrata servicios | Fintech ABC, Cooperativa XYZ |
| Organizacion | Subdivision dentro del tenant | Canal digital, sucursal, aliado comercial |
| Usuario operador | Empleado del tenant | Operador que crea solicitudes |
| Usuario final | Persona que pasa por un flujo | Solicitante de credito, firmante |
Row Level Security (RLS)
Funcion de Contexto
CREATE OR REPLACE FUNCTION get_current_tenant_id() RETURNS UUID AS $$
BEGIN
RETURN current_setting('app.current_tenant_id', true)::UUID;
END;
$$ LANGUAGE plpgsql STABLE;Politica de Aislamiento
Cada tabla con datos de negocio tiene RLS habilitado:
ALTER TABLE credit_products ENABLE ROW LEVEL SECURITY;
-- Politica: cada query solo ve datos de su tenant
CREATE POLICY tenant_isolation ON credit_products
FOR ALL
USING (tenant_id = get_current_tenant_id());
-- Politica de control plane: acceso global cuando no hay tenant
CREATE POLICY control_plane_access ON credit_products
FOR ALL
USING (
current_setting('app.current_tenant_id', true) IS NULL
OR current_setting('app.current_tenant_id', true) = ''
);Establecimiento del Contexto
Implementacion en .NET
// EF Core Interceptor - establece tenant antes de cada query
public class TenantRlsInterceptor : DbConnectionInterceptor
{
private readonly IIdentityContext _identity;
public override async Task ConnectionOpenedAsync(
DbConnection connection, ConnectionEndEventData eventData, CancellationToken ct)
{
if (_identity.TenantId != Guid.Empty)
{
await using var cmd = connection.CreateCommand();
cmd.CommandText = $"SET app.current_tenant_id = '{_identity.TenantId}'";
await cmd.ExecuteNonQueryAsync(ct);
}
// Si TenantId es Empty (platform admin), no se establece
// → las politicas de control plane permiten acceso global
}
}// Dapper - mismo patron para read replica
public abstract class BaseReadRepository
{
private readonly IDbConnection _readDb;
private readonly IIdentityContext _identity;
protected async Task SetTenantContextAsync()
{
if (_readDb.State != ConnectionState.Open)
await ((DbConnection)_readDb).OpenAsync();
await _readDb.ExecuteAsync(
"SET app.current_tenant_id = @TenantId",
new { _identity.TenantId });
}
}Aislamiento por Capa
| Capa | Estrategia | Detalle |
|---|---|---|
| API Gateway | JWT validation | Extrae tenant_id del JWT firmado por Keycloak |
| Servicios | IIdentityContext | Cada servicio valida JWT y extrae tenant context |
| PostgreSQL | RLS policies | Filtro automatico por tenant_id en cada query |
| Valkey | Key prefix | Keys prefijadas: tenant:{id}:session:{token} |
| RabbitMQ | Payload | Header tenant_id en cada mensaje |
| S3 | Key prefix | Objetos bajo tenants/{id}/... |
| Keycloak | Realm-per-tenant | Cada tenant tiene su propio realm |
Organizaciones dentro de Tenants
Modelo en Keycloak
Las organizaciones se modelan usando la Organizations API nativa de Keycloak (v24+):
Capacidades de Organizaciones
| Capacidad | Descripcion |
|---|---|
| Dominios | Cada org puede tener dominios de email asociados |
| IdP Brokering | Cada org puede tener su propio Identity Provider externo |
| Auto-membership | Usuarios con email del dominio se asignan automaticamente |
| Home Realm Discovery | Keycloak redirige al IdP correcto segun el dominio del email |
| Claim en token | El JWT incluye las organizaciones del usuario |
Claim organization en el JWT
{
"sub": "user-uuid",
"tenant_id": "tenant-uuid",
"organization": {
"org-uuid-1": {
"name": "canal-digital",
"attributes": {
"department": ["sales"]
}
}
}
}Flujos y Asignacion por Tenant
Los flujos son globales (creados por platform admin) y se asignan a tenants u organizaciones:
Reglas de asignacion:
- Los flujos se definen globalmente (no pertenecen a ningun tenant)
- Se asignan a un tenant (todas sus orgs) o a una org especifica
- Los tenants/orgs no pueden modificar los flujos, solo usarlos
- La asignacion referencia una
flow_version_idespecifica
Subject 360 - Cross-Tenant con Aislamiento
ImagID (Subject 360) tiene un modelo especial: los datos del sujeto son cross-tenant pero la vista es aislada.
| Tabla | RLS | Justificacion |
|---|---|---|
subject_profiles | No | Permite identificar al mismo sujeto en multiples tenants |
subject_tenant_views | Si | Cada tenant solo ve su propia vista del sujeto |
subject_events | Si | Cada tenant solo ve las interacciones que el genero |
subject_devices | Si | Cada tenant solo ve los dispositivos que el registro |
subject_list_entries | Si | Cada tenant gestiona sus propias listas |
Resultado: Si la cedula 1234567890 existe en Tenant A y Tenant B, cada uno tiene su propia vista, historial y listas. Pero internamente comparten el subject_id, lo que permite analisis cross-tenant para fraude a nivel plataforma (solo accesible por platform admin).
Roles y Permisos
| Rol | Scope | Permisos |
|---|---|---|
platform_admin | Global | CRUD flujos, proveedores, templates. Gestionar todos los tenants. |
delegated_admin | Tenants asignados | Gestionar tenants especificos via role admin-tenant-{code} |
tenant_admin | Su tenant | Gestionar usuarios, orgs, productos, asignar flujos. |
operator | Su org | Crear solicitudes, gestionar creditos, operar. |
viewer | Su org | Solo lectura de solicitudes y resultados. |
Delegated Admin (Cross-Tenant)
El platform admin puede asignar acceso a tenants especificos sin dar acceso global:
Onboarding de un Nuevo Tenant
Cache de Tenants
La resolucion de tenant se cachea en Valkey para evitar consultas repetidas:
| Key | TTL | Contenido |
|---|---|---|
tenant_cache:{code} | 300s | {id, code, name, status} |
domain_resolve:{hostname} | 300s | {tenant_id, alias, verified} |
Invalidacion: Al crear, actualizar o suspender un tenant, se elimina la key del cache.
Reglas de Multi-Tenancy
HACER
- Siempre establecer tenant context antes de queries (via IIdentityContext)
- Incluir
tenant_iden todos los eventos publicados - Usar RLS en todas las tablas con datos de negocio
- Validar que el usuario pertenece al tenant antes de operar
- Prefixar keys de Valkey con tenant_id
NO HACER
- Nunca hacer queries sin tenant context (excepto platform admin)
- Nunca confiar en headers custom para tenant_id (siempre JWT)
- Nunca acceder a datos de otro tenant sin delegated admin role
- Nunca almacenar tenant_id en URLs visibles al usuario
- Nunca compartir secrets entre tenants