Multi-tenancy tem fama de ser difícil. Não é, se você se compromete com quatro regras desde o início.
A fama vem de sistemas que enfiam multi-tenancy depois do fato — adicionando coluna de tenant em tabelas existentes, passando contexto de tenant por camadas que não foram projetadas para isso, e descobrindo edge cases um cliente bravo de cada vez. A abordagem do Quotery é diferente: todo business model foi projetado com multi-tenancy desde o dia um. A coluna de tenant não é um afterthought; é tão fundamental quanto a chave primária.
Regra um: toda linha carrega tenant
Todo business model não-auth tem tenant = ForeignKey('tenants.Tenant', ...). Sem exceção fora do auth. Se uma linha de negócio existe e não sabe o tenant dela, é bug.
Essa regra é aplicada por convenção e code review, não pelo banco — mas a aplicação é estrita. Todo model em api/<app>/models/ que herda de BaseModel ou tem FK de tenant ou tem uma razão explícita e documentada para não ter (models de auth, configuração de sistema). Contribuidores novos aprendem essa regra na primeira revisão de PR. É a invariante estrutural mais importante da codebase.
Regra dois: service filtra, view não
Toda função de service aceita tenant= e aplica filtro. View é transporte burro: resolve request.user.tenant, passa para o service, confia na camada.
Essa separação é o que mantém a codebase sustentável. Se views filtrassem, você teria que auditar cada view para verificar isolamento de tenant. Se models filtrassem (via manager customizado), você perderia a capacidade de fazer queries cross-tenant para fins administrativos. Ao centralizar a filtragem de tenant no service layer, tem exatamente um lugar para verificar que toda query está escopada: a função de service que é dona da query.
Regra três: cross-tenant é 404, não 403 Se um usuário pede um recurso que existe no tenant de outro, a API devolve 404, não 403. Devolver 403 vaza que o recurso existe. 404 trata como inexistente, que é o que o usuário deveria ver.
Esse é um argumento de segurança por obscuridade que está realmente correto nesse contexto. Um 403 diz 'você não tem permissão para ver isso, mas existe.' Um atacante determinado pode usar respostas 403 para mapear quais tenants têm quais recursos. Um 404 diz 'isso não existe no seu universo.' É indistinguível de um recurso genuinamente inexistente. O service layer consegue isso filtrando queries por tenant e levantando DoesNotExist quando a query filtrada volta vazia — nunca checando ownership depois da recuperação.
Regra quatro: tenant não vai na URL
Não tem prefixo /api/t/<slug>/.... O tenant é derivado do cookie de sessão. URLs ficam limpas, bookmarks portáveis, e sem espaço para URL-swap attack.
Roteamento de tenant baseado em URL é comum (Slack, por exemplo) mas introduz uma classe de bugs onde um usuário cria bookmark, compartilha, e acidentalmente revela seu contexto de tenant — ou pior, onde uma URL fabricada permite que um usuário tente acessar outro tenant trocando o slug. Resolução de tenant baseada em sessão elimina essa superfície de ataque completamente. O usuário autentica, a sessão carrega o tenant, e toda requisição subsequente é automaticamente escopada. O usuário nunca vê nem pensa sobre seu identificador de tenant.
