Resolução de tenant
Como todo request descobre em qual banco a consulta roda. Acontece antes de qualquer rota,
no TenantMiddleware (repo/apps/api/src/middleware/tenant.py), que é o middleware mais
externo da pilha — resolve a organização antes de tudo.
flowchart LR R[Request] --> J{Bearer token<br/>com org_id?} J -- sim --> OK[ContextVar<br/>current_org_id] J -- não --> S{X-Org ou ?org=} S -- sim --> SL[resolve_org_by_slug<br/>cache 60s] SL --> OK S -- não --> D[resolve_default_org<br/>is_default = true] D --> OK OK --> G["get_db / get_sync_db<br/>engine_for(org_id, kind)"]
A ordem, e por que ela é assim
| Prioridade | Fonte | Para quê |
|---|---|---|
| 1 | claim org_id do JWT | Requests autenticados. Token inválido aqui não dá erro — só significa “sem org via JWT”; a validação real fica em get_current_user. |
| 2 | header X-Org ou query ?org= | Rotas públicas com organização explícita (oferta, prévia, vitrine). |
| 3 | organização is_default | Fallback público — hoje a appstock (demo). |
Resolvida a organização, db/tenants.py entrega a engine: cacheada por (org_id, kind), criada
sob demanda a partir do DSN em org_databases. Pool app = 5+10; pool sync = 5+10 com
pool_timeout=10 de propósito — consulta pesada esgota o pool daquela organização e falha
rápido, em vez de enfileirar e contaminar as outras.
Caches de slug e de organização padrão têm TTL de 60s. tenants.invalidate() derruba tudo (é o
que o provisionamento chama no passo 4 do flx-onboarding).
Login: qual organização?
service_auth._resolve_org() decide antes de abrir qualquer sessão:
- Identificador com
@→ busca no ent-user-directory (email → org_id), que faz o join comorganizationspara pegar o slug. É o caminho normal. - Identificador sem
@(username) → usa o slug recebido no payload; sem slug, cai na organização padrão.
O JWT sai com org_id e org (slug), e a partir daí o passo 1 do middleware assume.
Estado real
O isolamento é físico e funciona. Um banco por organização; nenhuma query cross-org acontece fora do plano de controle. Ver adr-001-banco-por-org.
Três pontos frágeis:
1. A tela de login não pergunta a organização [P]
repo/apps/web/src/app/(auth)/login/ não tem campo de organização e o serviço não envia
org. Ou seja: o parâmetro existe na API e é inalcançável pela interface, e login por username
só funciona na organização padrão. Quem usa e-mail não percebe; quem usa username, sim.
2. Fallback silencioso para a organização padrão [M]
Rota pública sem ?org= não dá erro — resolve na demo. Foi decisão consciente (vitrine), mas
é o que faz links públicos de outras organizações abrirem em “não encontrado”:
o link é gerado sem o identificador da organização e o middleware entrega a demo, onde o token
não existe. Verificado: mesmo token com ?org=santonio → 404.
3. Sessão gravada não é validada [P]
get_current_user só decodifica o JWT. Existe tabela user_sessions, mas sair do sistema,
bloquear usuário ou revogar sessão não invalida o token — ele vale até vencer (até 30 dias
depois da renovação silenciosa por X-New-Token).
Lacunas
Rastreadas na fase-1-onboarding.