Skip to content

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

NivelDescripcionEjemplo
PlataformaReimagine Tech LLC opera la plataformaPlatform admin gestiona flujos, proveedores
TenantCliente B2B que contrata serviciosFintech ABC, Cooperativa XYZ
OrganizacionSubdivision dentro del tenantCanal digital, sucursal, aliado comercial
Usuario operadorEmpleado del tenantOperador que crea solicitudes
Usuario finalPersona que pasa por un flujoSolicitante de credito, firmante

Row Level Security (RLS)

Funcion de Contexto

sql
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:

sql
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

csharp
// 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
    }
}
csharp
// 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

CapaEstrategiaDetalle
API GatewayJWT validationExtrae tenant_id del JWT firmado por Keycloak
ServiciosIIdentityContextCada servicio valida JWT y extrae tenant context
PostgreSQLRLS policiesFiltro automatico por tenant_id en cada query
ValkeyKey prefixKeys prefijadas: tenant:{id}:session:{token}
RabbitMQPayloadHeader tenant_id en cada mensaje
S3Key prefixObjetos bajo tenants/{id}/...
KeycloakRealm-per-tenantCada 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

CapacidadDescripcion
DominiosCada org puede tener dominios de email asociados
IdP BrokeringCada org puede tener su propio Identity Provider externo
Auto-membershipUsuarios con email del dominio se asignan automaticamente
Home Realm DiscoveryKeycloak redirige al IdP correcto segun el dominio del email
Claim en tokenEl JWT incluye las organizaciones del usuario

Claim organization en el JWT

json
{
  "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_id especifica

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.

TablaRLSJustificacion
subject_profilesNoPermite identificar al mismo sujeto en multiples tenants
subject_tenant_viewsSiCada tenant solo ve su propia vista del sujeto
subject_eventsSiCada tenant solo ve las interacciones que el genero
subject_devicesSiCada tenant solo ve los dispositivos que el registro
subject_list_entriesSiCada 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

RolScopePermisos
platform_adminGlobalCRUD flujos, proveedores, templates. Gestionar todos los tenants.
delegated_adminTenants asignadosGestionar tenants especificos via role admin-tenant-{code}
tenant_adminSu tenantGestionar usuarios, orgs, productos, asignar flujos.
operatorSu orgCrear solicitudes, gestionar creditos, operar.
viewerSu orgSolo 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:

KeyTTLContenido
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_id en 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

Reimagine Tech LLC — Documentacion Interna