Pular para conteúdo

Operação multi-conta

Alterna a conta operacional com autorização server-side e faz chat e regeneração usarem esse contexto sem confiar em um tenant informado pelo navegador.

Por que foi construído assim

Uma relacao de hierarquia nao e autorizacao. A conta ativa e uma preferencia em cookie HttpOnly, sempre revalidada contra memberships explicitas. Isso impede que um UUID em cookie, body ou header conceda acesso.

Na fronteira com os workers, o Next assina uma assercao curta depois de resolver a sessao e a membership. O worker valida a assinatura e rele a autorizacao no banco antes de executar, preservando revogacao imediata.

Os callers do browser usam um único proxy porque o tenant precisa ser resolvido depois da sessão, no servidor. A troca continua terminando em reload para descartar o estado derivado da conta anterior; o chat recebe um aviso antes da navegação somente para abortar a sessão ou o stream em voo. Esse evento não transporta nem escolhe o novo tenant.

Stack

Camada Tecnologia
Linguagem TypeScript e Python 3.12
Framework Next.js 15, React 19 e FastAPI
Banco Supabase PostgreSQL
Onde roda Vercel, Coolify VPS Master e Preview isolado na VPS Dev
Servicos externos Supabase Auth, OpenRouter e Supabase Management API somente no cleanup E2E

Como funciona

flowchart TD
    classDef component fill:#D1FAE5,stroke:#10B981,color:#111
    classDef flow fill:#DBEAFE,stroke:#3B82F6,color:#111
    classDef decision fill:#EDE9FE,stroke:#8B5CF6,color:#111
    classDef core fill:#FEE2E2,stroke:#EF4444,color:#111
    classDef warning fill:#FEF9C3,stroke:#EAB308,color:#111

    subgraph SG_component["Componentes"]
        a_selector["Selecao ativa resolvida no servidor"]
        a_proxy["Proxy autenticado + `workersApi`"]
        a_chat["`ChatIdeaSection`"]
        a_dashboard["`HomeDashboard`"]
        a_preview["Preview e E2E"]
    end
    class a_selector,a_proxy,a_chat,a_dashboard,a_preview component

    subgraph SG_flow["Fluxo do processo"]
        b_login["Usuario seleciona uma membership explicita"]
        b_resolve["Next rele autorizacao e resolve a conta ativa"]
        b_proxy["Proxy assina o tenant resolvido pelo servidor"]
        b_chat["Chat abre sessao e consome resposta em stream"]
        b_output["Fila, carrossel final e headline permanecem em B"]
        b_switch["Troca aborta requests antigos e recarrega o app"]
        b_regen["Dashboard pede outra versao do documento em B"]
        b_cleanup["E2E revoga sessoes e remove todas as fixtures"]
    end
    class b_login,b_resolve,b_proxy,b_chat,b_output,b_switch,b_regen,b_cleanup flow

    subgraph SG_decision["Decisões"]
        c_membership["Autorizacao atual?"]
        c_action["Caller ativo?"]
        c_switch["Conta mudou durante o stream?"]
        c_accepted["Regeneracao aceita pelo worker?"]
        c_preview["Alvo e o Preview isolado no SHA esperado?"]
    end
    class c_membership,c_action,c_switch,c_accepted,c_preview decision

        title["Operacao multi-conta"]
    class title core

    subgraph SG_warning["Gotchas / Erros"]
        w_forbidden["403: membership, sessao ou documento recusado"]
        w_abort["Abortar stream antigo; nao reutilizar sessao em B"]
        w_regen["Mostrar erro e nao emitir `pipeline:job-created`"]
        w_refuse["Recusar Production ou drift de SHA antes das fixtures"]
    end
    class w_forbidden,w_abort,w_regen,w_refuse warning

    title --> b_login
    a_selector --> b_login
    b_login -->|"revalidar"| c_membership
    c_membership -->|"sim"| b_resolve
    c_membership -->|"nao"| w_forbidden
    b_resolve --> b_proxy
    a_proxy --> b_proxy
    b_proxy --> c_action
    c_action -->|"chat"| b_chat
    c_action -->|"regenerar"| b_regen
    a_chat --> b_chat
    b_chat --> c_switch
    c_switch -->|"sim"| w_abort
    c_switch -->|"nao: continuar"| b_chat
    w_abort -->|"reload"| b_switch
    a_dashboard --> b_regen
    b_regen --> c_accepted
    c_accepted -->|"sim: emitir job"| b_output
    c_accepted -->|"nao"| w_regen
    b_output -->|"validar"| c_preview
    c_preview -->|"sim"| b_cleanup
    c_preview -->|"nao"| w_refuse
    a_preview --> b_cleanup

O seletor lista apenas tenants ativos, nao arquivados, com configuracao e uma membership explicita. Trocar a conta grava cadencia_active_tenant, relido e revalidado em cada requisicao.

Chamadas aos workers passam por /api/app/workers/*. O proxy encaminha a sessao e uma assercao assinada de curta duracao. O worker compara identidade, tenant e papel com o banco antes de permitir leitura ou mutacao.

Na DEV-1845, ChatIdeaSection abre POST /chat/session e consome o SSE de POST /chat/message por workersApi. O wrapper remove Authorization fornecido pelo caller. Se a conta muda, cadencia:active-tenant-will-change aborta sessão e stream antigos antes do reload; um session_id criado em A não é reutilizável em B.

HomeDashboard envia somente content_document_id a POST /pipeline/regenerate, também por workersApi. A interface emite pipeline:job-created apenas após o aceite do worker. Fila, documento final e headline permanecem no tenant resolvido e revalidado no servidor.

Decisões técnicas

  • Hierarquia nunca concede acesso; user_tenant_roles autoriza.
  • O cookie registra preferencia, nao autoridade.
  • O browser nao envia tenant_id como credencial nem recebe o segredo.
  • O worker rele membership e arquivamento em cada requisicao.
  • O mesmo proxy atende JSON e stream SSE; responseType: "response" preserva a resposta bruta sem contornar autenticação.
  • O evento anterior ao reload serve somente para cancelamento local; não escolhe nem transporta tenant.
  • Regeneração só anuncia um job depois do aceite; rejeição não cria polling otimista.
  • Rollout e worker-first porque o Coolify nao possui auto-deploy.
  • Onboarding multi-conta pertence a DEV-1855 e nao pode ser simulado no banco.

Esses contratos estendem a arquitetura das DEV-1843 e DEV-1844. A DEV-1845 não cria ADR novo.

Gotchas & armadilhas

  • Subconta existe mas nao aparece - o provisionamento administrativo cria zero memberships.
  • Selecao volta para a mesma tela - a API pode retornar 200, mas /app devolve 307 quando o onboarding da filha esta pendente e a identidade possui varias memberships.
  • Frontend e worker divergentes - publicar o Next nao atualiza o Coolify; o worker exige deploy manual e paridade do segredo.
  • Super Admin nao equivale a Owner - o seletor comum continua dependendo de membership explicita.
  • Documentacao antiga - a story tenant-aware de onboarding e DEV-1855, nao DEV-1853.
  • Chamar /api/v1/* do browser - contorna o proxy; callers de UI usam workersApi para alcançar /api/app/workers/*.
  • Reutilizar sessão após a troca - o ID é tenant-scoped e retorna 403 SESSION_NOT_FOUND fora da conta original.
  • Sinalizar regeneração antes do aceite - cria polling fantasma; o evento pipeline:job-created pertence somente ao caminho aceito.
  • Uma credencial de Preview no lugar da outra - OPENROUTER_API_KEY atende o chat; OPENAI_API_KEY é lida pelo cliente unificado de research, seleção, headline, carousel e caption. As duas são obrigatórias.
  • Token de cleanup no runtime - DEV1845_SUPABASE_MANAGEMENT_TOKEN existe apenas no processo local do E2E; nunca entra em runtime.env ou containers.

Como operar

# Deployment do frontend
vercel inspect dpl_BxYQQ5GxUgowW2fQbZQdwVFkeG72

# Requisicoes recentes
vercel logs dpl_BxYQQ5GxUgowW2fQbZQdwVFkeG72 --since 30m --json

# Testes focados
npm test -- src/lib/supabase/tenant.test.ts src/app/api/app/active-tenant/route.test.ts

# Callers chat e regeneração
npm run test:dev1845

# Contratos do harness DEV-1845, sem rede
npm run qa:dev1845:e2e -- --self-test

O E2E real adquire o mesmo deploy.lock do deploy/rollback, recusa Production, exige SHA exato, cria fixtures com prefixo e2e-dev1845-*, dirige chat e regeneração pela UI, revoga as sessões QA e audita resíduo em todas as tabelas. As guardas de histórico impedem o cascade comum de tenant_editorials; por isso o cleanup usa a Management API somente para SQL limitado aos UUIDs da execução, restaura os triggers e então remove os tenants pelo fluxo comum.

Validação

No Preview isolado, o SHA 9e66c148131c7dc8cdfbb18b79e3abb4114c7f3a passou com chat e stream somente em A; troca durante o stream; rejeição da sessão de A em B; regeneração completa somente em B, incluindo fila, carrossel final e headline; documento de A recusado em B com 404 NOT_FOUND; membership revogada bloqueando os dois callers; sessões revogadas, inventário final zero e SHA estável.

Essa evidência vale para a Preview e para esse SHA. Não comprova deploy em produção e não autoriza merge da PR #414.

FAQ

A filha aparece automaticamente para usuarios da conta-mae? Nao. Apenas uma membership explicita concede acesso.

Por que uma subconta pendente retorna para a selecao? O onboarding legado ainda nao usa a conta ativa com seguranca. O layout bloqueia o caminho ate a DEV-1855 para evitar gravacoes no tenant errado.

A DEV-1844 termina quando o frontend e publicado? Nao. O worker precisa estar compativel, com segredo pareado, e o smoke cross-stack deve passar.

Posso marcar o onboarding como concluido para testar? Nao. Isso esconderia preparacao incompleta e deixaria a conta em estado parcial.

O browser pode enviar o UUID da conta selecionada ao worker? Não. O Next resolve o tenant usando sessão, cookie revalidado e membership atual; o worker relê a autorização antes de executar.

Por que abortar o chat se a página será recarregada? O aborto impede que um stream da conta antiga continue produzindo estado local na janela entre a seleção e a navegação.

Por que a regeneração usa OPENAI_API_KEY se o endpoint é OpenRouter? Esse é o nome lido pelo cliente unificado do pipeline. O chat lê OPENROUTER_API_KEY diretamente; uma variável não substitui a outra.

O token da Supabase Management API é necessário para o app? Não. Ele é exclusivo do cleanup do E2E.

Referencias

  • Issues: DEV-1843, DEV-1844, DEV-1845, DEV-1846 e DEV-1855.
  • Fonte tecnica: cadencia-app/docs/features/active-tenant-selection.md.
  • Contrato worker: cadencia-app/docs/features/worker-tenant-context.md.
  • Preview e cleanup: cadencia-app/docs/infra/preview-vps-dev.md.
  • Canvas fonte do diagrama: Projetos/Cadencia/operacao-multi-conta.canvas.
  • Hierarquia multi-conta.