Multi-tenancy tiene fama de ser difícil. No lo es, si te comprometes con cuatro reglas desde el principio.
La fama viene de sistemas que añaden multi-tenancy después del hecho — agregando columnas de tenant a tablas existentes, pasando contexto de tenant a través de capas que no fueron diseñadas para eso, y descubriendo edge cases un cliente enojado a la vez. El enfoque de Quotery es diferente: cada business model fue diseñado con multi-tenancy desde el día uno. La columna de tenant no es una idea tardía; es tan fundamental como la clave primaria.
Regla uno: cada fila lleva tenant
Todo business model no-auth tiene tenant = ForeignKey('tenants.Tenant', ...). Sin excepción fuera del auth. Si una fila de negocio existe y no sabe su tenant, es bug.
Esta regla se aplica por convención y code review, no por la base de datos — pero la aplicación es estricta. Cada model en api/<app>/models/ que hereda de BaseModel o tiene FK de tenant o tiene una razón explícita y documentada para no tenerlo (models de auth, configuración del sistema). Los nuevos contribuyentes aprenden esta regla en su primera revisión de PR. Es la invariante estructural más importante del codebase.
Regla dos: los services filtran, las views no
Cada función de service acepta tenant= y aplica el filtro. La view es transporte tonto: resuelve request.user.tenant, lo pasa al service, confía en la capa.
Esta separación es lo que mantiene el codebase mantenible. Si las views filtraran, tendrías que auditar cada view para verificar el aislamiento de tenant. Si los models filtraran (vía un manager personalizado), perderías la capacidad de escribir consultas cross-tenant para fines administrativos. Al centralizar el filtrado de tenant en la capa de service, hay exactamente un lugar para verificar que cada consulta está scopeada: la función de service que es dueña de la consulta.
Regla tres: cross-tenant devuelve 404, no 403 Si un usuario pide un recurso que existe en el tenant de otro, la API devuelve 404, no 403. Devolver 403 filtra que el recurso existe. 404 lo trata como inexistente, que es lo que el usuario debería ver.
Este es un argumento de seguridad por oscuridad que es realmente correcto en este contexto. Un 403 dice 'no tienes permiso para ver esto, pero existe.' Un atacante determinado puede usar respuestas 403 para mapear qué tenants tienen qué recursos. Un 404 dice 'esto no existe en tu universo.' Es indistinguible de un recurso genuinamente inexistente. La capa de service logra esto filtrando consultas por tenant y lanzando DoesNotExist cuando la consulta filtrada retorna vacía — nunca verificando ownership después de la recuperación.
Regla cuatro: el tenant no va en la URL
No hay prefijo /api/t/<slug>/.... El tenant se deriva del cookie de sesión. URLs limpias, bookmarks portables, sin espacio para un URL-swap attack.
El enrutamiento de tenant por URL es común (Slack, por ejemplo) pero introduce una clase de bugs donde un usuario guarda un bookmark, lo comparte, y accidentalmente revela su contexto de tenant — o peor, donde una URL fabricada permite que un usuario intente acceder a otro tenant cambiando el slug. La resolución de tenant por sesión elimina esta superficie de ataque por completo. El usuario se autentica, la sesión lleva el tenant, y cada solicitud subsiguiente está automáticamente scopeada. El usuario nunca ve ni piensa en su identificador de tenant.
