Introducción al diseño multi-tenant con FastAPI: patrones prácticos para aislamiento de tenants, autorización, estrategia de base de datos y logs de auditoría
Resumen
- El diseño multi-tenant es un enfoque para manejar de forma segura múltiples organizaciones clientes o unidades contractuales dentro de una sola aplicación.
- En FastAPI, el principio básico es determinar el
tenant_idde cada solicitud en función del usuario autenticado, y asegurar que todas las rutas de acceso a datos incluyan siempre esa condición. - Hay tres estrategias principales de aislamiento:
base de datos compartida / tablas compartidas,base de datos compartida / esquema por tenantybase de datos separada por tenant. Muchos equipos comienzan con tablas compartidas, pero según los requisitos, puede ser necesario aumentar antes el grado de aislamiento. - El estado peligroso no es “la autenticación está rota”, sino “la autenticación funciona, pero se pueden ver datos de otro tenant”. Los límites entre tenants se protegen más mediante autorización y diseño de consultas que mediante autenticación en sí.
- Este artículo organiza, paso a paso, cómo manejar
tenant_iden FastAPI, cómo delimitar consultas en SQLAlchemy, cómo diseñar permisos, cómo construir logs de auditoría, cómo probar límites y cómo pensar en futuras estrategias de separación.
¿Quién se beneficiará de leer esto?
Desarrolladores individuales y personas que están aprendiendo
Esto es útil si quieres construir un panel tipo SaaS o funciones basadas en equipos, pero la diferencia entre un diseño “basado en usuario” y uno “basado en organización” aún no está del todo clara.
Es especialmente útil si actualmente gestionas datos solo mediante user_id y ahora estás empezando a pensar: “Quiero separar la Empresa A de la Empresa B”.
Ingenieros backend en equipos pequeños
Esto está dirigido a quienes planean manejar múltiples clientes en una sola app FastAPI y se preguntan: “¿En qué capa debería tratar tenant_id?” o “¿Cómo debería diseñar la base de datos?”
Si después se descubren huecos de autorización, el daño puede ser grave, así que esto ayuda a organizar las ideas que conviene entender desde el principio.
Equipos SaaS y startups
Esto también es para equipos que ya operan en un entorno multi-tenant, donde permisos, auditabilidad, facturación y el grado de aislamiento de datos se están convirtiendo en cuestiones de diseño.
Puede usarse como guía de revisión de diseño, incluyendo cómo decidir si seguir con tablas compartidas o avanzar hacia separación por esquemas o por bases de datos.
Notas de accesibilidad
- El artículo organiza primero los conceptos y luego avanza en el orden de
autenticación → resolución de tenant_id → estrategia de aislamiento en BD → autorización → ejemplos de implementación → auditoría → testing. - Los términos técnicos se explican brevemente cuando aparecen por primera vez, y después se usa el mismo vocabulario de forma consistente para facilitar el seguimiento.
- Los ejemplos de código están divididos en bloques cortos, y cada bloque muestra un solo rol.
- El nivel objetivo es aproximadamente AA.
1. ¿Qué es multi-tenancy?
Multi-tenancy es un diseño en el que una sola plataforma de aplicación es compartida por múltiples clientes u organizaciones, manteniendo seguras las fronteras de datos y permisos de cada tenant.
Aquí, un tenant puede referirse a unidades como:
- Una empresa
- Un equipo
- Una escuela
- Un grupo de tiendas
- Una organización cliente definida por contrato
El punto importante es que “usuario” y “tenant” no son lo mismo.
- Un usuario es una persona individual
- Un tenant es la organización a la que pertenece
Por ejemplo, una sola persona usuaria puede pertenecer a varios tenants.
Por eso, es necesario diseñar el sistema para que tenant_id se maneje explícitamente, no solo user_id.
2. Lo primero que hay que decidir: dónde se representan los límites del tenant
La primera decisión de diseño en un sistema multi-tenant es cómo representar “en qué contexto de tenant está operando esta solicitud”.
Hay tres métodos representativos:
- Subdominio
- Ejemplo:
acme.example.com,foo.example.com
- Ejemplo:
- Ruta
- Ejemplo:
/t/acme/projects,/t/foo/projects
- Ejemplo:
- Token o header
- Poner
tenant_iden un claim del JWT - O usar un header como
X-Tenant-ID
- Poner
En la práctica, ayuda pensarlo así:
- Para SaaS orientado a navegador, representar explícitamente el tenant en el subdominio o en la ruta también facilita entenderlo desde la UI
- Para sistemas centrados en API, a menudo es más fácil incluir
tenant_ido una lista de membresías en el token de autenticación - Sin embargo, un diseño en el que
tenant_idpueda especificarse libremente solo mediante header es peligroso, así que siempre debe contrastarse con la información de autenticación
3. Tres estrategias de aislamiento en base de datos
El diseño de base de datos multi-tenant puede dividirse en tres estrategias generales.
3.1 Base de datos compartida / tablas compartidas
Todos los datos de tenants se almacenan en las mismas tablas, y cada registro tiene un tenant_id.
Ejemplo:
projects
- id
- tenant_id
- name
- created_at
Ventajas
- Fácil de implementar
- Bajo costo operativo
- Las adiciones de tablas y migraciones pueden gestionarse de una sola vez
Desventajas
- Si una consulta omite la condición de tenant, pueden hacerse visibles datos de otro tenant
- Las copias de seguridad y eliminaciones por cliente son engorrosas
- Débil frente a requisitos de aislamiento más estrictos
Si quieres empezar en pequeño, este suele ser el enfoque más realista.
Sin embargo, olvidar la condición tenant_id es el mayor riesgo individual.
3.2 Base de datos compartida / esquema por tenant
Este método separa a cada tenant en su propio esquema dentro de la misma base de datos.
Ejemplos:
tenant_acme.projectstenant_foo.projects
Ventajas
- Aislamiento más fuerte que con tablas compartidas
- Copias de seguridad y gestión de datos por cliente más fáciles
Desventajas
- Las migraciones y la gestión se vuelven algo más complejas
- La operación se vuelve más pesada a medida que crece el número de tenants
3.3 Base de datos separada por tenant
Cada tenant tiene su propia base de datos.
Ventajas
- El aislamiento más fuerte
- Más fácil cumplir requisitos legales, contractuales o de alta seguridad
- Más fácil recuperación y migración específicas por cliente
Desventajas
- Alto costo operativo
- Más difícil el cambio de conexiones y las migraciones
- A menudo es excesivo en una fase temprana
4. El enfoque básico en FastAPI: resolver tenant_id en dependencias
En FastAPI, el diseño se entiende fácilmente si el contexto del tenant se resuelve mediante Depends y luego se pasa a las capas de router y servicios.
4.1 Obtener el usuario actual
Primero, obtén el usuario autenticado.
# app/deps/auth.py
from fastapi import Depends, HTTPException, status
from app.models.auth import CurrentUser
def get_current_user() -> CurrentUser:
# En la realidad, verificar JWT, etc.
return CurrentUser(user_id=1, tenant_ids=[10, 20], active_tenant_id=10)
4.2 Resolver el tenant_id actual
Determina el tenant de esta solicitud a partir del token, la ruta o el header, verificando al mismo tiempo que la persona usuaria realmente pertenezca a él.
# app/deps/tenant.py
from fastapi import Depends, Header, HTTPException, status
from app.deps.auth import get_current_user
from app.models.auth import CurrentUser
def get_current_tenant_id(
current_user: CurrentUser = Depends(get_current_user),
x_tenant_id: int | None = Header(default=None),
) -> int:
tenant_id = x_tenant_id or current_user.active_tenant_id
if tenant_id is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="tenant is not selected",
)
if tenant_id not in current_user.tenant_ids:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="tenant access is forbidden",
)
return tenant_id
Lo que importa aquí es que x_tenant_id no se confía tal cual.
Siempre debes verificar: “¿Esta persona usuaria realmente pertenece a ese tenant?”
5. Pasar siempre tenant_id en SQLAlchemy
Con el enfoque de tablas compartidas, la regla básica es que todas las tablas principales deben incluir tenant_id.
5.1 Ejemplo de modelo
# app/models/project.py
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import Integer, String
from app.db.base import Base
class Project(Base):
__tablename__ = "projects"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
tenant_id: Mapped[int] = mapped_column(Integer, index=True, nullable=False)
name: Mapped[str] = mapped_column(String(200), nullable=False)
5.2 Filtrar siempre las consultas por tenant_id
En la capa de servicio o repositorio, incluye siempre tenant_id en las condiciones.
# app/repositories/project_repository.py
from sqlalchemy.orm import Session
from app.models.project import Project
class ProjectRepository:
def __init__(self, db: Session):
self.db = db
def list_by_tenant(self, tenant_id: int) -> list[Project]:
return (
self.db.query(Project)
.filter(Project.tenant_id == tenant_id)
.order_by(Project.id.desc())
.all()
)
def get_by_id(self, tenant_id: int, project_id: int) -> Project | None:
return (
self.db.query(Project)
.filter(Project.tenant_id == tenant_id, Project.id == project_id)
.first()
)
Es importante no buscar solo por project_id.
Aunque id sea único, para evitar accidentes en los que “puede recuperarse un proyecto de otro tenant”, hay que tratarlo siempre junto con tenant_id.
6. En los routers, inclínate por “llamadas a servicios que siempre incluyan tenant_id”
El router debería ocuparse solo de aspectos HTTP, mientras tenant_id se pasa explícitamente a servicios y repositorios.
# app/api/v1/routers/projects.py
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.deps.db import get_db
from app.deps.tenant import get_current_tenant_id
from app.repositories.project_repository import ProjectRepository
router = APIRouter(prefix="/projects", tags=["projects"])
@router.get("")
def list_projects(
tenant_id: int = Depends(get_current_tenant_id),
db: Session = Depends(get_db),
):
repo = ProjectRepository(db)
items = repo.list_by_tenant(tenant_id)
return items
La ventaja de este diseño es que en la revisión de código es fácil confirmar visualmente:
- ¿Se está recibiendo
tenant_id? - ¿Se está pasando a la capa de acceso a BD?
7. Diseño de roles dentro de un tenant: no todo el mundo en el mismo tenant tiene los mismos permisos
En sistemas multi-tenant, necesitas separar “fronteras entre tenants” de “permisos dentro del tenant”.
Por ejemplo, incluso dentro de la misma empresa A, puede haber roles como:
- Owner
- Admin
- Member
- Viewer
7.1 Tener una tabla de membresías
Un diseño común es colocar una tabla de unión entre users y tenants.
tenant_memberships
- user_id
- tenant_id
- role
7.2 Verificar permisos en dependencias o servicios
# app/deps/permissions.py
from fastapi import Depends, HTTPException, status
from app.deps.auth import get_current_user
from app.deps.tenant import get_current_tenant_id
def require_tenant_admin(
current_user=Depends(get_current_user),
tenant_id: int = Depends(get_current_tenant_id),
):
membership = None # En la realidad, consultar BD por user_id y tenant_id
if membership is None or membership.role not in {"owner", "admin"}:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="admin role required",
)
En otras palabras, comprueba en dos pasos:
- ¿La persona usuaria pertenece a ese tenant?
- ¿Qué se le permite hacer dentro de ese tenant?
8. UI de cambio de tenant y diseño del token
Si una persona usuaria pertenece a varios tenants, la UI necesita mostrar claramente “qué tenant se está viendo actualmente”.
Hay dos patrones comunes:
8.1 Incluir active_tenant_id en el token
Reflejar el tenant_id actualmente seleccionado dentro del JWT al iniciar sesión o al cambiar de tenant.
Ventajas
- No hace falta añadir un header en cada solicitud
- La implementación de la API es más simple
Desventajas
- El token debe reemitirse cada vez que se cambia de tenant
8.2 Poner la lista de membresías en el token y dejar que el cliente elija vía header
Guardar algo como tenant_ids=[...] en el token y hacer que la solicitud especifique el tenant actual vía X-Tenant-ID.
Ventajas
- Cambio flexible
- Más fácil de manejar en varias pestañas
Desventajas
- Es fácil crear confusión si no se estandariza el uso del header
- Siempre se requieren verificaciones de membresía
Ambos son válidos, pero lo importante es que el equipo use un solo enfoque de forma consistente.
9. Logs de auditoría: registrar quién hizo qué y en qué tenant
En sistemas multi-tenant, los logs de auditoría son especialmente importantes para investigación de incidentes y soporte al cliente.
Como mínimo, conviene registrar:
tenant_iduser_id- Operación realizada
- Recurso objetivo
- Éxito/fallo
- Timestamp
- Request ID
Aquí tienes un ejemplo simple.
# app/services/audit.py
import logging
logger = logging.getLogger("audit")
def log_audit(
tenant_id: int,
user_id: int,
action: str,
resource: str,
resource_id: int | None = None,
):
logger.info(
"audit event",
extra={
"tenant_id": tenant_id,
"user_id": user_id,
"action": action,
"resource": resource,
"resource_id": resource_id,
},
)
Por ejemplo, operaciones como “eliminó una factura” o “cambió el rol de un miembro” merecen registrarse siempre en el log de auditoría.
10. Estrategia de testing: el bug más aterrador es “se ve información de otro tenant”
En sistemas multi-tenant, la prioridad máxima en testing no es el happy path, sino probar fronteras.
10.1 Tests mínimos que conviene tener
- Un usuario del tenant A solo puede ver datos del tenant A
- Si un usuario del tenant A especifica el
tenant_iddel tenant B, la API devuelve 403 - Aunque exista un
project_id, si pertenece a otro tenant, la API devuelve 404 o 403 - Si la persona usuaria no tiene el rol requerido dentro del tenant, se niegan las operaciones de administración
10.2 Ejemplo de test de API
def test_user_cannot_access_other_tenant_project(client, token_for_tenant_a):
res = client.get(
"/projects/999",
headers={
"Authorization": f"Bearer {token_for_tenant_a}",
"X-Tenant-ID": "20", # other tenant
},
)
assert res.status_code in (403, 404)
Si devolver 404 o 403 depende de tu diseño.
Si quieres ocultar la propia existencia del recurso, devuelve 404. Si quieres hacer explícito el problema de permisos, devuelve 403. Alinea esto en todo el equipo.
11. Puntos de decisión pensando en el crecimiento futuro
Aunque empieces con tablas compartidas, deberías plantearte revisar tu estrategia de separación cuando empiecen a aparecer señales como estas:
- El volumen de datos de un cliente específico se vuelve mucho mayor que el del resto
- Se vuelven más fuertes los requisitos de copia de seguridad o eliminación por cliente
- Requisitos legales o contractuales exigen un aislamiento más fuerte
- Problemas de rendimiento a nivel de tenant se están derramando sobre otros tenants
- Los entornos dedicados por cliente se convierten en requisito de ventas
En ese momento, si tenant_id ya es explícito en todas partes y la capa de servicios y los logs de auditoría están bien organizados, la migración se vuelve mucho más fácil.
12. Hoja de ruta según el tipo de lector
Para desarrolladores individuales y personas que están aprendiendo
- Empieza con tablas compartidas y añade
tenant_ida todas las tablas principales - Construye una dependencia que resuelva
tenant_iddesde JWT o sesión - Añade condiciones
tenant_ida todas las consultas de lista y detalle - Añade al menos un test que impida acceso entre tenants
Para ingenieros en equipos pequeños
- Haz inventario de qué modelos pertenecen a tenants
- Estandariza el alcance por tenant en la capa de repositorio
- Decide el modelo de roles dentro del tenant
- Incluye
tenant_iden los logs de auditoría - Refleja también en OpenAPI y en los formatos de error “cómo funciona la selección de tenant”
Para equipos SaaS y startups
- Reevalúa el enfoque actual de aislamiento
- Separa las preocupaciones entre aislamiento de datos, aislamiento de rendimiento y requisitos legales
- Refuerza los tests de límites y los logs de auditoría
- Si es necesario, crea un plan de migración hacia separación por esquema o por base de datos
- Revisa de nuevo los flujos de facturación, auditoría y cambio de permisos por tenant
Enlaces de referencia
- FastAPI
- SQLAlchemy
- Contexto útil sobre diseño
Conclusión
- La esencia del diseño multi-tenant no es quedarse satisfecho con “la persona usuaria está autenticada”, sino seguir dejando claro “a qué contexto de tenant pertenece esta solicitud”.
- En FastAPI, resolver
tenant_iden dependencias y pasarlo explícitamente a las capas de servicio y repositorio es fácil de entender y seguro. - El enfoque de tablas compartidas es fácil para empezar, pero olvidar la condición
tenant_ides su mayor riesgo. Por eso, los tests y los logs de auditoría son esenciales para proteger el límite. - El enfoque práctico es empezar en pequeño y luego construir una base que más adelante pueda evolucionar hacia separación por esquema o por base de datos según futuros requisitos legales, de rendimiento y de clientes.
Una continuación natural de este tema serían artículos como “diseño de logs de auditoría en FastAPI”, “gestión de permisos con RBAC/ABAC” o “facturación y control de planes para SaaS”.
