Cuando construyes un SaaS empresarial, una de las decisiones mas importantes es como aislar los datos de cada cliente. En este articulo comparto el patron que use en ERP Market para manejar multiples empresas con total aislamiento de datos.

El Problema

Imagina un ERP donde multiples minimarkets usan el mismo sistema. Cada uno tiene sus productos, ventas, facturas y usuarios. Nunca deben ver los datos de otro.

Estrategias de Multi-Tenancy

Hay tres enfoques principales:

Estrategia Aislamiento Complejidad Costo
Base de datos por tenant Alto Alta Alto
Schema por tenant Medio Media Medio
Rows compartidos Bajo Baja Bajo

Para ERP Market elegí rows compartidos con filtrado automatico por su balance entre simplicidad y costo.

Implementacion

1. Modelo Base con Company

from django.db import models

class Company(models.Model):
    name = models.CharField(max_length=200)
    rut = models.CharField(max_length=12, unique=True)
    is_active = models.BooleanField(default=True)

class CompanyBoundModel(models.Model):
    """Modelo base que pertenece a una empresa."""
    company = models.ForeignKey(
        Company,
        on_delete=models.CASCADE,
        related_name='%(class)s_set'
    )

    class Meta:
        abstract = True

2. Manager con Filtrado Automatico

class CompanyManager(models.Manager):
    def for_company(self, company):
        return self.filter(company=company)

    def get_queryset(self):
        # El filtrado real se hace en las vistas
        return super().get_queryset()

3. Middleware para Contexto

from threading import local

_thread_locals = local()

def get_current_company():
    return getattr(_thread_locals, 'company', None)

class CompanyMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.user.is_authenticated:
            _thread_locals.company = request.user.profile.company
        response = self.get_response(request)
        return response

4. Mixin para Vistas

class CompanyFilterMixin:
    def get_queryset(self):
        qs = super().get_queryset()
        company = get_current_company()
        if company and hasattr(qs.model, 'company'):
            return qs.filter(company=company)
        return qs

Validaciones Criticas

Nunca confies solo en el filtrado. Agrega validaciones explicitas:

def crear_venta(request, producto_id):
    producto = get_object_or_404(
        Producto,
        id=producto_id,
        company=request.user.profile.company  # Validacion explicita
    )
    # ...

Resultados

Con este patron, ERP Market maneja multiples empresas con:

  • 0 filtraciones de datos entre tenants
  • Queries simples sin JOINs complejos
  • Migraciones unicas para todos los tenants
  • Backup centralizado de toda la plataforma

Conclusion

El multi-tenancy por rows es ideal para SaaS donde el costo de infraestructura importa. La clave es ser paranoico con las validaciones y nunca asumir que el filtrado automatico es suficiente.