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

PrioridadeFontePara quê
1claim org_id do JWTRequests autenticados. Token inválido aqui não dá erro — só significa “sem org via JWT”; a validação real fica em get_current_user.
2header X-Org ou query ?org=Rotas públicas com organização explícita (oferta, prévia, vitrine).
3organização is_defaultFallback 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 com organizations para 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.