michangarrito/docs/97-adr/ADR-0001-multi-tenant-architecture.md
rckrdmrd 97f407c661 [MIGRATION-V2] feat: Migrar michangarrito a estructura v2
- Prefijo v2: MCH
- TRACEABILITY-MASTER.yml creado
- Listo para integracion como submodulo

Workspace: v2.0.0 | SIMCO: v4.0.0
2026-01-10 11:28:54 -06:00

4.5 KiB

id type title status decision_date updated_at simco_version stakeholders tags
ADR-0001 ADR Arquitectura Multi-Tenant Accepted 2026-01-04 2026-01-10 3.8.0
Equipo MiChangarrito
multi-tenant
postgresql
rls
database
arquitectura
seguridad

ADR-0001: Arquitectura Multi-Tenant

Metadata

Campo Valor
ID ADR-0001
Estado Accepted
Fecha 2026-01-06
Autor Architecture Team
Supersede -

Contexto

MiChangarrito es un SaaS para multiples tiendas independientes. Cada tienda (tenant) debe tener sus datos completamente aislados de otras tiendas por razones de:

  • Seguridad: Un tenant no puede ver datos de otro
  • Privacidad: Informacion financiera sensible
  • Regulacion: Cumplimiento con leyes de proteccion de datos
  • Escalabilidad: Soporte para miles de tiendas

Se necesita decidir la estrategia de multi-tenancy para la base de datos.


Decision

Adoptamos el modelo de multi-tenancy por columna (tenant_id) con Row Level Security (RLS) de PostgreSQL.

Cada tabla tiene una columna tenant_id y politicas RLS que filtran automaticamente los datos segun el tenant actual.

-- Ejemplo de RLS
ALTER TABLE catalog.products ENABLE ROW LEVEL SECURITY;

CREATE POLICY products_tenant_isolation ON catalog.products
    USING (tenant_id = current_setting('app.current_tenant')::UUID);

Alternativas Consideradas

Opcion 1: Base de datos por tenant

  • Pros:
    • Aislamiento total
    • Facil backup/restore individual
    • Sin riesgo de data leaks
  • Cons:
    • Costoso en recursos
    • Complejo de mantener (miles de DBs)
    • Migraciones complicadas

Opcion 2: Schema por tenant

  • Pros:
    • Buen aislamiento
    • Un solo servidor
    • Backup conjunto
  • Cons:
    • Limite de schemas en PostgreSQL
    • Migraciones complicadas
    • Connection pooling complejo

Opcion 3: Tenant ID con RLS (Elegida)

  • Pros:
    • Simple de implementar
    • Escalable
    • Migraciones faciles
    • RLS garantiza aislamiento
    • Eficiente en recursos
  • Cons:
    • Requiere disciplina (no olvidar tenant_id)
    • Dependencia de RLS
    • Query planning puede ser afectado

Consecuencias

Positivas

  1. Escalabilidad: Una sola base de datos maneja todos los tenants
  2. Simplicidad: Migraciones aplican a todos
  3. Eficiencia: Pool de conexiones compartido
  4. Seguridad: RLS es enforced a nivel de DB

Negativas

  1. Disciplina: Todas las queries deben considerar tenant_id
  2. Testing: Necesitamos tests de aislamiento
  3. Performance: Indices deben incluir tenant_id

Neutrales

  1. Backup: Todos los tenants juntos
  2. Monitoreo: Una base de datos que monitorear

Implementacion

Estructura de Tablas

CREATE TABLE catalog.products (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    name VARCHAR(255) NOT NULL,
    price DECIMAL(10,2),
    -- ...
    CONSTRAINT idx_products_tenant UNIQUE (tenant_id, id)
);

Middleware de Contexto

// Interceptor NestJS
@Injectable()
export class TenantInterceptor implements NestInterceptor {
  async intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const tenantId = request.user?.tenantId;

    // Setear tenant en conexion
    await this.dataSource.query(
      `SET app.current_tenant = '${tenantId}'`
    );

    return next.handle();
  }
}

Validacion

Criterios de Exito

  • Todas las tablas tienen tenant_id
  • RLS habilitado en todas las tablas
  • Tests de aislamiento pasan
  • No hay queries sin filtro de tenant

Tests de Aislamiento

it('should not allow tenant A to see tenant B data', async () => {
  // Setup tenant A
  await setTenant(tenantAId);
  const productA = await createProduct({ name: 'Product A' });

  // Switch to tenant B
  await setTenant(tenantBId);
  const products = await productService.findAll();

  // Tenant B should not see product A
  expect(products).not.toContainEqual(
    expect.objectContaining({ id: productA.id })
  );
});

Referencias


Fecha decision: 2026-01-06 Autores: Architecture Team