Decisão · Arquitetura

Arquitetura

Sete decisões — e a primeira já estava tomada antes de a discussão começar: quatro regras escritas nas specs de negócio eliminam a arquitetura de microserviços sozinhas. Esta página escreve o que elas implicam (um processo, um banco, o evento sempre depois do commit), calibra o desenho para um time de uma a três pessoas e uma meta de centenas de tenants, e nomeia os sete pontos que hoje impedem o segundo processo de subir sem duplicar aviso e perder efeito. Substitui o documento de arquitetura de maio de 2026, escrito contra um escopo que já não existe.

7 decisões · 1 desenho · 7 contextos · escada de 7 degraus · 7 bloqueios calibrada para time de 1 a 3 · centenas de tenants · milhares de usuários substitui o documento de arquitetura de maio de 2026 v1.9 · 13/09/2026

Ponto de partida

O que já estava decidido, sem ninguém votar

Quatro regras foram escritas nas specs de negócio por razões que nada tinham a ver com infraestrutura. Juntas, elas decidem a fronteira do sistema — e é por isso que valem: são exigências do domínio, e o desenho que não as honra não é mais simples, é errado.

Trava 1 · Transação

Desfecho e efeito na mesma unidade de trabalho. A decisão que conclui uma instância de aprovação invoca o contrato de desfecho, e o módulo dono aplica o efeito antes do commit — aprovar requisição grava o empenho no orçamento atomicamente. Se o efeito falha, a decisão não é gravada. Em serviços separados isso vira saga com compensação sobre dinheiro. Ver Fluxo de Aprovação.

Trava 2 · Motor universal

Um motor de aprovação sobre dez processos. REQUISITION, QUOTATION, PURCHASE_ORDER, CONTRACT, SUPPLIER_REGISTRATION, SUPPLIER, BANK_ACCOUNT_CHANGE, ITEM, BUDGET e BUDGET_CHANGE passam pelo mesmo motor. Quebrar os domínios em serviços obriga ou um orquestrador distribuído, ou o motor replicado dez vezes — e dez cópias de uma regra de alçada é a definição de defeito futuro.

Trava 3 · Saldo

Razão de orçamento append-only em três estágios. O saldo é materializado por tupla concreta sobre lançamentos que nunca são reescritos. Duas aprovações simultâneas sobre as mesmas linhas de orçamento candidatas precisam se enxergar — e a consequência do erro é verba estourada sem ninguém ter aprovado o estouro. Isso exige travar as linhas candidatas que podem recusar ou exigir aprovação (Block, RequireApproval), no mesmo banco onde o lançamento é gravado. Ver Orçamento.

Trava 4 · Recorte

Três mecanismos por contexto de persistência. Filtro global de tenant, exclusão lógica e resolução do UserCompanyScope a cada requisição são mecânica por DbContext, não por serviço — e o Nexio tem um contexto de persistência só (Trava 1), então nenhum deles se escreve duas vezes. O filtro de tenant vem do framework; a exclusão lógica fora do GrydAuth ainda não vem, e está na demanda de plataforma; o recorte por empresa é do Nexio — e ele falharia aberto se a guarda não rodasse: o dado de outra empresa apareceria. Por isso ele falha fechado por regra: resolução que não roda recusa a requisição (RN-EMP-25 da Empresa), toda entidade com empresa entra no filtro (asserção 7), e o recorte espelha as garantias da leitura de grupo do framework — conjunto vazio é nada, escrita sob escopo amplo é recusada.

Desenho

Como isso fica de pé

O mesmo código-fonte, quatro perfis de execução, um sistema de registro. Tudo à direita é gerenciado — não há máquina, fila ou banco para o time operar. O que ainda não existe está tracejado.

quem usa AWS · sa-east-1 borda VPC ECS Fargate dados gerenciados Navegador SPA do tenant 1 Fornecedor sem conta do tenant 2 CloudFront + S3 SPA e portal, estáticos 3 ALB + WAF entrada única 4 API n réplicas · rotas do tenant 5 Worker mensagens · jobs · aviso · varredura 6 Portal rotas públicas do fornecedor 7 RDS PostgreSQL primário · registro 8 Réplica de leitura degrau 2 · não existe 9 ElastiCache Redis cache · trava 10 Mensageria estudo da plataforma 11 S3 arquivo · URL assinada 12 Secrets Manager credencial e rotação 13 CloudWatch + OTel rastro por tenant 14 ECR + Actions imagem e entrega 15 GrydAuth módulo em processo 16 Migration tarefa do pipeline 17
Legenda
#ComponenteO que faz, e por que está aí
1NavegadorAplicação única do tenant. Envia anexo direto ao armazenamento com URL pré-assinada — o byte do arquivo nunca passa pela API.
2FornecedorNão tem conta no tenant. A plataforma não tem o conceito de usuário externo, e é por isso que o Portal é perfil próprio. Ver Portal do Fornecedor.
3CloudFront + S3As duas aplicações de tela são estáticas. Nada de servidor de frontend.
4ALB + WAFEntrada única. O firewall de aplicação existe por causa do Portal, a única superfície aberta à internet.
5APISem estado, n réplicas. Escala com requisição. Enfileira job, mas não executa job nem consome fila.
6WorkerEntrega de mensagens, jobs agendados, fila de aviso e varredura de arquivo. É o segundo processo — e é ele que torna os sete bloqueios urgentes.
7PortalSó as rotas públicas do fornecedor. Mesmo banco, mesmo domínio, host separado.
8RDS PostgreSQLPrimário. Toda invariante mora aqui, e é o único lugar onde a Trava 1 e a Trava 3 se sustentam.
9Réplica de leituraAinda não existe. É o degrau 2 da escada de escala, e o desenho já reserva o lugar dela para que a decisão de roteamento seja tomada antes, não depois.
10ElastiCache RedisCache, trava distribuída e lista de revogação de token — que o GrydAuth já guarda no cache distribuído quando configurado. Nunca guarda verdade.
11MensageriaA definir pelo estudo da plataforma. O que ela precisa garantir — entrega pelo menos uma vez, tenant restaurado, ordem só onde muda o resultado, nada descartado em silêncio — está na página de Mensageria, com critério de aceite. Tracejada porque ainda não existe.
12S3Arquivo. O provedor já vem pronto no framework, com URL pré-assinada. Ver GrydFiles.
13Secrets ManagerCredencial e rotação sem redeploy.
14CloudWatch + OTelO framework já emite rastro com identificador de tenant e de usuário. É ligar, não construir.
15ECR + ActionsImagem e entrega. O código e os pacotes do framework já vivem no GitHub.
16GrydAuthMódulo do framework carregado dentro dos perfis, não serviço à parte: a validação do token confere a versão dele no banco, as guardas de permissão e de escopo rodam no pipeline de cada requisição, e o modelo de projeto do framework faz o contexto de persistência da aplicação herdar o do GrydAuth. Desenhado à parte só para marcar o dono. Conferido no código do Gryd.IO, commit e4eb5a4f.
17MigrationÉ o bloqueio 7. O modelo de projeto do framework só migra no arranque em desenvolvimento; em produção a migração é tarefa do pipeline, executada uma vez antes de trocar as réplicas.

Três coisas que o desenho torna visíveis e a prosa escondia. Primeiro: não há salto de rede entre módulos — nem para o GrydAuth, que roda no mesmo processo. Segundo: a fatura no começo é dominada por banco e balanceador, porque os quatro perfis são contêineres pequenos. Terceiro: o que separa o desenho de hoje do desenho da meta são três caixas tracejadas — a réplica de leitura, o migrador fora do arranque e a mensageria, que espera o estudo da plataforma. Não é uma arquitetura diferente; é a mesma, com dois degraus subidos e um requisito atendido.

ADR 1 · Estilo

Monolito modular, com fronteira compilada

Decisão. O backend do Nexio é uma aplicação, organizada por bounded context em projetos separados, cada contexto referenciando apenas o contrato publicado pelos outros — nunca a entidade. A escala horizontal vem de réplicas sem estado atrás de um balanceador, não de fatiar o domínio.

O argumento não é preferência por monolito; é que as quatro travas acima já eliminaram a alternativa. Um serviço de aprovação separado do orçamento transformaria a regra mais central do produto — desfecho e efeito na mesma unidade de trabalho — numa saga com compensação sobre lançamento financeiro. E as três garantias de recorte teriam que ser reimplementadas em cada serviço, sendo que uma delas falharia aberto se esquecida.

A carga não pede outra coisa. Microserviço resolve problema de time e de ciclo de deploy, não problema de requisição por segundo — quem resolve carga é a réplica, e a réplica é a mesma imagem rodando n vezes. Com um time de uma a três pessoas, cada serviço a mais é um pipeline, um alerta, um esquema, uma versão de contrato e um modo de falha novo — pagos todo mês, começando no primeiro mês.

O próprio Gryd.IO já pratica essa fronteira: módulo por pasta, DbContext e migrations próprios, e referência cruzada sempre para o .Core do outro módulo. O Nexio copia o padrão em vez de inventar.

As três regras da fronteira

Um projeto por contexto

Organização · Catálogo · Fornecimento · Orçamento · Aprovação · Espinha transacional · Transversais. A fronteira é referência de projeto, não convenção de pasta: o compilador recusa o atalho.

Nenhuma chave estrangeira cruza contexto

Já é regra do produto. Referência entre contextos é id, sem navegação do ORM e sem join no banco — o que mantém a porta de extração aberta sem custo hoje.

A fronteira é testada, não confiada

Um teste de arquitetura no CI reprova referência proibida entre projetos. Sem isso, a fronteira dura até a primeira sexta-feira apertada — e a extração futura vira reescrita em vez de mudança de host.

Descartado: microserviços por domínio — saga sobre dinheiro, motor de aprovação replicado dez vezes, três mecanismos de recorte reimplementados por serviço, com um time de três pessoas. Monolito sem fronteira compilada — barato hoje e caríssimo no dia da extração, porque a fronteira que ninguém verifica não existe.

ADR 1 · continuação

Organização do backend

O documento de maio organizava o código por camada primeiro — um projeto de domínio com uma pasta por contexto. Isso não sobrevive ao ADR 1: se todos os contextos moram no mesmo projeto, o compilador não recusa nada, e “fronteira compilada” vira convenção. O eixo primário inverte: contexto primeiro, camada dentro.

Nexio.sln
├─ src/
│  ├─ hosts/
│  │   ├─ Nexio.Api             ← perfil API
│  │   ├─ Nexio.Worker          ← perfil Worker
│  │   ├─ Nexio.Portal          ← perfil Portal
│  │   └─ Nexio.Migrator        ← perfil Migration
│  │
│  ├─ shared/
│  │   ├─ Nexio.SharedKernel    ← Money, TaxId, ids tipados, enums que cruzam,
│  │   │                          sufixos de erro, a regra de resíduo
│  │   ├─ Nexio.Persistence     ← NexioDbContext, configurações, repositórios,
│  │   │                          migrations, interceptors
│  │   └─ Nexio.Platform        ← adaptadores: armazenamento, cache, relógio,
│  │                              cotação do BCB
│  │
│  └─ contexts/
│      ├─ Nexio.Organization.Contracts   +  Nexio.Organization.Core
│      ├─ Nexio.Catalog.Contracts        +  Nexio.Catalog.Core
│      ├─ Nexio.Sourcing.Contracts       +  Nexio.Sourcing.Core
│      ├─ Nexio.Budget.Contracts         +  Nexio.Budget.Core
│      ├─ Nexio.Approval.Contracts       +  Nexio.Approval.Core
│      ├─ Nexio.Procurement.Contracts    +  Nexio.Procurement.Core
│      └─ Nexio.Reference.Contracts      +  Nexio.Reference.Core
└─ tests/
    ├─ Nexio.ArchitectureTests
    ├─ Nexio.<Contexto>.Tests
    └─ Nexio.IntegrationTests

Os sete contextos

ContextoAgregadosEstado
OrganizationCompany, Establishment, GLAccount, GLAccountCompany, NumberSequence, CostCenter + CostCenterAssignment, DeliveryLocation, UserCompanyScopeespecificado
CatalogItemType, CategoryNode, Item, UnitOfMeasureespecificado
SourcingSupplier, SupplierEstablishment, ItemSupplier, SupplierQualificationespecificado
BudgetBudget e o razão em três estágiosespecificado
Approvalas onze do motor — sete de configuração, quatro de execuçãoespecificado
ProcurementRequisition, QuotationRequest, PurchaseOrder, Receipt, Invoice, Contracta especificar
ReferenceCurrency e câmbio, Attachment + AttachmentType, preferência e represamento de aviso, ReasonCodeespecificado

Dois projetos por contexto — e por que não quatro

O compilador protege o que as pessoas cruzam sem perceber e o que é caro desfazer. Cruzar contexto é as duas coisas: acontece por descuido e inviabiliza o degrau 4 da escala — por isso é projeto, e a referência só pode ser para .Contracts. Cruzar camada dentro de um contexto não é nenhuma das duas: aparece em revisão e conserta-se em minutos — por isso é namespace mais teste.

Quatro projetos por contexto dariam vinte e oito projetos só de domínio, para um time de três. Clean Architecture é sobre direção da dependência, não sobre contagem de projetos, e a direção continua garantida — só que verificada por teste em vez de por referência. Dentro de um .Core:

Nexio.Catalog.Core/
├─ Domain/
│   ├─ Items/        Item.cs · ItemStatus.cs · Events/ItemApproved.cs
│   ├─ Categories/   CategoryNode.cs
│   ├─ Uom/          UnitOfMeasure.cs · UomConversion.cs
│   └─ Ports/        IItemRepository.cs · ICategoryRepository.cs
├─ Application/
│   ├─ Items/        Commands · Queries · Handlers · Validators · Dtos
│   ├─ Outcomes/     ItemApprovalOutcome.cs      ← contrato de desfecho
│   └─ Permissions/  CatalogPermissions.cs
└─ CatalogModule.cs  ← AddCatalog(this IServiceCollection)

Gatilho para subir para quatro projetos: um contexto ganhar dono próprio, ou o teste de arquitetura reprovar a mesma regra repetidamente. Aí a separação deixa de ser cerimônia e passa a ser proteção — e o caminho é o que o próprio framework já usa nos módulos dele.

Um banco, um contexto de persistência — a consequência que não foi escolha

A Trava 1 exige que aprovação e orçamento gravem na mesma transação; logo, um contexto de persistência só; logo, ele precisa conhecer a configuração de todas as entidades. Por isso não existe Infrastructure por contexto — existe uma, organizada por pasta.

Nexio.Persistence/
├─ NexioDbContext.cs          ← evento de domínio pelo mecanismo da plataforma (bloqueio 4)
├─ Interceptors/              ← só o que o framework não dá; tenant, exclusão lógica e autor vêm dele (os dois últimos, demanda de plataforma)
├─ Configurations/<Contexto>/ ← uma configuração por entidade
├─ Repositories/<Contexto>/   ← implementa as Ports do .Core
└─ Migrations/

Nexio.Persistence referencia os .Core; nenhum .Core referencia a persistência. A porta fica no domínio, a implementação aqui, a fiação no host — a inversão de dependência continua de pé.

É o único lugar onde o compilador não protege a fronteira. A persistência enxerga todas as entidades, então alguém pode escrever uma junção entre contextos. Por isso duas das dez asserções de arquitetura são especificamente sobre ela. É o preço de ter uma transação só — e o preço vale.

Os quatro hosts

HostRegistraNão registraRéplicasLeitura sem tenant
Nexio.ApiOs serviços dos módulos do framework · Add<Contexto>() dos sete · controllers do tenant · só os endpoints da lista branca (asserção 8): os do Nexio; do GrydAuth.API, só as actions que o produto usa — autenticação, usuários, papéis, permissões, preferências e o detalhe do próprio tenant; e, do GrydNotifications.API, a caixa in-app e o texto por tenantexecução de job, agendador, consumidor de fila, toda parte de aplicação fora da lista branca — inclusive a que chega por referência transitiva, como o GrydAudit.API pelo GrydAuth —, as actions de administração de tenants e o disjuntor do GrydAuth.API, GrydFiles.API, painel e controllers do GrydJobs, hub de notificação (asserções 5 e 8)nzero linhas · teste de fumaça
Nexio.WorkerEntrega de mensagens · jobs agendados · fila de aviso · varredura de arquivo · consumidorescontroller e endpoint sem controller (painel, hub), exceto verificação de saúde1 na v1; n sem mudança de códigozero linhas · teste de fumaça dentro de um job e de um consumidor
Nexio.PortalAutenticação por token do fornecedor · só os contextos que usa · só as rotas públicasautenticação de tenant, contextos que não usa, controller e endpoint de módulo do frameworknzero linhas · teste de fumaça
Nexio.MigratorMigração · reconciliação idempotente das permissões do produto em todo tenant (a semeadura em tenant novo é demanda de plataforma)tudo o maistarefavarre tenant a tenant, com o filtro desligado de forma explícita

Leitura sem tenant devolve zero linhas em todo host do Nexio. Com a configuração padrão, o filtro de tenant do framework devolve as linhas de todos os tenants quando não há tenant em vigor, e em consumidor e job esse ramo é alcançado (TenantQueryFilterExtensions.cs:142 para entidade de tenant e :170 para entidade de tenant opcional, commit e4eb5a4f). Tornar isso o padrão da plataforma é requisito da página de Mensageria; até lá vale o regime provisório do Nexio, em três partes. A exigência de tenant na leitura vale por fluxo de execução, não por host: a opção é gravada no fluxo do arranque, sobre estado assíncrono local (MultiTenancyServiceCollectionExtensions.cs:48, TenantDataFilterAccessor.cs:28), e job ou consumidor que roda em outro fluxo pode encontrá-la desligada. Por isso cada host a liga no arranque e todo job e todo consumidor do Nexio a liga de novo ao abrir o escopo do tenant — ligar é escrever no estado do filtro, porque o contrato do framework só oferece desligar (ITenantDataFilter tem IsEnabled e Disable(), ITenantDataFilter.cs:31), e é assim que o próprio framework faz (TenantDataFilterAccessor.RequireTenantOnRead = true em DurableNotificationProcessor.cs:54 e DurableNotificationMaintenance.cs:46, commit e4eb5a4f); um único ponto do Nexio escreve nesse estado, e só com o valor true — o TenantExecutionScope do Nexio.Platform, o auxiliar que abre o escopo do tenant do job ou do consumidor —, e qualquer outra referência ao acessor, Reset() inclusive, quebra o build (asserção 9). E o teste de fumaça — entidade de tenant lida sem tenant em vigor, com dois tenants populados, afirmando zero linhas — roda numa requisição na API e no Portal e, no Worker, dentro de um job e dentro de um consumidor. O código do Nexio não desliga o filtro nem alarga a leitura por fora dele: o framework oferece cinco caminhos — o desligamento explícito, a escrita no estado do filtro, a consulta que ignora os filtros, o pedido que se declara entre tenants e o escopo de grupo, que troca o predicado de um tenant por um conjunto deles —, e fora dos cinco está o SQL cru, que não passa por filtro global nenhum; a asserção 9 reprova todos fora do Migrator — a exceção declarada, que varre tenant a tenant e desliga o filtro de forma explícita. Jobs de módulo do framework são exceção declarada, não conformidade: a varredura e as rotinas do GrydFiles desligam o filtro na execução inteira e leem com a consulta que ignora os filtros (FileScanJob.cs:107, :122, commit e4eb5a4f), fora do alcance da asserção. O envio agendado do GrydReports não é exceção: desliga o filtro só para ler o agendamento, que é quem nomeia o tenant, e executa o relatório com o tenant dele (ScheduledReportJob.cs:107–119 e :90–91) — é o padrão que a demanda de plataforma pede aos outros. A correção deles é da demanda de plataforma — leitura de arquivo sem tenant falha fechada, e job que atravessa tenants trata cada registro no tenant dele — e é exigida antes da primeira Requisição com anexo.

Cada contexto expõe um ponto de composição — AddCatalog(), AddBudget() — e é isso que faz o arranque de um host ser cinco linhas em vez de trezentas. É o padrão que o framework já usa nos AddGryd*. Os controllers ficam no host, em pasta por contexto: o host é o único que precisa saber que existe HTTP. Gatilho para movê-los para um projeto por contexto: duas pessoas mexendo em endpoints diferentes e colidindo.

Regras de camada

ProjetoPode referenciar
*.ContractsNexio.SharedKernel. Só. Sem entidade, sem ORM, sem barramento de mensagem.
*.Core · Domain/SharedKernel e o .Contracts do próprio contexto
*.Core · Application/o próprio Domain, o .Contracts dos outros, e as abstrações de aplicação do framework
Nexio.Persistencetodos os .Core e a infraestrutura do framework
Nexio.Platform.Contracts e SDKs externos
hoststudo — é a raiz de composição

O contrato de desfecho, ou SOLID deixando de ser slogan

É o exemplo que vale por todos, porque é o que faz a Trava 1 funcionar sem o motor conhecer domínio. O contexto de aprovação publica, no seu .Contracts, uma interface de desfecho identificada pelo código do processo. Cada módulo dono implementa a sua no próprio .Core e registra. O motor resolve pelo código e invoca dentro da transação.

OCP · aberto para extensão

Décimo primeiro processo de aprovação = uma implementação nova registrada. Zero linha no motor. É a diferença entre um motor universal e dez motores parecidos — e é o que a decisão do motor único comprou.

DIP · dependência invertida

O motor depende da abstração; quem sabe o que fazer com o desfecho é o módulo dono. É por isso que Approval não referencia Budget nem Procurement — e é o que mantém o grafo de dependências sem ciclo.

ISP · interface segregada

.Contracts é a segregação de interface tornada física: um contexto publica exatamente o que os outros precisam, e o compilador recusa o resto. Não é disciplina, é referência de projeto.

SRP e LSP · o pipeline

Um comando por caso de uso, um handler por comando. Todos implementam os mesmos contratos de aplicação, então validação, trilha e recorte de tenant entram como behaviors — sem decorador manual e sem tocar em handler existente.

DRY — e onde ele para

Não se duplica: o motor de aprovação, que é universal e mora em um lugar; a regra de resíduo, com uma implementação única no núcleo compartilhado; a conversão de moeda, que acontece uma vez, na submissão; filtro de tenant e mapeamento de erro, que vêm do framework; listagem — página, ordenação e busca, do GrydCrud — e relatório com exportação e envio agendado, do GrydReports, que se reusam em vez de ser reescritos em cada cadastro; e os nomes de permissão, num arquivo de constantes por contexto de onde sai a reconciliação idempotente do Nexio.Migrator. Ainda não vêm do framework, e estão na demanda de plataforma: exclusão lógica por remoção, autor da alteração e unidade de trabalho fora do GrydAuth, e a trilha de ato de controle isolada por tenant.

Duplica-se de propósito: o dado que cruza fronteira. Se a espinha transacional precisa do nome do fornecedor, o contexto de fornecimento publica um resumo próprio — não se compartilha a mesma classe entre contextos, mesmo quando os campos são idênticos. Acoplamento entre contextos custa mais caro do que três propriedades repetidas, e é exatamente o que impede o degrau 4 quando ele chegar.

As dez asserções de arquitetura

Sem isto o ADR 1 é decoração: a fronteira que ninguém verifica dura até a primeira sexta-feira apertada. Todas são baratas e rodam no CI — nove por análise estática dos assemblies, e a décima com uma segunda metade sobre o contêiner montado.

1 · Contexto não enxerga contexto

Nenhum .Core referencia o .Core de outro contexto — só .Contracts. É a asserção que sustenta todas as outras, e é ela que torna a extração futura um problema de implantação em vez de um refactor.

2 · Domínio não conhece infraestrutura

Tipos sob Domain/ não dependem de ORM, de barramento de mensagem nem de HTTP. É esta asserção que substitui o projeto separado — e é por isso que a economia de projetos não custa a Clean Architecture.

3 · Nenhuma navegação entre contextos

Verificada no modelo do ORM, não no código: nenhuma entidade tem propriedade de navegação nem chave estrangeira para entidade de outro contexto. É a única defesa da fronteira dentro da persistência, e é ela que dá o degrau 4 de graça lá na frente.

4 · Nenhum .Core referencia a persistência

A porta é declarada no domínio e implementada fora. Quebrar isto inverte a dependência sem ninguém perceber, porque o código continua compilando e os testes continuam passando.

5 · Perfil não invade perfil

O Worker não expõe controller nem endpoint sem controller, exceto a verificação de saúde; a API não executa job nem agendador — enfileirar pode. Nenhum host monta middleware que abre rota fora da lista branca da asserção 8. O painel do GrydJobs é middleware, não controller (UseGrydJobsDashboard, que a documentação do framework manda chamar — docs/modules/jobs/getting-started.md:97), só confere autenticação (GrydJobsDashboardAuthFilter.cs:19, commit e4eb5a4f) e mostra e reenfileira os jobs de todos os tenants, com o tenantId e o onBehalfOfUserId do payload à vista. Critério: a rota padrão do painel (/grydjobs/dashboard) responde 404 em todo host do Nexio, e o teste de fumaça de cada perfil confere. E, porque o caminho é parâmetro (GrydJobs.API/ServiceCollectionExtensions.cs:75–93, pathMatch com valor padrão) e o painel é middleware fora do roteamento, montá-lo em outro caminho — UseGrydJobsDashboard("/ops") — passaria nos quatro critérios da asserção 8: por isso o teste reprova o painel em qualquer assembly do Nexio, qualquer que seja o caminho — e não por uma lista de três nomes de método: o Hangfire.AspNetCore fixado em 1.8.23 (Directory.Packages.props:153, commit e4eb5a4f) expõe, ao lado do MapHangfireDashboard, o MapHangfireDashboardWithAuthorizationPolicy e o MapHangfireDashboardWithNoAuthorizationFilters (validação R6), e nome novo entra a cada versão do pacote — enumerar métodos é perder a corrida. O próprio UseGrydJobsDashboard mostra por que a lista não segura: ele chama o painel pelo nome inteiro do tipo, Hangfire.HangfireApplicationBuilderExtensions.UseHangfireDashboard(app, pathMatch, …) (GrydJobs.API/ServiceCollectionExtensions.cs:81). O que se reprova são os tipos e o namespace do painel: o UseGrydJobsDashboard e o que o GrydJobs.API exporta para montá-lo, e todo tipo do painel do Hangfire.AspNetCore — as opções, os filtros de autorização e os métodos de montagem que os recebem. Enquanto o framework não oferece registrar jobs só para enfileirar (demanda de plataforma), a asserção se verifica pelo efeito: o teste de fumaça do perfil API prova que nenhum job executa ali. É o bloqueio 2 impedido de voltar — e ele volta sozinho, na primeira vez que alguém registrar um job “só para testar”.

6 · .Contracts é contrato

Todo tipo publicado é imutável e sem dependência de infraestrutura. No dia em que virar contrato de rede, ele já é serializável — e ninguém precisa descobrir que publicou uma entidade por engano.

7 · O recorte por empresa fecha por construção

Toda entidade com companyId ou establishmentId está registrada no filtro de escopo por empresa; entidade nova sem registro quebra o build. É a mitigação da Trava 4, ao lado da regra de que a resolução que não roda recusa a requisição.

8 · Lista branca de endpoints por host

Cada host declara a lista branca do que pode expor, e o teste monta o host e lê os endpoints efetivos — tudo o que o roteamento registrou depois do arranque, com controller ou sem: rota de controller, hub, painel e rota aberta por middleware —, reprovando todo endpoint fora da lista. Deixar de referenciar não exclui nada: módulo do framework arrasta a parte de aplicação de outro módulo por referência transitiva — o projeto de infraestrutura do GrydAuth referencia o pacote agregador do GrydAudit (GrydAuth.Infrastructure.csproj:20), que referencia o projeto de controllers dele (GrydAudit.csproj:15), e o contexto de persistência do Nexio herda o do GrydAuth, então todo host recebe o AuditLogsController sem declarar nada (commit e4eb5a4f) —, e o registro de um módulo acrescenta ao host a parte de aplicação dele. Comparar a lista declarada também não basta, e prefixo de rota não serve: a rota real do GrydFiles é versionada (api/v{version}/files). Regra do Nexio: o host retira toda parte de aplicação fora da lista branca, inclusive a que chegou por referência transitiva ou pelo registro do módulo, e não monta endpoint sem controller que a lista não nomeie.

A lista branca, por host — e ela nomeia actions, não assemblies. A razão é uma só, e não muda: o host só expõe o que o produto usa. Nexio.Api: os endpoints do Nexio; do GrydAuth.API, só as actions que o produto usa — autenticação e MFA, usuários, papéis, permissões, preferências, diretório de usuários e o detalhe do próprio tenant (GET /tenants/{id}, TenantsController.cs:69), cujo handler exige vínculo ativo do chamador e responde 404 anti-disclosure —; e, do GrydNotifications.API, só a caixa in-app (UserNotificationsController) e o texto do aviso por tenant (NotificationTemplatesController), que a página de Notificação usa. Do GrydAuth.API ficam de fora as actions de administração de tenants — a lista (TenantsController.cs:38), a busca por domínio (:101), as estatísticas (:129), os filhos e a hierarquia (:154, :178), os usuários do tenant (:448) e toda escrita de tenant e de vínculo (:202 a :539) — e o disjuntor inteiro (CircuitBreakerController), porque o Nexio não tem tela para nenhuma delas: administração de tenant é trilho de plataforma e o disjuntor é operação. Que cinco dessas actions devolvam hoje dados de qualquer tenant a um Admin comum é defeito da plataforma, registrado na demanda de plataforma — a lista branca não existe por causa dele, não o compensa e não muda quando ele for corrigido. Nexio.Portal: as rotas do próprio portal, nenhuma de módulo do framework. Nexio.Worker: a verificação de saúde. Fora de todo host: GrydAudit.API, que expõe a trilha a qualquer usuário autenticado enquanto o GrydAudit não isola tenant (AuditLogsController.cs:17, só autenticação); GrydFiles.API, que deixaria a leitura contornar o CanRead do dono do anexo — o arquivo entra no Nexio só pelo processo, e o dono declara a referência esperada ao confirmar; GrydJobs.API inteiro, controllers e painel, que só conferem autenticação e alcançam os jobs de todos os tenants (asserção 5); GrydReports.API, porque o Nexio usa o relatório em processo; o envio avulso do GrydNotifications.API (NotificationsController), porque a Notificação não tem tela de envio e é por ele que um fileId alheio sai por e-mail (demanda de plataforma); e o hub de notificação, endpoint sem controller (MapHub, GrydNotifications.API/ServiceCollectionExtensions.cs:77) — a caixa in-app se lê pela rota, e entrega em tempo real entra por revisão desta página. Controller ou endpoint novo de módulo do framework fica fora até entrar na lista, e entrar nela é revisão desta página. Critérios: (a) host só com GrydAuth.Infrastructure referenciado → zero endpoints de GrydAudit.API; (b) host de teste que monta o AuditLogsController por referência transitiva sem a retirada → o teste encontra o endpoint e reprova; (c) rota padrão do painel do GrydJobs → 404 em todo host do Nexio, e nenhuma referência aos tipos do painel em assembly do Nexio (asserção 5); (d) hub montado sem estar na lista → o teste reprova; (e) chamada a uma action de administração de tenants ou ao disjuntor do GrydAuth.API em host do Nexio → 404, e o endpoint não aparece na leitura dos endpoints efetivos. Que upload:files, read:files, retain:files e purge:files não possam ir a papel de tenant é requisito do catálogo da plataforma — está na demanda de plataforma, não neste teste.

9 · O filtro de tenant só se desliga no Migrator

Nenhum código do Nexio fora do Nexio.Migrator desliga o filtro de tenant ou a exigência de tenant na leitura, por nenhum dos cinco caminhos que o framework oferece: (1) o desligamento explícito (ITenantDataFilter.Disable(), ITenantDataFilter.cs:31); (2) a escrita direta no estado do filtro que afrouxa a leitura — o sentido importa, e é só este que se proíbe: ligar a exigência de tenant (TenantDataFilterAccessor.RequireTenantOnRead = true) é o que todo job e todo consumidor precisa fazer, porque o contrato só oferece Disable(), e é o que o próprio framework faz (DurableNotificationProcessor.cs:54, DurableNotificationMaintenance.cs:46); por isso a asserção admite um único ponto do Nexio escrevendo nesse estado, e só com o valor true — e esse ponto tem nome, porque teste estático não reprova “fora do ponto único” sem saber qual é ele: é o TenantExecutionScope, no Nexio.Platform, o auxiliar que abre o escopo do tenant do job ou do consumidor, e é aí que a exigência de tenant se liga. O tipo é do Nexio e o nome é declarado aqui: o framework não oferece auxiliar nenhum para isso — cada módulo dele escreve o par “ligar o filtro, exigir tenant” à mão (DurableNotificationProcessor.cs:51–54, commit e4eb5a4f) —, e um teste que reprove por posição precisa de um nome para comparar. E o que se reprova é a referência, não só a escrita: o acessor também expõe Reset() (TenantDataFilterAccessor.cs:47–52), que zera o estado e devolve a exigência de tenant ao padrão false (:30, ?? false) — não é Disable(), não é escrita, não é IgnoreQueryFilters(), não é pedido que se declara entre tenants, não é escopo de grupo e não é SQL cru, de modo que um finally { …Reset(); } num job do Nexio devolveria a leitura ao fail-open com o build verde. Por isso a asserção reprova toda referência ao TenantDataFilterAccessor fora do ponto único — leitura, escrita, Reset() e o que o tipo vier a expor —, e no ponto único admite só a escrita com true; (3) a consulta que ignora os filtros (IgnoreQueryFilters()), que derruba de uma vez o de tenant, o de empresa e o de exclusão lógica — e que o próprio framework usa (FileScanJob.cs:122); (4) o pedido que se declara entre tenants (ICrossTenantRequest, ICrossTenantAuthorizedBy): o comportamento do pipeline do GrydAuth desliga o filtro sozinho quando o chamador tem a permissão que o pedido nomeia (ICrossTenantAuthorizedBy.cs:14; CrossTenantDataFilterBehavior, registrado em GrydAuth.Application/ApplicationServiceRegistration.cs:52, desliga nas l.106–124), sem uma linha de desligamento no código do Nexio; e (5) o escopo de grupo — IGroupScopableQuery com o TenantScopeBehavior (registrado em GrydAuth.Application/ApplicationServiceRegistration.cs:58) —, que não desliga o filtro mas troca o predicado de um tenant por um conjunto deles (TenantScopeApplier.cs:27, TenantQueryFilterExtensions.cs:139–143): falha fechada, e ainda assim leitura de mais de um tenant sem nenhum dos quatro caminhos acima. E a interface não é a única porta desse quinto caminho: o ITenantScopeApplier é serviço registrado (MultiTenancyServiceCollectionExtensions.cs:67) e o TenantContextAccessor.ScopeTenantIds tem setter público estático (:75–78) — um tipo do Nexio que peça o applier ao contêiner, ou escreva no acessor, lê N tenants sem implementar IGroupScopableQuery, que era a única coisa que a asserção reprovava aqui — e o próprio framework diz que é defeito: o acessor documenta que o único escritor é o ITenantScopeApplier, movido pelo TenantScopeBehavior, e que escrever de qualquer outro lugar “bypasses the entitlement check and is a defect” (TenantContextAccessor.cs:72–73). Documentação não quebra build. Por isso ela reprova também o uso do applier e a escrita no ScopeTenantIds, além da interface. E o contêiner não é a única porta do applier: o TenantScopeApplier é public sealed class de construtor público (TenantScopeApplier.cs:13) e TenantScope.Group(…) é fábrica pública (TenantScope.cs:137) — um tipo do Nexio que faça new TenantScopeApplier().Apply(TenantScope.Group(…)) não pede nada ao contêiner, não nomeia a interface e não escreve no acessor (quem escreve é o próprio applier, :27, dentro do Gryd.Infrastructure), não implementa IGroupScopableQuery e lê N tenants com o build verde — e o Apply não confere direito nenhum: quem resolveu o conjunto foi o TenantScopeBehavior, que esse caminho pula. Por isso o alvo aqui é a referência, como no caminho (2): reprovam-se toda referência ao tipo concreto TenantScopeApplier nos assemblies do Nexio e toda fábrica de TenantScope que não seja Single — as duas que existem são Single (TenantScope.cs:106) e Group (:137), e fábrica nova do framework entra reprovada até revisão desta página. Fora dos cinco fica o SQL cru (Database.SqlQuery, ExecuteSql), que não passa por filtro global nenhum. Commit e4eb5a4f. O teste varre os assemblies do Nexio e reprova as chamadas de (1) e (3), toda referência ao TenantDataFilterAccessor fora do ponto único de (2), e nele toda escrita cujo valor não seja true, todo tipo que implemente as interfaces de (4) ou de (5), todo uso do ITenantScopeApplier, toda referência ao tipo concreto TenantScopeApplier, toda fábrica de TenantScope que não seja Single e toda escrita em TenantContextAccessor.ScopeTenantIds, e todo SQL cru sobre tabela de tenant sem predicado de tenant conferido por teste. Desligar o filtro falha aberto: com ele desligado, o código que chamou é a única coisa entre o chamador e os dados de todos os tenants. As exceções são declaradas e nomeadas uma a uma, e exceção nova é revisão desta página: os jobs de módulo do framework que desligam o filtro na própria execução, até a demanda de plataforma de leitura de arquivo sem tenant ser entregue; e o IServerFileStore.OpenReadAsync, que hoje atende o tenant do parâmetro com IgnoreQueryFilters() sem compará-lo com o que está em vigor (EfStoredFileStore.cs:177–186, chamado pelo GrydFilesEmailAttachmentReader.cs:34) — é o caminho do e-mail com anexo, está na demanda de plataforma e sai desta lista quando o tenant nomeado passar a ser conferido. Critério: job ou consumidor do Worker que desliga o filtro, consulta do Nexio com IgnoreQueryFilters(), qualquer referência ao TenantDataFilterAccessor fora do TenantExecutionScope — Reset() inclusive — ou escrita nele com valor diferente de true, comando do Nexio que implementa ICrossTenantAuthorizedBy ou IGroupScopableQuery, tipo do Nexio que peça o ITenantScopeApplier ao contêiner, que referencie o tipo concreto TenantScopeApplier, que chame fábrica de TenantScope diferente de Single ou que escreva em TenantContextAccessor.ScopeTenantIds, e SQL cru sobre tabela de tenant sem predicado de tenant — cada um quebra o build.

10 · Quem resolve a porta de cada escopo é o consumidor declarado

O Nexio é consumidor registrado do módulo de arquivo, e em processo o módulo não confere permissão: o que autoriza é o consumidor registrado para o escopo, o ato e o autor declarado (RN-GF-28 do GrydFiles). Quem registra escopo repete a asserção no próprio repositório — e é esta. Sem ela, qualquer assembly do processo que alcance a porta de nexio.attachment retira a guarda de um anexo ou pede a eliminação dele, e não há permissão nenhuma para barrar.

Os consumidores declarados, e as duas portas. A porta amarrada ao escopo nexio.attachment — reservar, confirmar, declarar referência esperada, ligar e retirar guarda, pedir eliminação — é resolvida só pelo adaptador de arquivo do Nexio.Platform, o único assembly do Nexio registrado para esse escopo; e a porta de conteúdo no servidor (IServerFileStore, RN-GF-06 — por onde o job lê a planilha de importação já liberada) é referenciada só por ele. Nenhum .Core, nenhum host e nenhum outro adaptador resolve a porta do escopo nem referencia a porta de conteúdo: o domínio declara a sua porta e o adaptador a implementa (asserção 4). Consumidor novo é revisão desta página, como a lista branca da asserção 8.

O alvo é quem resolve a porta, não quem passa o texto. Escopo é constante de compilação: o compilador a embute em cada chamador, e nenhum teste de dependência entre assemblies vê quem passou nexio.attachment — a asserção “só o consumidor registrado passa o escopo” é inverificável enquanto o escopo for argumento. Por isso o escopo sai das assinaturas e a porta se resolve por um tipo-marca do próprio consumidor: um tipo que mora no assembly registrado para aquele escopo, de modo que resolver a porta obrigue a referenciar esse assembly e a dependência apareça. Como a plataforma amarra o tipo-marca ao registro é decisão dela, e o pedido está na demanda de plataforma.

E o teste estático sozinho não fecha. Chave de texto, Type montado em execução e reflexão (IServiceProvider.GetService(Type)) alcançam a porta sem deixar dependência entre assemblies — é o mesmo defeito da constante embutida, um nível acima. Por isso esta asserção tem duas metades, e a segunda roda em execução, sobre o contêiner montado. Critérios: (a) assembly do Nexio fora da lista que resolve a porta de nexio.attachment, ou que referencia a IServerFileStore → quebra o build, e continua quebrando se esse assembly declarar a mesma constante de escopo por conta própria; (b) nenhuma assinatura da porta usada pelo Nexio tem parâmetro de escopo, e nenhum assembly do Nexio alcança, por caminho nenhum, uma porta em que o escopo seja argumento; (c) em execução, falha na resolução — e nenhuma chamada chega à IFileService com aquele escopo — em dois casos: pedir ao contêiner a porta de um escopo não registrado, por tipo-marca de quem não registrou, por chave de texto ou por Type montado em execução; e pedir a porta de um escopo registrado — nexio.attachment — por Type montado em execução, por tipo-marca obtido em execução ou pelo tipo-marca de quem não registrou aquela porta. O alvo dos dois é o mesmo: o pedido sem referência estática ao assembly do consumidor, registrado ou não — limitar a verificação ao escopo sem registro a deixa um nível abaixo do requisito, porque reflexão sobre o tipo-marca de um escopo registrado resolve a porta e nem o (a) nem a metade não registrada a veem. O padrão existe no repositório do framework (JobExecutionAdapters.cs:235, :303 e HangfireJobActivator.cs:43, commit e4eb5a4f) — não é hipótese. O teste de fumaça de cada perfil confere os dois casos. É o que o (a) não dá: o que o (c) reprova não aparece como dependência entre assemblies.

O que isto compra, e o que não compra, no dia da extração

A pergunta chega cedo: se um dia quisermos tirar o motor de aprovação do monolito, é fácil? A resposta tem duas metades que puxam para lados opostos, e vale escrever as duas.

A parte mecânica é fácil, e é mensurável. A interface do futuro serviço já está escrita — .Contracts é a única coisa que os outros contextos enxergam, então virar serviço é dar a ela uma implementação sobre rede e trocar uma linha de registro por host. Não há acoplamento escondido para descobrir: a asserção 1 prova a cada build que ele não existe. E os dados saem limpos, porque a asserção 3 garante que nada navega para lá. Um monolito comum gasta semanas só para descobrir o que está acoplado; aqui a resposta é a suíte de testes.

A parte que estrutura nenhuma resolve é que o motor de aprovação é o pior candidato do sistema — e não por acidente, por decisão. A RN-AP-38 diz que a decisão e o efeito acontecem na mesma unidade de trabalho. Separe o motor e passam a existir dois commits em dois lugares: se o segundo falha, há requisição aprovada sem empenho — dinheiro liberado que o orçamento não conhece. A saída clássica é saga com compensação, mas a compensação aqui é estornar uma aprovação, que não é operação técnica: é evento de negócio com peso de auditoria. E há dois agravantes deste motor em particular: ele é universal sobre dez processos, então como serviço precisaria chamar de volta dez módulos pela rede; e ele lê muito para decidir — escopo do aprovador, alçada com vigência, valor na moeda funcional, materialidade, calendário —, leituras que hoje são em processo e virariam salto de rede no caminho quente do aprovar.

O que impede a extração não é a arquitetura — é a regra de negócio. A fronteira compilada já está pronta. A RN-AP-38 é que foi escolhida deliberadamente para o outro lado, e mudá-la é decisão de produto: aceitar um estado intermediário visível — aprovada, empenho pendente — com efeito garantido eventualmente e um caminho de compensação para quando a verba tiver sido consumida no meio. Que é, aliás, um caso de negócio real, não só um detalhe técnico.

O fork que decide tudo

Existem dois motores de aprovação possíveis. Motor como função de decisão — dada a política e o documento, devolve a cadeia e o próximo aprovador, e não escreve em ninguém: extrair é trivial. Motor como dono do desfecho — conclui a instância e aplica o efeito na mesma transação: extrair é inviável sem mudar a regra. O do Nexio é o segundo, por decisão.

O movimento que provavelmente serve

Se um dia o motor pesar, o que se extrai não é a entidade, é a operação: um host separado servindo o lado de leitura — fila do aprovador, painéis, a simulação — enquanto decidir e aplicar continua no host da API, dentro da transação. Mesmo código, mesmo banco, perfil novo. É o que o Portal já faz, custa um arranque, e resolve carga sem tocar em regra nenhuma.

Extratibilidade por contexto — para ninguém começar a separar achando que é só mover pasta
ContextoExtrair éPor quê
ReferencefácilLeitura predominante, sem invariante cruzada — e sem motivo: a carga é irrelevante
Catalog · Sourcingnão se separa sem decisãoO desfecho do ITEM, dos SUPPLIER_* e do BANK_ACCOUNT_CHANGE grava na transação do motor (contrato de desfecho), e o registro de ato de controle vai na transação do ato. Separar exige trocar o contrato de desfecho por outro mecanismo — decisão registrada nesta página antes, que a mensageria não pode tomar por baixo. O preço, lido na submissão, não pesa
OrganizationruimÉ lido em toda requisição — escopo e alçada. Separar cobra um salto de rede por chamada
Budget · Approval · Procurementnão se separaDividem a transação por exigência da Trava 1

ADR 2 · Persistência

Um sistema de registro, e ele é PostgreSQL

Decisão. PostgreSQL é o único sistema de registro. Redis e armazenamento de objeto são coadjuvantes — cache, trava e arquivo, nunca verdade. Qualquer outro armazenamento que venha a existir é derivado e descartável: pode ser reconstruído a partir do Postgres sem perda.

Este ADR é o único lugar do acervo que registra a escolha de banco. Ela é de implementação: as specs de negócio continuam escrevendo o requisito — vigência sem sobreposição, unicidade só entre os registros liberados, trava da linha — e citam recurso específico só como exemplo, com equivalente. O Modelo de Classes e o mapa de domínio citam esta página em vez de repetir a escolha.

A escolha não saiu de preferência: saiu das exigências que as specs já escreveram. Cada linha abaixo é um requisito publicado, e a coluna do meio é o que ele cobra do banco.

O que as specs exigem do banco — e o que isso elimina
Exigência já publicadaO que cobra do bancoConsequência da escolha
Soft delete global com unicidade preservadaÍndice único parcial (WHERE isDeleted = false)Elimina MySQL e MariaDB, que não têm. A plataforma já depende disso na unicidade da preferência de usuário.
Vigência sem sobreposição — preço de fornecedor, atribuição de centro de custoRestrição de exclusão por intervaloNativa só no Postgres. Nos demais, a garantia migra para trava na aplicação — que com n réplicas precisa de trava distribuída.
Sequência de numeração sem número repetidoIncrementar e ler o valor no mesmo comandoPostgres e Oracle diretamente; SQL Server por cláusula equivalente. Sem isso, a corrida volta.
Saldo por tupla concreta, sob trava das linhas de orçamento candidatasTrava de linha explícitaExiste em todos os candidatos, com semântica diferente. Ver o ADR 5.
Snapshot congelado e configuração por tenantDocumento dentro do campoPostgres e SQL Server. O framework já usa para as configurações do tenant.
Busca do catálogo por texto aproximadoÍndice de trigramaNativo no Postgres — o que adia o motor de busca externo por muito tempo. Ver os gatilhos.

Caminho materializado de árvore (categoria, centro de custo, plano de contas) não entra nesta tabela: a mecânica foi escrita em termos neutros — texto com prefixo escapado — justamente para não depender de tipo proprietário. Continua assim.

Um banco de registro, não dois

A regra é simples e vale para sempre: uma verdade só. O que mais existir é derivado.

PeçaPapelQuando entra
PostgreSQLSistema de registro. Toda invariante mora aqui.v1
RedisCache, trava distribuída e lista de revogação de token. Nunca guarda verdade.antes do 2º processo
Armazenamento de objetoArquivo. O framework já entrega a abstração com URL pré-assinada de upload. Ver GrydFiles.v1
Réplica de leituraRelatório e consulta pesada saindo do caminho transacional.quando o relatório competir com o transacional
Índice de busca externoBusca do catálogo.acima de ~500 mil itens ou p95 de busca > 300 ms
Armazém analíticoAnálise histórica multi-tenant.não antes do primeiro cliente pagante

Multiempresa e a porta de saída para isolamento físico

Esquema único com tenantId em toda tabela, com o filtro global do framework. É o que o produto precisa para centenas de tenants, e é o que mantém o custo baixo enquanto os primeiros clientes são pequenos.

A porta de saída está desenhada no framework, não pronta. Existem o resolvedor de conexão por tenant e o orquestrador de migração por tenant, mas nenhum executor de migração implementado, e o catálogo de tenants é só em memória (Gryd.IO, commit e4eb5a4f). É item da demanda de plataforma, e bloqueia o primeiro cliente com isolamento físico — não a v1. Para a porta continuar possível, vale uma regra desde já: nenhum código assume conexão única. A conexão é sempre resolvida pelo contexto de tenant, mesmo quando a resposta é a mesma para todos.

Descartado: banco de documento — o domínio é invariante pura, com transação sobre dez tabelas num ato só; a consequência do erro é fiscal. Dois sistemas de registro — dobra a conciliação sem resolver nada que a réplica de leitura não resolva. Banco por tenant na v1 — paga o custo operacional do isolamento antes de existir cliente que o exija.

ADR 2 · continuação

Como o banco escala, e em que ordem

A premissa que circula está meio certa. O Postgres escala leitura horizontalmente e nativamente — replicação em streaming é do próprio banco, e o serviço gerenciado admite até quinze réplicas por instância, encadeáveis. O que ele não faz é escalar escrita: há um primário só. E isso não é um defeito do Postgres — é igual no MySQL, no SQL Server e no Oracle. É o preço das garantias que a Trava 1 e a Trava 3 exigem: só existe transação com efeito atômico porque existe um lugar que decide a ordem.

Antes da escada: o banco não é o gargalo na meta declarada

Compras é processo de ritmo humano. Centenas de tenants com milhares de usuários ativos produzem dezenas de escritas por segundo em média e algumas centenas no pico — uma fração do que uma instância média entrega. O que dói muito antes é o que parece banco e não é: índice ausente, consulta N+1 do mapeador, relatório varrendo tabela sem paginação, e pool de conexão mal dimensionado — este praticamente garantido, porque cada réplica de API abre o seu, e o Postgres paga caro por conexão ociosa. Um intermediador de conexão entra junto com a segunda réplica de API, não depois: é a única parte do degrau 0 que não é reativa.

A escada — sete degraus, cada um com gatilho

Nenhum destes é trabalho de hoje. Todos são reação a um sinal observável.
DegrauO que éGatilhoO que compra
0Índice, N+1, relatório, intermediador de conexãoUso alto de CPU com carga baixaUma a duas ordens de grandeza
1Escala verticalUso sustentado acima de 60–70%Muito, por uma janela de manutenção — é a melhor relação esforço/ganho que existe
2Réplicas de leituraLeitura competindo com escritaLeitura é 90% da carga
3Tirar carga do banco — cache, índice de busca, particionamento por períodoTabela que cresce sem pararAdia os degraus caros
4Instância por contextoUm contexto domina o perfil de cargaJá habilitado pelo ADR 1 para o contexto que não divide transação — sem chave estrangeira nem junção cruzando contexto, mover é configuração; o que divide transação está no quadro abaixo. A mensageria não pode desfazer isso: requisito da página de Mensageria
5Partição por tenantEscrita saturando o primárioEscala linear de escrita
6Postgres distribuídoOperar muitas partições à mão deixou de caberParticionamento gerenciado, sem mudar a aplicação
Degrau 5 · o que o produto já ganhou

Um SaaS multi-tenant tem a chave de partição perfeita, e o Nexio já a tem em toda linha. Todo registro carrega tenantId, nenhuma consulta cruza tenant — o filtro global garante —, e o framework já desenhou conexão e migração por tenant — ainda a entregar (ADR 2). Particionar aqui é colocar grupos de tenants em instâncias diferentes, e isso escala escrita linearmente. É a mesma porta que o ADR 2 abriu para o cliente que exigir isolamento físico: uma porta, duas motivações. Quando escrita saturar, movem-se tenants — não se redesenha nada.

Degrau 4 · o que não se separa

Só sai para instância própria o contexto que não divide transação com ninguém — hoje, referência e notificação, cujo efeito vem depois do commit; para esses, a fronteira do ADR 1 já garante. Aprovação ↔ orçamento ↔ espinha transacional nunca se separam: dividem a mesma transação por exigência da Trava 1. Catálogo e fornecedor também não se separam enquanto o desfecho do ITEM, dos SUPPLIER_* e do BANK_ACCOUNT_CHANGE gravar na transação do motor, pelo contrato de desfecho: separá-los exige trocar esse contrato por outro mecanismo, e isso é decisão registrada nesta página antes de qualquer movimento — não configuração. A trilha de ato de controle não é contexto separável: o registro é gravado na transação do ato, então mora no banco do contexto do ato e vai junto com ele. Separar o que divide transação não seria escalar, seria trocar uma transação por uma saga sobre dinheiro e controle.

O que escala nenhuma resolve

Contenção não é capacidade. Se duas aprovações disputam as mesmas linhas de orçamento candidatas — as que podem recusar ou exigir aprovação —, mais máquinas não ajudam: a fila é na linha, não no servidor. Isso é o ADR 5, e nenhum degrau desta escada substitui o saldo materializado sob trava. O mesmo vale para o vizinho barulhento: um cliente com catálogo desproporcional degradando os outros não se resolve com mais CPU, e sim movendo aquele tenant — que é o degrau 5 outra vez.

As três regras que custam barato hoje e caro depois

Intenção de leitura declarada na consulta

Toda consulta nasce dizendo se é somente-leitura. Sem isso, o degrau 2 vira auditoria de todos os handlers em vez de mudança de roteamento. E a regra que vem junto é dura: saldo de orçamento, estado de aprovação e qualquer leitura dentro de transação vêm sempre do primário — réplica tem atraso, e atraso quebra ler o que se acabou de escrever. Só relatório, listagem e busca vão para réplica.

tenantId como primeira coluna dos índices

Não basta a coluna existir na tabela: ela precisa liderar o índice. É a chave de partição do degrau 5 e o que faz o filtro global ser barato em toda consulta. Refazer índice em tabela povoada é janela de manutenção; nascer certo é uma linha de configuração.

Particionar por período o razão e a trilha de aprovação, desde a criação

O razão de orçamento (BudgetEntry) e a trilha de aprovação (ApprovalEvent) são append-only e crescem para sempre. A trilha de auditoria da plataforma fica fora: a tabela é do GrydAudit, e o Nexio não decide a chave dela. Particionar tabela vazia é trivial; particionar tabela com anos de dado é migração longa com janela. O preço a pagar é declarado: a coluna de período entra na chave primária — decisão barata agora, cara depois.

Limites citados conferidos em 09/09/2026 na documentação do provedor: réplicas de leitura no RDS, réplicas encadeadas e Postgres distribuído gerenciado, disponível em São Paulo. Número de provedor envelhece — a ordem dos degraus não.

ADR 3 · Implantação

Um código-fonte, quatro perfis de execução

Decisão. A mesma solução gera quatro perfis: API, Worker, Portal e Migration. Não são serviços — não têm banco próprio, nem contrato de rede entre si, nem versão independente. São recortes de superfície do mesmo código, e é isso que dá isolamento de falha e de escala sem pagar o preço do sistema distribuído.

Os quatro perfis
PerfilO que registraRéplicasPor que separado
APIRotas do tenant, autenticadasnSem estado. Escala com requisição e é o único perfil no caminho do usuário.
WorkerEntrega de mensagens, jobs agendados, fila de aviso, varredura de arquivo1 na v1; n sem mudança de códigoTrabalho de fundo não pode competir com requisição nem disparar uma vez por réplica. É a causa direta do bloqueio 2 — e por isso nada no Worker pode depender de ele ser único: a página de Mensageria testa com duas réplicas.
PortalSó as rotas públicas do fornecedornModelo de autenticação diferente do tenant, exposto na internet aberta. Raio de falha e superfície de ataque isolados. Ver Portal do Fornecedor.
MigrationSó o migrador; roda até o fim e sai1, no pipelineCom n réplicas, migrar no start é corrida — e a corrida acontece exatamente no deploy que muda o esquema.

A consequência prática cabe numa frase: o registro de serviços passa a depender do perfil. O Worker não registra controller nem painel; a API enfileira, mas não executa job nem consome fila; o Portal registra um conjunto reduzido de rotas e nenhuma delas exige token de tenant. Um teste de fumaça por perfil prova que a separação não escorregou — e, enquanto registrar o GrydJobs subir sempre um servidor de execução, é esse teste que prova que a API não executa job.

Descartado: tudo num processo só — é o desenho de hoje, e é o que a fila em memória força; quebra na segunda réplica. Função sem servidor para a API — partida a frio e o custo de inicialização do ORM pesam num backend transacional, e a transação da Trava 1 não ganha nada com isso. Portal como serviço com banco próprio — duplicaria o cadastro de fornecedor e a cotação, que é justamente o dado que ele existe para escrever.

ADR 4 · Integração

Efeito na transação, evento depois do commit — e o mecanismo é da plataforma

Decisão. O efeito que precisa ser atômico com o ato acontece na mesma transação — pelo próprio caso de uso, por chamada síncrona declarada entre contextos (.Contracts) ou pelo contrato de desfecho do motor; o evento de domínio é publicado depois do commit e serve para notificar, medir e integrar — nunca para produzir efeito de que o ato dependa. O que o Nexio exige da entrega está na página de Mensageria, com critério de aceite; mecanismo e transporte são do estudo da plataforma, e esta página não os escolhe.

As três camadas do assíncrono
CamadaComo aconteceGarantia exigidaExemplo
Efeito de negócioCaso de uso, chamada síncrona entre contextos ou contrato de desfecho, na mesma transaçãoAtômicoAprovação concluída grava o empenho no orçamento
Evento de domínioPublicado depois do commitPelo menos uma vez; consumidor idempotente; tenant restauradoAviso ao requisitante, métrica, índice de leitura
Integração externaEvento que sai do processoAs do evento, mais ordem por documento, idempotência no destino e “aguardando” visívelPacote ao ERP

Uma exceção, declarada: revogar papel ou permissão incrementa a versão do token na transação da revogação, não como reação. Se virasse reação e a reação falhasse, quem perdeu o acesso continuaria com ele.

Por que esta página não escolhe o mecanismo

O que o Nexio precisa é verificável sem nomear tecnologia: nada é publicado antes do commit, nada se perde, nenhum consumidor lê dado de outro tenant, o pedido não chega em dobro ao ERP. O mesmo mecanismo serve a todos os produtos do Gryd.IO, e escolhê-lo dentro da arquitetura de um produto é decidir pela plataforma com a informação de um consumidor só. A página de Mensageria reúne os nove requisitos, os critérios de aceite e as evidências de código; o estudo do time do Gryd.IO devolve a escolha provada contra eles, e ela entra aqui como constatação.

O que continua sendo do Nexio, escrito nas páginas donas: quais efeitos são atômicos e por qual caminho, quais eventos existem e quem os consome, onde a ordem muda o resultado e qual é a chave, o que “aguardando” significa em cada integração e o ExternalRef de cada destino.

Fora do requisito: entrega exatamente uma vez; ordem global; publicação em leque antes de existir o segundo consumidor do mesmo evento; reconstrução de estado a partir do histórico.

ADR 5 · Concorrência

Três mecanismos, cada um onde o dano seria

Decisão. Token de versão no agregado, trava de linha nas linhas de orçamento que podem recusar ou exigir aprovação, chave de idempotência na borda. Nada de bloqueio global e nada de isolamento serializável em toda transação.

Token de versão otimista

As specs já pedem version inteiro e expectedVersion na escrita. A entidade base do framework não tem coluna de versão, e alterar a base é mudança que atravessa todos os módulos — então o token é coluna do agregado do Nexio. Onde a corrida é sobre estado, há precedente melhor na própria plataforma: marcar a coluna de estado como token de concorrência, que é como o vínculo entre tenants resolve duas decisões simultâneas.

Saldo por tupla, sob trava das linhas que podem recusar ou exigir aprovação

O razão continua append-only; o saldo é materializado por tupla concreta — empresa, centro de custo folha, conta folha, balde e estágio — e atualizado na mesma transação do lançamento. O saldo de uma linha de orçamento é a soma das tuplas sob ela, lida na hora. A trava cai sobre o conjunto de linhas candidatas da tupla cuja política pode recusar ou exigir aprovação (Block, RequireApproval), em ordem de id, e a soma é lida sob a trava. Sem trava, duas aprovações simultâneas leem o saldo antigo e a verba estoura sem que ninguém tenha aprovado o estouro; fora de ordem, duas decisões sobre as mesmas linhas se travam uma à outra. Linha que só avisa não trava. A regra está no Orçamento.

Idempotência na borda

Toda rota de ato e todo consumidor de fila aceitam chave de idempotência: repetir a mesma chave devolve o mesmo resultado e não duplica efeito. É o par obrigatório da entrega “pelo menos uma vez” do ADR 4, e é também o que protege o duplo clique do usuário.

Descartado: bloqueio pessimista amplo — transforma contenção pontual em fila global. Isolamento serializável por padrão — troca corrida por tempestade de retentativa, sem ganho onde o token de versão já basta. Alterar a entidade base do framework para ganhar versão — muda todos os módulos de todos os produtos para resolver uma necessidade de um.

ADR 6 · Fila e agendamento

Aviso durável sem fila nem agendador próprios

Decisão. O Nexio não constrói fila nem agendador. O requisito — aviso imediato e agendado que não some no reinício e não sai uma vez por réplica — está na página de Mensageria; a solução é da plataforma. O que o framework já tem vai abaixo como constatação — se cobre o requisito, quem diz é o estudo. O laço de entrega roda só no perfil Worker.

O problema tem tamanho: com fila e agendamento em memória, duas réplicas disparam todo aviso agendado duas vezes, e cada reinício apaga o que estava pendente. Resolver isso é da plataforma.

Constatação — o que o framework já entrega (Gryd.IO, commit e4eb5a4f)
PeçaO que fazOnde
Entrega durável de notificaçãoCom a opção DurableDelivery, fila e agendamento gravam a intenção de entrega junto com a notificação; o consumidor restaura o tenant da mensagem e exige tenant na leitura; o laço de entrega só roda onde RunBackgroundServices está ligado. Desligada por padrão.GrydNotifications.Infrastructure
Agendador sobre jobsAddGrydNotificationsScheduling troca o agendador em processo por um sobre GrydJobs, com armazenamento persistente.GrydNotifications.Scheduling
Padrão em memóriaSem nenhuma das duas, fila e agendador continuam em memória — servem a uma instância só.GrydNotifications.Infrastructure

Qual das duas, e com que prova, é do estudo da plataforma — contra os critérios de aceite da página de Mensageria: aviso agendado com duas réplicas sai uma vez, reinício não perde o pendente. Se o estudo confirmar uma delas, ao Nexio sobra configuração no arranque do Worker; a regra de perfil vale de qualquer jeito: a API registra quem produz aviso, nunca quem entrega.

Descartado: fila ou agendador próprios no Nexio — duplicaria o que o framework já mantém, sobre um transporte que o produto teria de operar. Manter em memória e rodar uma réplica só — é abrir mão da decisão de escala inteira.

ADR 7 · Nuvem

AWS, tudo gerenciado, região de São Paulo

Decisão. AWS em sa-east-1, com contêiner sem servidor para operar — nada de orquestrador de contêineres administrado pelo time. O critério que ordena todas as escolhas abaixo é um só: quantas horas por mês essa peça custa a um time de três pessoas.

De necessidade a serviço
NecessidadeServiçoPor quê
Executar os quatro perfisECS FargateContêiner sem nó para manter. Kubernetes é imposto fixo: paga-se em toda semana, não só na de pico.
Sistema de registroRDS PostgreSQL Multi-AZCusto previsível e baixo no começo. Aurora quando o teto de leitura importar de verdade — não antes.
Cache e trava distribuídaElastiCache RedisTambém guarda a lista de revogação de token, pelo cache do GrydAuth.
ArquivoS3O provedor já existe pronto no framework, com URL pré-assinada de upload — o arquivo nem passa pela API.
Transporte de mensagema definirEscolha do estudo da plataforma, contra os requisitos da página de Mensageria — e sob o critério desta tabela: horas por mês de um time de três.
Segredo e configuraçãoSecrets ManagerRotação sem redeploy; nada de credencial em variável de ambiente escrita à mão.
EntradaALB + WAFO WAF é por causa do Portal, que é a única superfície aberta à internet.
ObservabilidadeCloudWatch + OpenTelemetryO framework já emite rastro com identificador de tenant e de usuário. É ligar, não construir.
Imagem e entregaECR + GitHub ActionsO código já vive no GitHub e os pacotes do framework já vêm de lá.

Por que São Paulo

Dado de compra de empresa brasileira, com nota fiscal, CNPJ e fornecedor identificado. Manter o dado no país é resposta pronta na primeira reunião de segurança do primeiro cliente médio — e a latência para o usuário é consequência gratuita. É argumento comercial antes de ser técnico.

O que isso custa e o que não prende

No começo a conta é dominada por banco e balanceador, não por computação: o Fargate de dois perfis pequenos é a menor linha da fatura. E a portabilidade real está preservada onde importa — o armazenamento de arquivo já é abstraído em três provedores pelo framework, o cache fica atrás de interface do Nexio e a mensageria, atrás da plataforma. Trocar de nuvem custaria infraestrutura, não domínio.

Descartado: Kubernetes gerenciado — capacidade que o time não tem e não vai ter no ano. Aurora Serverless v2 na largada — tem piso de capacidade, e no cenário de poucos usuários custa mais, não menos. Azure — defensável para uma casa .NET, e sem motivo concreto (crédito, exigência de cliente, time que já opera) não paga a mudança. Nuvem do cliente ou instalação local — mudaria a fila, o agendamento e a entrega inteira; se virar exigência comercial, é revisão do ADR 3 e do ADR 7 juntos, não ajuste.

Estado atual

Os sete bloqueios do segundo processo

Nenhum dos sete depende do banco escolhido nem do estilo de arquitetura — todos existiriam igual em qualquer desenho. E o prazo deles não é “quando tivermos muitos usuários”: o segundo processo é o Worker do ADR 3, e ele sobe no primeiro dia em que houver um job para rodar.

O que quebra hoje, e onde
#BloqueioO que aconteceSaída
1Fila de aviso em memóriaReinício perde o que estava na fila; duas réplicas processam em duplicidadeADR 6 · plataforma
2Agendador em processoCada réplica dispara o mesmo job — lembrete, escalada e devolução de delegação saem n vezesADR 6 · plataforma
3Evento sem publicação garantidaAviso e integração se perdem em silêncio se o processo cair entre o commit e o envio; o único envelope durável do framework serve só à notificaçãoPlataforma · Mensageria
4Evento publicado dentro da transação, e job sem tenantO único contexto que despacha evento publica antes do commit — aviso de decisão que reverteu — e o do Nexio não despacha nenhum; job e consumidor rodam sem tenant, e sem tenant o filtro libera tudoPlataforma · Mensageria
5Entidade base sem token de concorrênciaDuas escritas simultâneas no mesmo agregado: a última vence, sem erro e sem rastroADR 5
6Trava de orçamento sobre várias linhasO lançamento cai sob mais de uma linha que pode recusar ou exigir aprovação; sem travar todas, em ordem fixa, duas aprovações leem o saldo antigo e furam a verba — ou se travam uma à outraADR 5
7Migração sem tarefa própria em produçãoO framework só migra no arranque em desenvolvimento; sem o perfil Migration no pipeline, ou ninguém migra, ou alguém liga a migração no arranque e as réplicas migram ao mesmo tempoADR 3

O 7 é de implantação e é do ADR 3: tarefa própria no pipeline. O 1 e o 2 são entrega da plataforma: o ADR 6 registra como constatação o que o framework já tem, e é o estudo que diz se serve — esta página não estima prazo. O 4, o 5 e o 6 precisam estar em pé antes da Requisição, porque ela é o primeiro agregado que grava empenho na mesma transação da aprovação e publica evento depois dela: o 4 tem duas metades: o evento depois do commit e a versão do token na transação são entrega da plataforma (página de Mensageria); o job sem tenant é o regime provisório do Nexio — exigência de tenant na leitura por fluxo, teste de fumaça dentro de job e de consumidor, asserção 9 —, que não espera a plataforma e fica em pé antes do primeiro job. A entrega da plataforma de tenant restaurado é exigida onde esse regime não alcança, nos jobs de módulo do framework, antes da primeira Requisição com anexo. O 5 e o 6 são a forma da gravação, e são o ADR 5. O 3 bloqueia a primeira entrada em produção, não o desenvolvimento da Requisition: em produção a API já tem n réplicas, o Worker é processo à parte e todo processo do motor publica aviso.

Manutenção

Quando revisar cada decisão

Decisão de arquitetura sem gatilho vira dogma: ninguém revisa porque ninguém sabe o que observar. Cada linha abaixo é um sinal observável — quando ele aparecer, a decisão correspondente volta à mesa; enquanto não aparecer, não volta.

DecisãoGatilho de revisão
ADR 1 · EstiloDois donos de contexto com colisão real de deploy; ou um contexto com necessidade de escala dez vezes diferente dos outros. Crescer o time não é gatilho sozinho. Dentro da organização do backend há mais dois, menores: quatro projetos por contexto quando um contexto ganhar dono próprio, e controllers em projeto por contexto quando duas pessoas colidirem em endpoints diferentes.
ADR 2 · PersistênciaA escada de sete degraus tem gatilho próprio por degrau — ver a seção de escala. Fora dela: cliente exigindo isolamento físico → partição por tenant, pelo caminho que o framework desenhou e ainda precisa entregar; catálogo acima de ~500 mil itens ou p95 de busca acima de 300 ms → índice externo.
ADR 3 · ImplantaçãoWorker saturado → particionar por fila em vez de aumentar a réplica. Portal com perfil de carga próprio → dimensionamento separado.
ADR 4 · IntegraçãoEstudo da plataforma entregue e provado contra os critérios da página de Mensageria → a solução entra aqui como constatação. Segundo consumidor do mesmo evento, ou necessidade de reconstruir estado a partir do histórico → requisito novo na Mensageria, não decisão nesta página.
ADR 5 · ConcorrênciaContenção medida na trava das linhas de orçamento candidatas, não suposta.
ADR 6 · FilaA plataforma trocar ou retirar a entrega durável de notificação → rever a constatação e a configuração do Worker.
ADR 7 · NuvemCrédito relevante de outro provedor, exigência contratual de cliente, ou entrega em nuvem do cliente.

Fronteira

O que esta página não decide

Estas ficam de fora de propósito — umas porque pertencem a outro dono, outras porque a decisão depende de um número que o negócio ainda não tem.

Outro dono

Versão de runtime, do ORM e do framework são do Gryd.IO; o Nexio acompanha. O que o produto precisa do núcleo e ainda não tem — mensageria, tenant em job, trilha de ato de controle, permissões em tenant novo, jobs só para enfileirar, exclusão lógica fora do GrydAuth, entre outros — está na demanda de plataforma de 11/09/2026, item por item, com o que cada um bloqueia.

Depende de número

Continuidade — objetivo de ponto e de tempo de recuperação, retenção de cópia, plano de restauração. É decisão de negócio disfarçada de infraestrutura: o custo muda por ordem de grandeza conforme a resposta, e a resposta é do dono do produto.

Vem depois

Estrutura de projetos e nomes saem junto com a poda do serviço, que já tem levantamento próprio. Ambientes e esteira — desenvolvimento, homologação, produção — são decisão separada, encostada no ADR 3 mas não contida nele.

Sem mudança

Frontend. Aplicação única para o tenant e portal do fornecedor em aplicação separada continuam como estavam; microfrontend segue descartado pelo mesmo motivo do ADR 1 — resolve problema de time, e o time é de três. Integração com ERP espera o primeiro cliente que a exija, e nasce com dois requisitos já escritos: idempotência no destino (página de Mensageria) e leitura do destino de pagamento ativo no ERP, para a conferência periódica de conta divergente do Fornecedor.

Os ADRs 1, 2, 3 e 5 — e, da página de Mensageria, o evento depois do commit e a versão do token na transação — são pré-requisito da Requisition. Ela é o primeiro agregado transacional do produto e nasce gravando empenho na mesma transação da aprovação, com token de versão, e publicando evento depois do commit. A publicação garantida (bloqueio 3) não é pré-requisito dela: bloqueia a primeira entrada em produção. O tenant restaurado em job também não: o regime provisório do Nexio vale desde o primeiro job, e a entrega da plataforma é exigida onde ele não alcança — jobs de módulo do framework —, antes da primeira Requisição com anexo. O ADR 4 depende da entrega da plataforma, não de trabalho desta página; o ADR 6 espera o mesmo estudo; o ADR 7 anda em paralelo com a spec.

Revisões

Histórico de revisões

VersãoDataO que mudou
v1.913/09/2026Correção da validação R7 (ataque de controle 4 e os resíduos de T1). Asserção 9, o quinto caminho pelo tipo concreto: o TenantScopeApplier é public sealed class de construtor público e TenantScope.Group(…) é fábrica pública — new TenantScopeApplier().Apply(TenantScope.Group(…)) não pede nada ao contêiner, não nomeia a interface, não escreve no acessor e não implementa IGroupScopableQuery, e lê N tenants com o build verde. Como no caminho (2), o alvo passa a ser a referência: reprovam-se toda referência ao tipo concreto TenantScopeApplier e toda fábrica de TenantScope que não seja Single. O “ponto único” ganhou nome: é o TenantExecutionScope, no Nexio.Platform — teste estático não reprova “fora do ponto único” sem um nome para comparar, e o framework não oferece auxiliar nenhum para isso. Asserção 10, critério (c) em dois casos: acompanha o critério novo da RN-GF-28 — além da porta de escopo não registrado, a porta de escopo registrado pedida por Type montado em execução, por tipo-marca obtido em execução ou pelo tipo-marca de quem não registrou aquela porta; o alvo dos dois é o pedido sem referência estática ao assembly do consumidor.
v1.812/09/2026Correção da validação R6 (ataques 13, 14, 15 e 16). Asserção 9, o alvo passa a ser a referência, não a escrita: o TenantDataFilterAccessor.Reset() devolve a exigência de tenant ao padrão false sem ser nenhum dos cinco caminhos, e um finally num job deixaria a leitura fail-open com o build verde — a asserção reprova toda referência ao acessor fora do ponto único. E a interface não é a única porta do escopo de grupo: o ITenantScopeApplier é serviço registrado e o TenantContextAccessor.ScopeTenantIds tem setter público estático — entram o uso do applier e a escrita no acessor, além da interface. Asserção 5 deixa de ser lista de três nomes: o Hangfire.AspNetCore expõe também MapHangfireDashboardWithAuthorizationPolicy e …WithNoAuthorizationFilters, e nome novo entra a cada versão do pacote — reprovam-se os tipos e o namespace do painel. Asserção 10, nova — posse do escopo: quem resolve a porta de cada escopo é o consumidor declarado, por tipo-marca do próprio consumidor, com critério estático e verificação em execução (contêiner recusa a porta de escopo não registrado), porque reflexão e chave de texto não deixam dependência entre assemblies; é a asserção que a RN-GF-28 do GrydFiles manda o produto repetir. Menores: a Requisição em prosa, no pré-requisito dos bloqueios.
v1.712/09/2026Correção da validação R5. A lista branca da asserção 8 passa a nomear actions, não assemblies, pela razão que sempre valeu — o host só expõe o que o produto usa: do GrydAuth.API entram autenticação e MFA, usuários, papéis, permissões, preferências, diretório e o detalhe do próprio tenant; ficam fora as actions de administração de tenants e o disjuntor, com critério próprio. Que cinco dessas actions devolvam dados de qualquer tenant é defeito da plataforma (demanda P22), e a lista branca não o compensa nem muda quando ele for corrigido; o template global de notificação virou o P23. Asserção 5: como o caminho do painel do GrydJobs é parâmetro e o painel é middleware fora do roteamento, o teste passa a reprovar a chamada de montagem em qualquer assembly do Nexio, qualquer que seja o caminho. Asserção 9: ligar a exigência de tenant é escrita no estado do filtro, porque o contrato só oferece desligar — um único ponto do Nexio escreve, e só para true; entram o quinto caminho (escopo de grupo) e o SQL cru, e as exceções passam a ser nomeadas uma a uma, com o IServerFileStore.OpenReadAsync entre elas.
v1.611/09/2026Correção da validação R4. Asserção 8 vira lista branca de endpoints: o host retira toda parte de aplicação fora da lista, inclusive a transitiva — a regra "nunca referencia o pacote agregador" era impossível de cumprir, porque o GrydAuth arrasta o GrydAudit.API —, a lista nomeia o que cada host expõe e o que fica fora (GrydAudit, GrydFiles, GrydJobs, GrydReports, envio avulso e hub de notificação). Asserções 5 e 8 cobrem middleware e endpoint sem controller: o painel do GrydJobs responde 404 em todo host. Asserção 9 reprova os quatro caminhos de desligar o filtro, inclusive a consulta que ignora os filtros e o pedido que se declara entre tenants. O envio agendado do GrydReports deixa de constar como job que desliga o filtro na execução inteira: desliga só para ler o agendamento. Notas de commit apontam e4eb5a4f.
v1.511/09/2026Correção da validação R3. Asserção 8 lê os endpoints efetivos do host montado, e o host referencia os projetos do módulo, nunca o pacote agregador (a descoberta automática expunha o AuditLogsController com a lista declarada limpa). Asserção 9 nova: o filtro de tenant só se desliga no Migrator. Leitura sem tenant: a exigência vale por fluxo, o teste de fumaça do Worker roda dentro de job e de consumidor, e os jobs de módulo do framework que desligam o filtro ficam como exceção declarada. O tenant restaurado em job deixa de constar como pré-requisito da Requisition: o regime provisório vale desde o primeiro job, e a entrega da plataforma é exigida antes da primeira Requisição com anexo — mesma resposta da página de Mensageria e da demanda de plataforma. Trava 4 sem "cada contexto reimplementa os três"; ADR 5 e bloqueio 6 com "recusar ou exigir aprovação"; catálogo com as quatro permissões de arquivo. Constatações do commit 2bc811df conferidas sem mudança até d6c5e9fa.
v1.411/09/2026Correção da validação R2. Degrau 4 e extratibilidade: catálogo, fornecedor e a trilha de ato de controle deixam de ser separáveis — o desfecho grava na transação do motor e o registro, na transação do ato. Asserção 8 vira lista branca de controllers por host, por assembly, cobrindo GrydFiles.API e GrydAudit.API; o catálogo de read:files/upload:files vai à demanda. Tabela de hosts ganha a coluna de leitura sem tenant, com teste de fumaça em API, Worker e Portal. Worker com uma réplica na v1 e n sem mudança de código. Bloqueios: 1 e 2 como entrega da plataforma sem prazo; publicação garantida bloqueia a primeira entrada em produção. Trava 3, ADR 2, escala e gatilho do ADR 5 falam em linhas de orçamento candidatas e tupla concreta; árvore da persistência e reconciliação do Migrator no lugar de interceptor de tenant e de backfill.
v1.311/09/2026ADR 4 e ADR 6 deixam de escolher mecanismo: requisito na página de Mensageria, solução do estudo da plataforma; SQS e Redis como transporte saem do desenho e do ADR 7. A escolha de banco passa a viver só no ADR 2. GrydAuth é módulo em processo, não serviço por HTTP. Bloqueio 6 vira o conjunto de trava das linhas de orçamento (ADR 5). Particionamento restrito ao razão e à trilha de aprovação. Banco por tenant registrado como desenhado, não pronto. Trava 4 falha fechada. Duas asserções novas (recorte por empresa; arquivo só pelo dono) e a asserção de perfil verificada pelo efeito. Migração e lista de revogação de token conferidas no framework. Pendências com o núcleo passam a citar a demanda de plataforma.
v1.209/09/2026Organização do backend: sete contextos, dois projetos cada, quatro hosts, uma persistência, asserções de arquitetura e extratibilidade.
v1.109/09/2026Desenho de implantação e escada de escala do banco.
v1.009/09/2026Criação: sete decisões e sete bloqueios.