Catalisa.Building Blocks
Catálogo/Plataforma/Audit Trail

Audit Trail

Beta

Quem fez o quê, quando e o que mudou — para toda a plataforma, por organização

6
Endpoints
2
Entidades
0
Provedores
Tenant
Escopo
3008
Porta
2026-02
Desde

Cada empresa cliente sua consulta a própria trilha de auditoria por API — quem mexeu, no que mexeu, o que estava antes e o que ficou depois — sem você instrumentar log em cada serviço nem exportar planilha para o auditor.

Para quem é
  • Fintechs e instituições reguladas que precisam responder a auditoria sobre acesso e alteração de dado
  • Plataformas B2B que querem entregar trilha de auditoria como funcionalidade para as próprias empresas clientes
  • Times de segurança que hoje reconstroem "quem mudou isso" lendo log de aplicação em cinco serviços
Substitui
  • Consulta manual a log de aplicação espalhado por vários serviços para reconstruir um incidente
  • Tabela de histórico reimplementada dentro de cada building block
O que não é
  • Uma plataforma de observabilidade — não guarda métrica, trace nem log de aplicação
  • Um SIEM — não correlaciona, não alerta e não detecta ameaça
  • Um cofre de log imutável com selo temporal ou WORM — ver §14 e §15
  • Uma captura automática de tudo — cada building block publica os eventos dele explicitamente
O que dá para fazer

7 endpoints em 5 recursos.

Explorar a API →
01

Resumo executivo

O Audit Trail guarda a resposta para a pergunta que sempre chega tarde: quem mexeu nisso? Ele recebe os eventos que os outros building blocks publicam — usuário criado, produto alterado, cliente excluído — e transforma cada um numa linha consultável com o autor, o recurso, o instante e o antes e o depois do que mudou. Cada empresa cliente lê a própria trilha, com o mesmo token que usa no resto da plataforma.

Na prática ele resolve o dia em que alguém pergunta "por que este produto está com a taxa errada desde terça?". Sem trilha, a resposta é uma escavação em log de aplicação de cinco serviços, que na maioria das vezes termina em "não dá para saber". Com trilha, é GET /audit-trail/api/v1/logs/resource/product/:id e a linha do tempo do registro aparece.

Está em beta. As duas tabelas, as seis rotas (cinco de leitura e configuração, uma de ingestão para produtores externos), o consumidor de eventos e o expurgo por retenção estão implementados e cobertos por testes. Uma coisa a saber antes de qualquer outra: o consumidor que popula a trilha é desligado por padrão (AUDIT_CONSUMER_ENABLED, padrão false). Desde 10/09/2026 os stacks de staging e de produção o ligam no serviço audit-trail; num ambiente montado à mão, sem a variável, as rotas de consulta funcionam e devolvem vazio. Detalhe na §13 e na §15.

flowchart LR
  BB["Building blocks<br/>IAM · Customers · Products · ..."] -->|"publicam evento de domínio"| ST(["Redis Stream<br/>iam-events"])
  ST -->|"consome, quando ligado"| AC["AuditConsumer"]
  AC -->|"grava uma linha"| DB[("PostgreSQL<br/>audit.audit_logs")]
  DB -->|"GET /logs — com o token da própria organização"| CL["Empresa cliente"]
AtributoValor
Identificadoraudit-trail
CategoriaPlataforma
EscopoTenant (exige organizationId no token em todas as 6 rotas; a ingestão carimba a organização do token no evento)
Porta (standalone)3008
Path alias@audit-trail
Prefixo HTTP/audit-trail
Schema PostgreSQLaudit
StatusBeta
Depende dePostgreSQL, Redis (fluxo de eventos), IAM
PermissõesAUDIT_LOGS_READ, AUDIT_CONFIG_MANAGE

02

O problema

negócio

O cenário

Uma plataforma B2B cresce e passa a ter dez serviços e dezenas de empresas clientes. Um dia chega a pergunta: quem alterou o limite de crédito daquele cliente, e quando? Ou pior, ela chega de fora — de um auditor, de um cliente irritado ou de um regulador.

O que trava hoje

  • O log de aplicação não é trilha de auditoria. Ele foi escrito para depurar, não para responder. Tem o que o desenvolvedor achou útil naquele dia, some com a rotação de disco e não separa o que é de qual empresa cliente.
  • A resposta exige juntar cinco fontes. O serviço que fez a alteração, o que autenticou, o que notificou e o balanceador — cada um com formato e retenção diferentes. Reconstruir uma sequência é trabalho manual de horas.
  • Ninguém guarda o antes. Log registra "produto atualizado". O que estava lá antes, que é a informação que resolve a disputa, foi sobrescrito.
  • Cada serviço reimplementa histórico. Uma tabela *_history aqui, um campo updated_by ali, um gatilho de banco acolá. Nenhum é igual ao outro, e o que falta só aparece quando é preciso.
  • A trilha do cliente é problema seu. Se você vende plataforma para empresas, elas vão pedir a própria trilha de auditoria — e vão pedir com um contrato de fornecimento na mão. Construir isso por cima de log de infraestrutura é caro e frágil.

O custo de não resolver

Há uma exigência legal direta e curta. O art. 37 da LGPD diz, na íntegra:

Art. 37. O controlador e o operador devem manter registro das operações de tratamento de dados pessoais que realizarem, especialmente quando baseado no legítimo interesse.

Não é recomendação, é obrigação. E "está no log de aplicação, se ninguém tiver rodado a rotação" não é registro. A lei não fixa formato nem prazo de guarda — o art. 40 delega à autoridade nacional dispor sobre "o tempo de guarda dos registros", e enquanto isso não vem, o prazo é decisão sua.

Para instituição regulada, o prazo é nominal

Para instituição autorizada pelo BACEN a exigência é nominal e vem com prazo. A Resolução CMN 4.893/2021, art. 21, I, manda instituir mecanismos de acompanhamento e de controle incluindo "a definição de processos, testes e trilhas de auditoria"; e o art. 23, VIII, determina que os registros desses mecanismos fiquem à disposição do Banco Central pelo prazo de cinco anos. Para instituição de pagamento, que a 4.893 exclui, a exigência espelhada está na Resolução BCB 85/2021, nos mesmos artigos 21 e 23.

NormaArtigoO que mandaPrazo de guarda
LGPD (Lei 13.709/2018)art. 37Manter registro das operações de tratamento de dados pessoaisNão fixa — o art. 40 delega o tempo de guarda à autoridade nacional
Res. CMN 4.893/2021art. 21, IMecanismos de acompanhamento e controle, incluindo trilhas de auditoria—
Res. CMN 4.893/2021art. 23, VIIIRegistros desses mecanismos à disposição do Banco CentralCinco anos
Res. BCB 85/2021arts. 21 e 23Mesma exigência, para instituição de pagamentoCinco anos

Fora do regulatório, o custo é operacional e chega antes: sem trilha, todo incidente vira investigação, e toda investigação sem dado termina em suposição.


03

Proposta de valor

negócio
AntesDepois
"Quem alterou isso?" termina em suposiçãoGET /logs/resource/:tipo/:id devolve a linha do tempo
O antes foi sobrescritoO evento carrega changes.before e changes.after
Cinco fontes de log, cinco formatosUma tabela, um formato, um filtro
Trilha por empresa cliente é projetoCada organização lê a própria com o token que já tem
Retenção é o que sobrou no discoRetenção declarada por organização, com expurgo executável

A trilha é do cliente, não só sua. O organizationId vem do token, e a consulta só devolve o que pertence àquela organização. Isso permite entregar auditoria como funcionalidade do seu produto, não só como ferramenta interna.

O evento é de domínio. products.product.updated com changes.before.description e changes.after.description diz o que mudou em linguagem de negócio. Um log de infraestrutura diria que houve um UPDATE numa tabela.

Registrar não custa latência de quem escreve. O building block que gerou a mudança publica no fluxo Redis e segue. O Audit Trail consome depois, em processo separado, com nova tentativa em caso de falha.

sequenceDiagram
  autonumber
  actor U as Usuário
  participant BB as Products
  participant ST as Redis Stream iam-events
  participant AC as AuditConsumer
  participant PG as PostgreSQL audit_logs
  U->>BB: altera o produto
  BB->>BB: grava a alteração
  BB->>ST: XADD iam-events
  BB-->>U: 200 OK
  Note over U,BB: a resposta não espera a auditoria
  ST->>AC: XREADGROUP no grupo audit-trail-consumers
  AC->>PG: INSERT da linha da trilha

Segredo não entra na trilha. Antes de gravar, o serviço percorre changes e metadata recursivamente e substitui por [REDACTED] todo campo cujo nome contenha password, secret, token, apiKey, authorization e variações. A lista completa está na §14 — junto com o que ela não cobre.

A retenção é uma decisão declarada. Cada organização define de 1 a 3.650 dias. O padrão é 365. O expurgo apaga o que passou do prazo e devolve quantas linhas removeu.


04

Casos de uso reais

negócio

Caso 1 — A alteração de taxa ganha autor e horário Cenário ilustrativo

Contexto

Financeira de crédito com quatro produtos ativos e uma equipe de produto com cinco pessoas autorizadas a alterar parâmetro comercial.

A dor

Na terça-feira, a taxa de um produto apareceu diferente do que o comitê aprovou. Ninguém assumiu, e o log de aplicação registrava apenas Request error e alguns 200. A reconstrução consumiu dois dias e terminou sem conclusão — o que é pior que uma conclusão ruim, porque não gera ação.

A solução com o BB

O building block Products publica products.product.updated a cada alteração, com o productId e os campos alterados. O consumidor grava a linha com actorId, action: UPDATE, resourceType: product e o instante. A investigação vira uma chamada: GET /audit-trail/api/v1/logs/resource/product/{id}.

flowchart LR
  T["Alguém do time altera a taxa"] --> P["Products"]
  P -->|"products.product.updated<br/>productId e campos alterados"| ST(["iam-events"])
  ST --> AC["AuditConsumer"]
  AC --> L["Linha da trilha<br/>actorId · UPDATE · product · instante"]
  L --> Q["GET /audit-trail/api/v1/logs/resource/product/{id}"]
O resultado

A pergunta "quem alterou" passa a ter resposta em segundos. E o efeito preventivo é maior que o investigativo: alteração com autor registrado muda o comportamento de quem altera.

Caso 2 — O cliente final consulta a própria trilha Cenário ilustrativo

Contexto

Plataforma B2B que atende 60 empresas clientes na mesma instância. Três delas são instituições reguladas e exigem trilha de auditoria contratualmente.

A dor

A cada auditoria de cliente, o time de suporte extraía log manualmente, filtrava numa planilha e mandava por e-mail. Três dias de trabalho por pedido, com risco real de vazar linha de uma empresa no relatório de outra.

A solução com o BB

As três empresas recebem um usuário com a permissão AUDIT_LOGS_READ e consultam GET /audit-trail/api/v1/logs com filtros por autor, tipo de recurso, ação e período. O organizationId sai do token, então não existe caminho em que uma empresa enxergue a linha de outra.

flowchart LR
  TA["Token da Empresa A<br/>claim organizationId = A"] --> R["GET /audit-trail/api/v1/logs<br/>permissão AUDIT_LOGS_READ"]
  TB["Token da Empresa B<br/>claim organizationId = B"] --> R
  R --> W["where organizationId = claim assinado do token"]
  W --> LA["Só as linhas da Empresa A"]
  W --> LB["Só as linhas da Empresa B"]
O resultado

O pedido de auditoria deixa de passar pelo suporte. E a exportação manual — que era o vetor de vazamento — deixa de existir.

Caso 3 — A retenção deixa de ser "o que sobrou no disco" Cenário ilustrativo

Contexto

Operação com contratos de níveis diferentes: clientes enterprise exigem cinco anos de trilha, os demais não exigem nada.

A dor

A retenção era a rotação de log do servidor, igual para todo mundo, e ninguém sabia dizer quanto tempo de fato. Responder "vocês guardam por quanto tempo?" numa proposta era um chute com cara de compromisso.

A solução com o BB

PUT /audit-trail/api/v1/config/retention define de 1 a 3.650 dias por organização. GET /config mostra o valor vigente, com padrão de 365 dias quando nunca foi configurado. POST /cleanup executa o expurgo da organização e devolve quantas linhas foram removidas.

flowchart LR
  C["PUT /config/retention<br/>retentionDays entre 1 e 3.650"] --> G["GET /config<br/>valor vigente, padrão 365"]
  G --> X["POST /cleanup"]
  X --> D["DELETE físico das linhas<br/>com createdAt fora do prazo"]
  D --> N["deletedCount na resposta"]
  X -. "não há agendador chamando o job" .-> M["Alguém precisa executar — §15"]
O resultado

A retenção vira cláusula verificável em vez de estimativa. Com uma ressalva importante: o expurgo não roda sozinho — não há agendador chamando o job. Ele é executável por API e alguém precisa executar. Está na §15.

Caso 4 — A obrigação de registrar operações de tratamento é da lei Referência de mercado

Contexto

O art. 37 da Lei 13.709/2018 obriga controlador e operador a manter registro das operações de tratamento de dados pessoais. Para instituição autorizada pelo BACEN, a Resolução CMN 4.893/2021 exige "trilhas de auditoria" (art. 21, I) e cinco anos de guarda à disposição do Banco Central (art. 23, VIII).

A dor

No mercado, a maioria das empresas descobre que "registro das operações de tratamento" e "log de aplicação" não são a mesma coisa no dia em que precisa apresentar o registro. Log de aplicação não tem autor confiável, não tem o estado anterior, não separa por controlador, e some com a rotação. E há uma armadilha adicional: quando o titular exerce o direito de eliminação do art. 18, VI, apagar a trilha junto costuma ser o oposto do que a lei pede — o art. 16, I autoriza a conservação para cumprimento de obrigação legal ou regulatória, que é exatamente o caso dos cinco anos do BACEN.

A solução com o BB

É assim que a Catalisa endereça: cada operação sobre dado pessoal em building block que publica evento gera uma linha com autor, tipo de autor, recurso, instante e as diferenças. A separação por organização é estrutural. A retenção é declarada por organização e vai a 3.650 dias, o que cobre com folga os cinco anos exigidos — e permite manter a trilha separada da base operacional quando o titular pede eliminação do cadastro.

flowchart LR
  T["Titular pede eliminação — LGPD art. 18, VI"] --> D{"O dado está sob obrigação legal ou regulatória?"}
  D -->|"não"| E["Elimina o cadastro operacional"]
  D -->|"sim — art. 16, I"| K["Conserva a trilha pelo prazo<br/>cinco anos, Res. CMN 4.893/2021 art. 23, VIII"]
  K --> R["Retenção declarada por organização<br/>até 3.650 dias"]
  E --> R2["A trilha do tratamento permanece como registro do art. 37"]
O resultado

O registro passa a existir como dado consultável, com prazo declarado. A avaliação de suficiência continua sendo do encarregado e do jurídico de cada operação, e este documento não a substitui — em especial porque a ausência de imutabilidade forte (§14) é o ponto que um auditor rigoroso vai levantar.


05

Mercado e diferenciais

negócio

Panorama. "Audit log" nomeia três produtos diferentes, e a confusão entre eles é a origem da maioria das compras erradas.

flowchart TD
  A["Audit log nomeia três produtos diferentes"]
  A --> F1["1 · Auditoria de infraestrutura<br/>AWS CloudTrail"]
  A --> F2["2 · Observabilidade e SIEM<br/>Datadog · Splunk"]
  A --> F3["3 · Audit log como funcionalidade do seu produto<br/>WorkOS · Catalisa Audit Trail"]
  F1 --> D1["Audita a conta de nuvem, sem instrumentação"]
  F2 --> D2["Ingere, indexa, busca e correlaciona qualquer log"]
  F3 --> D3["A sua empresa cliente consulta a própria trilha"]

Família 1 — auditoria de infraestrutura

O primeiro é auditoria de infraestrutura: AWS CloudTrail é o exemplo canônico. Ele registra tudo que acontece na conta de nuvem, automaticamente e sem instrumentação, e é excelente nisso. Mas ele não sabe o que é um produto de crédito, e não tem noção de "empresa cliente do cliente".

Família 2 — observabilidade e SIEM

O segundo é observabilidade e SIEM: Datadog e Splunk. Eles ingerem, indexam e buscam qualquer log em volume, com poder de correlação que nenhum banco relacional alcança. O custo acompanha o volume, a operação exige time, e transformar essa base em uma trilha que a sua empresa cliente possa consultar sozinha é um projeto, não uma configuração.

Família 3 — audit log como funcionalidade do seu produto

O terceiro é audit log como funcionalidade do seu produto: WorkOS Audit Logs é o representante mais claro, e o único análogo de verdade desta lista. A documentação dele é explícita no ponto que importa — "all events are scoped to an Organization" — e o painel administrativo gera links que você envia ao seu cliente para ele consultar e exportar a própria trilha. É a mesma intenção do Audit Trail da Catalisa, com duas diferenças de origem: no WorkOS você emite cada evento por API, integrando mais um fornecedor, e o dado de compliance sai do seu domínio e do país.

Atenção. Vale dizer isso com todas as letras, porque é o eixo de decisão: CloudTrail, Datadog e Splunk auditam a sua infraestrutura; WorkOS e o Audit Trail auditam o seu produto para os seus clientes. Comparar os cinco na mesma linha é o erro que produz compra errada.

Tabela comparativa

CritérioCatalisa Audit TrailWorkOS Audit LogsAWS CloudTrailDatadog Audit TrailSplunk
O que auditaDomínio dos seus building blocksEventos que você emiteChamadas de API da conta AWSRequisições ao próprio Datadog e log enviadoQualquer log ingerido
Trilha exposta ao seu cliente finalSim, nativoSim, com portal e exportaçãoNãoNãoVocê constrói
Captura sem instrumentarNão — cada BB publica (§7)NãoSim, automáticaParcialNão
Antes e depois da mudançaSim, changesSim, se você enviarParâmetros da chamadaDepende do logDepende do log
Retenção por organizaçãoSim, 1 a 3.650 diasPor planoPor trilha, não por tenantPadrão de 90 diasPor índice
Imutabilidade forte (WORM, selo)Não (§14)Não localizadaCom bloqueio de objeto no S3Não localizadaCom complemento
Busca e correlaçãoFiltros e paginaçãoBusca no painelConsulta básica, ou via LakeBusca completaO melhor da amostra
Alerta e detecçãoNãoNãoVia EventBridgeSimSim
ExportaçãoNão (§15)SimSim, para S3SimSim
Dado fica no seu domínioSimNãoSimNãoDepende da implantação
Custo públicoPrecificação em definiçãoUS$ 125/mês por conexão SIEM; US$ 99/mês por 1 M de eventos retidosPrimeira cópia de eventos de gestão sem custo; US$ 2,00 por 100 mil eventos nas cópias adicionaisSem preço unitário próprio publicadoSem valor publicado

Preços consultados em 2026-08-16 nas páginas públicas de WorkOS e AWS CloudTrail. Datadog lista o Audit Trail dentro de Log Management sem preço unitário próprio, e Splunk não publica valor em página aberta — direciona a contato comercial. Onde escrevemos "não localizada", significa exatamente isso: não encontramos documentação pública, o que não equivale a afirmar que o recurso não exista. Valores variam por região, plano, volume e negociação — consulte a tabela vigente antes de qualquer comparação numérica.

Nossos diferenciais

  1. A trilha nasce multi-tenant e o dado não sai do seu domínio. O organizationId é claim assinado e entra na cláusula where de toda consulta. Entregar auditoria à sua empresa cliente é conceder uma permissão, não construir um produto — e sem enviar registro de compliance para um fornecedor fora do país, que é o custo escondido da alternativa mais parecida. Um SIEM chega ao mesmo lugar, mas passando por modelagem de índice, controle de acesso e uma interface que alguém precisa manter.
  2. O evento é de domínio porque a plataforma é a mesma. customers.person.updated existe porque o Customers o publica, com o changes já montado. Um fornecedor externo só chega a esse nível se você instrumentar cada serviço à mão — que é exatamente o trabalho que este building block dispensa.
  3. Registrar não entra no caminho crítico. O building block que mudou o dado publica no fluxo Redis e responde ao usuário. O consumidor grava depois, com reprocessamento de mensagem não confirmada. Auditoria síncrona é auditoria que, no dia de pico, alguém desliga.

Quando escolher o concorrente

Se o seu requisito éEscolhaPor quê
Auditar infraestrutura — quem abriu porta no grupo de segurança, quem assumiu qual papelAWS CloudTrailEle faz isso automaticamente, e este building block não faz nada disso
Detecção, correlação e alerta sobre a trilhaUm SIEM: Splunk ou DatadogO Audit Trail não substitui nenhum dos dois
Imutabilidade forte — armazenamento WORM, selo temporal, prova criptográfica de não adulteraçãoUma trilha em armazenamento com bloqueio de objetoO Audit Trail não atende hoje (§14 e §15)
O mesmo caso de uso, com a stack fora da CatalisaWorkOS Audit LogsEntrega com painel pronto e sem depender dos nossos building blocks

O Audit Trail ganha quando o problema é auditar o domínio dos building blocks que você já usa, e entregar essa trilha às suas empresas clientes.


06

Modelo de cobrança e ROI

negócio

Precificação em definição. O Audit Trail não tem preço fechado. Ele é infraestrutura transversal: faz sentido junto do conjunto de building blocks cujos eventos ele registra, não isolado. Não há valor a divulgar, e este documento não estima nenhum.

O que dispara custo

DriverPor quê
Eventos registrados por mêsÉ o volume de escrita e a base do armazenamento
Período de retenção contratadoCinco anos de trilha custam cinco vezes o armazenamento de um ano
Consultas à trilhaFiltro por período e por recurso em tabela grande é a operação cara

Comparação de custo

A Catalisa não tem preço definido para este building block, então qualquer tabela comparativa teria um lado vazio e nós não a inventamos. O que dá para oferecer é a única âncora pública da categoria, para orientar conversa:

FornecedorBase de cálculo públicaOrdem de grandeza
Catalisa Audit TrailPrecificação em definição—
WorkOS Audit LogsPor evento retido e por conexão SIEMUS$ 99/mês por 1 milhão de eventos retidos; US$ 125/mês por conexão SIEM
AWS CloudTrailPor evento registradoPrimeira cópia de eventos de gestão sem custo; US$ 2,00 por 100 mil eventos nas cópias adicionais
Datadog Audit TrailDentro de Log ManagementSem preço unitário próprio publicado
SplunkWorkload, ingestão ou entidadeSem valor publicado

Consulta em 2026-08-16 às páginas públicas de WorkOS e AWS CloudTrail. Os modelos são incomparáveis na origem — evento retido, evento registrado, GB ingerido e workload não convertem entre si —, então trate a tabela como referência de modelo, não de valor. Não estimamos custo mensal para nenhum cenário.

ROI

A conta tem três linhas, e nenhuma é licença.

LinhaO que se evitaTem número?
Tempo de investigaçãoHoras a dias de pessoas caras por cada pergunta "quem alterou o quê"Depende da frequência da sua operação
Atendimento a auditoriaExtração manual recorrente do suporte, e o vazamento que ela pode causarDepende do número de pedidos por cliente
Risco regulatórioDescumprimento do art. 37 da LGPDNão — sem estimativa defensável

A primeira é o tempo de investigação. Sem trilha, reconstruir "quem alterou o quê" custa de horas a dias de pessoas caras, e frequentemente termina sem conclusão. Com trilha, custa uma consulta. Multiplique pela frequência com que a sua operação faz essa pergunta — a maioria dos times subestima, porque parou de perguntar quando percebeu que não havia resposta.

A segunda é o atendimento a auditoria. Extração manual por pedido de cliente é trabalho recorrente do suporte e é um vetor de vazamento — a planilha filtrada errada. Entregar consulta por API elimina os dois.

A terceira é o risco regulatório. O art. 37 da LGPD exige registro das operações de tratamento. Não colocamos número nessa linha porque não existe estimativa defensável de sanção, e material comercial com número inventado sobre multa de LGPD é passivo, não argumento.


07

Arquitetura

Este é o building block em que a arquitetura importa mais que a lista de rotas. Ele tem dois caminhos independentes: um de escrita, assíncrono, que os blocos alimentam pelo fluxo e que produtores externos alcançam por uma rota de ingestão (POST /events, que só publica no fluxo — não grava linha); e um de leitura, síncrono, com cinco rotas.

O caminho de escrita — assíncrono, sem HTTP

flowchart TD
  BBS["IAM · Customers · Products · Billing · File Storage · ...<br/>25 building blocks"]
  EP["EventPublisher.publish()<br/>campos: type · payload JSON · metadata JSON<br/>injeta traceId e spanId do contexto de rastreamento"]
  ST(["Redis Stream iam-events"])
  G1["grupo audit-trail-consumers<br/>AuditConsumer<br/>AUDIT_CONSUMER_ENABLED — PADRÃO false — §15"]
  G2["grupo webhook-consumers<br/>webhooks-engine"]
  DEC{"metadata.organizationId presente?"}
  DROP["confirma e DESCARTA<br/>a mensagem é reconhecida e nada é gravado"]
  SVC["AuditLogService.logEvent()<br/>maskSensitiveData() em changes e metadata → [REDACTED]"]
  DB[("PostgreSQL · schema audit · audit_logs")]

  BBS -->|"service.create(...).andThen(publicar evento)<br/>a publicação é EXPLÍCITA, escrita linha a linha em cada serviço"| EP
  EP -->|"XADD"| ST
  ST --> G1
  ST --> G2
  G1 -->|"parseEventPayload — deriva ação, tipo de recurso e autor"| DEC
  DEC -->|"não"| DROP
  DEC -->|"sim"| SVC
  SVC --> DB

O caminho de leitura — síncrono, HTTP

flowchart LR
  CLI["Cliente"] -->|"Bearer JWT"| HN["Hono basePath /audit-trail"]
  HN --> R1["/api/v1/logs<br/>AUDIT_LOGS_READ"]
  HN --> R2["/api/v1/logs/resource/:t/:id<br/>AUDIT_LOGS_READ"]
  HN --> R3["/api/v1/config<br/>AUDIT_LOGS_READ"]
  HN --> R4["/api/v1/config/retention<br/>AUDIT_CONFIG_MANAGE"]
  HN --> R5["/api/v1/cleanup<br/>AUDIT_CONFIG_MANAGE"]
  R1 --> SV["AuditLogService · AuditConfigService<br/>RetentionCleanupJob"]
  R2 --> SV
  R3 --> SV
  R4 --> SV
  R5 --> SV
  SV --> PG[("PostgreSQL<br/>audit_logs · organization_audit_configs")]

Publicar, consumir, gravar — o caminho inteiro

O diagrama abaixo é a resposta detalhada para "como uma alteração de dado vira uma linha da trilha", com os comandos Redis reais e os dois pontos em que a mensagem pode não virar linha nenhuma.

sequenceDiagram
  autonumber
  actor U as Usuário
  participant BB as Products — building block emissor
  participant EP as EventPublisher
  participant ST as Redis Stream iam-events
  participant AC as AuditConsumer
  participant SV as AuditLogService
  participant PG as PostgreSQL audit.audit_logs

  U->>BB: PUT /products/api/v1/products/:id
  BB->>BB: grava a alteração no próprio schema
  BB->>EP: publish com type, payload e metadata
  EP->>EP: injeta traceId, spanId e traceFlags do contexto de rastreamento
  EP->>ST: XADD iam-events com os campos type, payload e metadata
  BB-->>U: 200 OK
  Note over U,BB: a resposta ao usuário não espera a auditoria

  loop consumeLoop, enquanto o consumidor estiver ligado
    AC->>ST: XPENDING — reivindica com XCLAIM o que está ocioso há mais de 30.000 ms
    AC->>ST: XREADGROUP grupo audit-trail-consumers, COUNT 10, BLOCK 5000
  end
  ST-->>AC: até 10 mensagens
  AC->>AC: parseEventPayload — ação, tipo de recurso, autor, resourceId e changes

  alt sem metadata.organizationId
    AC->>ST: XACK e descarta — nada é gravado
  else com organizationId
    AC->>SV: logEvent
    SV->>SV: maskSensitiveData em changes e metadata
    SV->>PG: INSERT em audit_logs
    alt gravação bem-sucedida
      AC->>ST: XACK
    else falha na gravação
      AC--)ST: sem XACK — a mensagem volta na varredura de pendências
    end
  end

Quem escreve na trilha — e isso é automático?

É a pergunta mais importante deste documento, e a resposta tem duas partes.

A publicação do evento é explícita. Não existe middleware, gatilho de banco nem interceptador do Prisma que audite sozinho. Cada serviço, em cada operação que quer auditar, chama EventPublisher.publish() com o tipo, a carga e a metadata. Contamos 25 dos 32 building blocks com serviços que publicam eventos, e cerca de 87 tipos de evento declarados no código. Uma operação para a qual ninguém escreveu a chamada de publicação não aparece na trilha, e não há como saber disso pela trilha — a ausência é silenciosa.

O consumo é automático, quando ligado. Do lado do Audit Trail não há nada a instrumentar: o AuditConsumer lê o fluxo iam-events inteiro, deriva ação e tipo de recurso do nome do evento e grava. Um building block novo que publique algo.recurso.created já aparece na trilha, sem mudar uma linha aqui.

Publicação, nos outros building blocksConsumo, aqui
É automático?Não — a chamada é escrita à mão em cada serviçoSim, desde que o consumidor esteja ligado
Cobertura atual25 dos 32 building blocks, cerca de 87 tipos de eventoTodo evento que chegar ao fluxo
Building block novoPrecisa escrever a publicação neleAparece sem alterar uma linha aqui
Falha silenciosa?Sim — operação não publicada não deixa rastroSim — evento sem organizationId é descartado

O que isso significa comercialmente, sem rodeio: o Audit Trail é uma garantia sobre os eventos que a plataforma publica, e não sobre tudo que acontece nela. Vender como "captura automática de tudo" é venda errada e não sobrevive a uma auditoria técnica. O texto correto é: cobre as operações de domínio que os building blocks publicam, com o antes e o depois, por organização.

Decisões não óbvias

  • O consumidor é desligado por padrão. AUDIT_CONSUMER_ENABLED tem padrão false no schema de configuração; desde 10/09/2026 os stacks de staging e de produção o definem como 'true' no serviço audit-trail. Além disso, ele só é iniciado pelo main.ts do modo standalone — em modo monolito, src/main.ts não chama startAuditConsumer(). A intenção original era que o processamento fosse opcional por ambiente; o efeito hoje é que a trilha não é populada em nenhum ambiente publicado. Ver §13 e §15.
  • Um fluxo Redis, dois grupos de consumidor. O Audit Trail e o Webhooks Engine leem o mesmo iam-events, cada um no próprio grupo. Ligar ou desligar um não afeta o outro, e cada um tem a própria posição de leitura.
  • Evento sem metadata.organizationId é descartado. Sem tenant não há onde gravar, então o consumidor confirma a mensagem e segue. É a decisão que mantém o isolamento inviolável e, ao mesmo tempo, a causa mais provável de "publiquei o evento e ele não apareceu na trilha".
  • A ação é derivada do nome do evento, não declarada. O último segmento do tipo é traduzido: created vira CREATE, updated vira UPDATE, deleted vira DELETE, e assim por diante. Um evento com sufixo desconhecido cai no padrão ACCESS. Isso torna o consumidor genérico — building block novo entra sem alteração aqui — e o preço é que a nomenclatura do evento vira contrato implícito.
  • Para CREATE, a carga inteira vira o "depois". Para DELETE, vira o "antes". Quando o evento não traz changes, o consumidor sintetiza: criação grava changes.after com a carga toda, exclusão grava changes.before. É o que dá conteúdo útil à trilha sem exigir que cada serviço monte a diferença.
  • A gravação é síncrona dentro do consumidor, com nova tentativa. Se a gravação falhar, a mensagem não é confirmada e volta na varredura de pendências. Mensagem entregue mais de três vezes é confirmada e descartada — o código chama isso de fila de mensagens mortas, mas não há fila: a mensagem simplesmente some, com um aviso no log. Ver §15.
  • O mascaramento acontece na gravação, não na leitura. maskSensitiveData roda antes do INSERT e é recursivo em objetos e listas. O valor sensível nunca chega ao banco, o que é o comportamento certo — e significa que a lista de campos protegidos vale a partir do momento em que foi escrita, sem efeito retroativo.
  • ipAddress e userAgent existem na tabela e nunca são preenchidos por este caminho. CreateAuditLogInput aceita os dois, e o consumidor não os fornece porque o evento não os carrega. As colunas ficam nulas. Ver §15.

Ciclo de vida da mensagem dentro do consumidor

stateDiagram-v2
  [*] --> Publicada: XADD por um building block
  Publicada --> Entregue: XREADGROUP pelo grupo audit-trail-consumers
  Entregue --> Gravada: INSERT em audit_logs
  Gravada --> Confirmada: XACK
  Confirmada --> [*]
  Entregue --> Descartada: sem metadata.organizationId — XACK sem gravar
  Entregue --> Pendente: falha na gravação, sem XACK
  Pendente --> Entregue: ociosa há mais de 30.000 ms — XCLAIM
  Pendente --> Descartada: mais de 3 entregas — XACK e a mensagem some
  Descartada --> [*]

Atenção. O estado Descartada não deixa linha na trilha e não gera métrica dedicada. No caso das três entregas, o log registra Message exceeded max retries — é o único sinal disponível de perda de auditoria.

Monolito vs. standalone

As rotas de leitura funcionam nos dois modos. O consumidor não: ele só é iniciado pelo main.ts de standalone, e ainda assim apenas se AUDIT_CONSUMER_ENABLED for verdadeiro. Em monolito, a trilha não é populada de forma alguma. Essa é a diferença de comportamento mais relevante entre os modos em todo o catálogo, e ela não é óbvia olhando as rotas.


08

Conceitos e modelo de dados

Glossário

TermoSignifica
Evento de domínioMensagem publicada por um building block quando algo acontece: type, payload e metadata. Trafega no fluxo Redis iam-events.
Tipo de evento (eventType)O nome completo, em três partes: customers.person.created. A primeira parte é o building block, a segunda o recurso, a terceira a ação.
Ação (action)Enumeração derivada do último segmento do tipo. É o que você filtra.
Tipo de autor (actorType)user quando a metadata traz userId; api_client quando há organização e não há usuário; system no restante.
Autor (actorId)O identificador do usuário. Quando o autor é api_client, recebe o identificador da organização — o mesmo valor que o IAM usa como client_id.
Tipo de recurso (resourceType)Normalizado a partir do prefixo do evento: customers.person vira person. Prefixo desconhecido cai no primeiro segmento do tipo.
changesJSONB com before e after. Em criação, só after. Em exclusão, só before.
RetençãoDias que a trilha da organização é preservada. De 1 a 3.650. Padrão 365 quando nunca configurada.
Expurgo (cleanup)Remoção física das linhas mais antigas que a retenção. Não é exclusão lógica: o DELETE é definitivo.
Grupo de consumidoraudit-trail-consumers. Mantém a posição de leitura no fluxo Redis e permite reprocessar mensagem não confirmada.

Modelo de dados

Schema audit no PostgreSQL. 2 modelos.

Modelo PrismaTabelaPropósitoCampos-chave
AuditLogaudit.audit_logsUma linha por evento registradoorganizationId, eventType, action, actorId, actorType, resourceType, resourceId, changes (JSONB), ipAddress, userAgent, metadata (JSONB), createdAt
OrganizationAuditConfigaudit.organization_audit_configsRetenção da organizaçãoorganizationId (único), retentionDays (padrão 365)
erDiagram
  ORGANIZATION ||--o{ AUDIT_LOG : "registra"
  ORGANIZATION ||--o| ORGANIZATION_AUDIT_CONFIG : "configura"

  AUDIT_LOG {
    uuid id PK
    uuid organization_id FK
    string event_type "customers.person.created"
    string action "CREATE, UPDATE, DELETE, ..."
    uuid actor_id "nulo quando actorType e system"
    string actor_type "user, system, api_client"
    string resource_type "person, product, user, ..."
    uuid resource_id
    jsonb changes "before e after"
    string ip_address "sempre nulo hoje"
    string user_agent "sempre nulo hoje"
    jsonb metadata
    timestamp created_at
  }

  ORGANIZATION_AUDIT_CONFIG {
    uuid id PK
    uuid organization_id UK
    int retention_days "padrao 365, de 1 a 3650"
    timestamp created_at
    timestamp updated_at
  }

Índices de audit_logs: (organizationId, createdAt), (organizationId, resourceType, resourceId), (organizationId, actorId) e eventType. Os três primeiros são compostos com a organização à frente — é o que sustenta o desempenho da consulta filtrada por tenant.

Observe o que não existe na tabela: updatedAt, deletedAt, campo de assinatura, encadeamento com o registro anterior. AuditLog é criado e nunca atualizado. As implicações estão na §14.

Enumerações

EnumValoresObservação
AuditActionCREATE · UPDATE · DELETE · LOGIN · LOGOUT · ACCESS_DENIED · TOKEN_ISSUED · TOKEN_FAILED · RATE_LIMITED · EXPORT · ACCESSOs cinco relacionados a segurança não ocorrem hoje — ver §15
ActorTypeuser · system · api_client—

Como um evento vira uma linha da trilha

O ponto de partida é a mensagem publicada por um building block, com três campos:

json
{
  "type": "customers.person.updated",
  "payload": { "personId": "...", "changes": { "name": "..." } },
  "metadata": { "organizationId": "...", "userId": "...", "timestamp": "...", "traceId": "..." }
}
{
  "type": "customers.person.updated",
  "payload": { "personId": "...", "changes": { "name": "..." } },
  "metadata": { "organizationId": "...", "userId": "...", "timestamp": "...", "traceId": "..." }
}

A partir dela, o consumidor decide se grava e o que grava:

flowchart TD
  EV["Evento publicado por um building block<br/>type · payload · metadata"] --> D{"metadata.organizationId presente?"}
  D -->|"não"| X["confirma e DESCARTA<br/>nada é gravado"]
  D -->|"sim"| M["último segmento updated → UPDATE<br/>prefixo customers.person → person<br/>metadata.userId presente → user<br/>resourceId ← primeiro campo achado entre<br/>id, userId, personId, organizationId, productId, ..."]
  M --> R["maskSensitiveData em changes e metadata<br/>campo sensível vira [REDACTED]"]
  R --> I["INSERT em audit.audit_logs"]

Atenção ao resourceId. Ele é o primeiro campo encontrado na carga, na ordem id, userId, personId, organizationId, productId, fileId, projectId, decisionId, versionId, tagId, riskBandId, pricingRuleId. Uma carga que traga organizationId antes do identificador do recurso próprio vai gravar a organização como recurso. Ao publicar evento novo, coloque o identificador do recurso em id ou no campo nominal dele.

Ciclo de vida de uma linha da trilha

stateDiagram-v2
  [*] --> Gravada: evento consumido
  Gravada --> Expurgada: POST /cleanup com AUDIT_CONFIG_MANAGE remove createdAt anterior a hoje menos retentionDays
  Expurgada --> [*]

  note right of Gravada
    createdAt fixo. Não há UPDATE.
    Não há exclusão lógica.
    Nenhuma rota altera uma linha existente.
  end note

  note right of Expurgada
    DELETE FÍSICO. Definitivo.
    Sem cópia, sem arquivamento,
    sem registro de que o expurgo ocorreu.
    A linha deixa de existir.
  end note

09

Referência da API

Prefixo: /audit-trail. Em monolito, a base é http://localhost:3000. Em staging, https://audit-trail.bb.stg.catalisa.app.

Todas as 6 rotas exigem authMiddleware (Bearer JWT do IAM), requirePermission(...) e o middleware local requireOrganization, que devolve 403 quando o token não carrega organizationId.

flowchart LR
  RQ["Requisição"] --> A["authMiddleware<br/>Bearer JWT do IAM, HS256 verificado localmente"]
  A -->|"401 sem token válido"| E1["Erro"]
  A --> P["requirePermission<br/>AUDIT_LOGS_READ, AUDIT_CONFIG_MANAGE ou AUDIT_EVENTS_WRITE"]
  P -->|"403 sem a permissão"| E2["Erro"]
  P --> O["requireOrganization<br/>local ao router"]
  O -->|"403 sem organizationId no token"| E3["Erro"]
  O --> H["Handler · Service · PostgreSQL"]

Consulta da trilha — /audit-trail/api/v1/logs

MétodoRotaDescriçãoPermissão
GET/audit-trail/api/v1/logsLista a trilha da organização, paginada e filtrávelAUDIT_LOGS_READ
GET/audit-trail/api/v1/logs/resource/:type/:idLinha do tempo completa de um recursoAUDIT_LOGS_READ

Ingestão — /audit-trail/api/v1/events

MétodoRotaDescriçãoPermissão
POST/audit-trail/api/v1/eventsRecebe um evento de domínio, ou até 100 em events[], de um produtor fora deste código (o painel, um sistema parceiro) e publica no fluxo iam-events — não grava linha. 202 com {accepted, messageIds}AUDIT_EVENTS_WRITE

Corpo JSON:API: { "data": { "type": "audit-events", "attributes": { "type": "credit.operation.created", "payload": {…}, "metadata": { "timestamp"?, "correlationId"?, "userId"? } } } } ou attributes.events[] com a mesma forma. O type tem que ser bloco.recurso.acao (três segmentos ou mais, minúsculas) — senão 400 com o índice do evento. A organização vem do token, nunca do corpo: metadata.organizationId enviado é ignorado. Sem userId, entra o sub de quem chamou. A linha aparece quando o consumidor processar o fluxo (segundos); a resposta é 202, não 201.

Configuração — /audit-trail/api/v1/config

MétodoRotaDescriçãoPermissão
GET/audit-trail/api/v1/configRetenção vigente da organizaçãoAUDIT_LOGS_READ
PUT/audit-trail/api/v1/config/retentionDefine a retenção, de 1 a 3.650 diasAUDIT_CONFIG_MANAGE

Expurgo — /audit-trail/api/v1/cleanup

MétodoRotaDescriçãoPermissão
POST/audit-trail/api/v1/cleanupRemove fisicamente as linhas fora do prazo de retençãoAUDIT_CONFIG_MANAGE

Saúde do serviço

MétodoRotaDescrição
GET/audit-trail/healthStatus estático do serviço. Pública, não contabilizada nas 6 rotas

Atenção. A sonda não consulta o banco e não reporta o estado do consumidor de eventos. Ela responde ok com o Postgres fora do ar e com o consumidor desligado. Não use /health para concluir que a trilha está sendo gravada.

Não existe rota que grave linha diretamente, e isso é proposital: a única forma de entrar na trilha é um evento de domínio no fluxo — os blocos publicam direto, produtores externos passam por POST /events, que publica por eles com a organização do token. Também não existe GET /logs/:id — o campo links.self de cada linha aponta para /audit-trail/api/v1/logs/{id}, que não é uma rota implementada. Ver §15.


GET /audit-trail/api/v1/logs

Parâmetros de consulta

ParâmetroTipoPadrãoDescrição
page[number]inteiro1Página, mínimo 1
page[size]inteiro20Itens por página, máximo 100
filter[actorId]UUID—Quem executou. UUID inválido devolve 400
filter[resourceType]texto—Igualdade exata: person, product, user, ...
filter[resourceId]UUID—O recurso específico. Precisa ser UUID
filter[action]enum—Um valor de AuditAction. Valor fora do enum devolve 400
filter[eventType]texto—Igualdade exata do tipo completo, por exemplo products.product.updated
filter[dateFrom]data-hora ISO—Início do período, inclusivo
filter[dateTo]data-hora ISO—Fim do período, inclusivo

Ordenação fixa: createdAt decrescente. Filtros combinam com E lógico.

Resposta 200

json
{
  "data": [
    {
      "type": "audit-logs",
      "id": "d41f9b2c-6f0e-4a55-9f1a-3b7c2e8d5a10",
      "links": { "self": "/audit-trail/api/v1/logs/d41f9b2c-6f0e-4a55-9f1a-3b7c2e8d5a10" },
      "attributes": {
        "eventType": "products.product.updated",
        "action": "UPDATE",
        "actorId": "b1000000-0000-0000-0000-000000000001",
        "actorType": "user",
        "resourceType": "product",
        "resourceId": "8d2b1f47-3c5a-4e19-b0d6-7a1e9c4f2b88",
        "changes": {
          "before": null,
          "after": { "active": false }
        },
        "ipAddress": null,
        "userAgent": null,
        "metadata": {
          "correlationId": null,
          "timestamp": "2026-08-16T13:52:07.220Z",
          "traceId": "4b1e...",
          "productId": "8d2b1f47-3c5a-4e19-b0d6-7a1e9c4f2b88"
        },
        "createdAt": "2026-08-16T13:52:07.318Z"
      }
    }
  ],
  "meta": {
    "totalItems": 482, "totalPages": 25, "currentPage": 1,
    "itemsPerPage": 20, "hasNextPage": true, "hasPreviousPage": false
  },
  "links": {
    "self":  "/audit-trail/api/v1/logs?page[number]=1&page[size]=20",
    "first": "/audit-trail/api/v1/logs?page[number]=1&page[size]=20",
    "next":  "/audit-trail/api/v1/logs?page[number]=2&page[size]=20",
    "last":  "/audit-trail/api/v1/logs?page[number]=25&page[size]=20"
  }
}
{
  "data": [
    {
      "type": "audit-logs",
      "id": "d41f9b2c-6f0e-4a55-9f1a-3b7c2e8d5a10",
      "links": { "self": "/audit-trail/api/v1/logs/d41f9b2c-6f0e-4a55-9f1a-3b7c2e8d5a10" },
      "attributes": {
        "eventType": "products.product.updated",
        "action": "UPDATE",
        "actorId": "b1000000-0000-0000-0000-000000000001",
        "actorType": "user",
        "resourceType": "product",
        "resourceId": "8d2b1f47-3c5a-4e19-b0d6-7a1e9c4f2b88",
        "changes": {
          "before": null,
          "after": { "active": false }
        },
        "ipAddress": null,
        "userAgent": null,
        "metadata": {
          "correlationId": null,
          "timestamp": "2026-08-16T13:52:07.220Z",
          "traceId": "4b1e...",
          "productId": "8d2b1f47-3c5a-4e19-b0d6-7a1e9c4f2b88"
        },
        "createdAt": "2026-08-16T13:52:07.318Z"
      }
    }
  ],
  "meta": {
    "totalItems": 482, "totalPages": 25, "currentPage": 1,
    "itemsPerPage": 20, "hasNextPage": true, "hasPreviousPage": false
  },
  "links": {
    "self":  "/audit-trail/api/v1/logs?page[number]=1&page[size]=20",
    "first": "/audit-trail/api/v1/logs?page[number]=1&page[size]=20",
    "next":  "/audit-trail/api/v1/logs?page[number]=2&page[size]=20",
    "last":  "/audit-trail/api/v1/logs?page[number]=25&page[size]=20"
  }
}

ipAddress e userAgent vêm nulos: as colunas existem, e o caminho de consumo de evento não as preenche (§15).

Erros

StatusQuando
400Filtro reprovado no Zod — UUID malformado, ação fora do enum, data fora do formato ISO
401Token ausente, inválido ou expirado
403Falta AUDIT_LOGS_READ, ou o token não carrega organizationId

Lista vazia com 200 é resposta válida. Se ela vier vazia sempre, o suspeito número um é o consumidor desligado — ver §13.


GET /audit-trail/api/v1/logs/resource/:type/:id

A linha do tempo de um recurso: tudo que aconteceu com ele, do mais recente ao mais antigo.

ParâmetroDescrição
:typeO resourceType normalizado: person, product, user, organization, file, ... Não é o nome da tabela nem o prefixo do evento
:idO resourceId. Precisa ser UUID válido, senão 400

Resposta 200

json
{
  "data": [
    { "type": "audit-logs", "id": "...", "attributes": { "action": "UPDATE", "createdAt": "2026-08-16T13:52:07.318Z", "...": "..." } },
    { "type": "audit-logs", "id": "...", "attributes": { "action": "CREATE", "createdAt": "2026-07-02T09:14:55.001Z", "...": "..." } }
  ]
}
{
  "data": [
    { "type": "audit-logs", "id": "...", "attributes": { "action": "UPDATE", "createdAt": "2026-08-16T13:52:07.318Z", "...": "..." } },
    { "type": "audit-logs", "id": "...", "attributes": { "action": "CREATE", "createdAt": "2026-07-02T09:14:55.001Z", "...": "..." } }
  ]
}

Atenção — sem paginação e sem limite. Esta rota devolve todas as linhas do recurso, em um único documento. Para um recurso com histórico longo, a resposta cresce sem teto. Use GET /logs com filter[resourceId] quando precisar de página. Ver §15.


GET /audit-trail/api/v1/config

Retenção vigente. Quando a organização nunca configurou, devolve o padrão sem id e sem carimbos de tempo — a forma da resposta muda:

json
{ "data": { "type": "audit-config", "attributes": { "retentionDays": 365 } } }
{ "data": { "type": "audit-config", "attributes": { "retentionDays": 365 } } }

Depois de configurada:

json
{
  "data": {
    "type": "audit-config",
    "id": "1c9e4b77-a0d2-4f31-8e55-b2c0d9f1a3e6",
    "attributes": {
      "retentionDays": 1825,
      "createdAt": "2026-08-16T14:02:11.000Z",
      "updatedAt": "2026-08-16T14:02:11.000Z"
    }
  }
}
{
  "data": {
    "type": "audit-config",
    "id": "1c9e4b77-a0d2-4f31-8e55-b2c0d9f1a3e6",
    "attributes": {
      "retentionDays": 1825,
      "createdAt": "2026-08-16T14:02:11.000Z",
      "updatedAt": "2026-08-16T14:02:11.000Z"
    }
  }
}

Trate data.id como opcional no seu cliente.


PUT /audit-trail/api/v1/config/retention

Request — as duas formas funcionam:

json
{ "data": { "attributes": { "retentionDays": 1825 } } }
{ "data": { "attributes": { "retentionDays": 1825 } } }
json
{ "retentionDays": 1825 }
{ "retentionDays": 1825 }
CampoTipoRegra
retentionDaysinteiroDe 1 a 3650 (1 dia a 10 anos)

Responde 200 com a configuração gravada. É um upsert: cria se não existir, sobrescreve se existir.

Atenção. Reduzir a retenção não apaga nada sozinho. O expurgo só acontece quando alguém chama POST /cleanup. Isso é importante nos dois sentidos: você não perde trilha por engano ao ajustar o número, e você também não cumpre a retenção só por declará-la.


POST /audit-trail/api/v1/cleanup

Executa o expurgo da sua organização, usando a retenção vigente. Sem corpo.

Resposta 200

json
{
  "data": {
    "type": "cleanup-result",
    "attributes": {
      "organizationId": "b0000000-0000-0000-0000-000000000001",
      "deletedCount": 12480,
      "retentionDays": 365
    }
  }
}
{
  "data": {
    "type": "cleanup-result",
    "attributes": {
      "organizationId": "b0000000-0000-0000-0000-000000000001",
      "deletedCount": 12480,
      "retentionDays": 365
    }
  }
}

Atenção — a remoção é física e definitiva. Não há exclusão lógica, não há cópia, não há arquivamento e o expurgo não deixa registro de si mesmo na trilha. Leia a §14 antes de conceder AUDIT_CONFIG_MANAGE.


10

Início rápido

Do zero à primeira consulta da trilha. Credenciais em AMBIENTES.md.

Os comandos abaixo não foram executados na redação deste documento. As respostas são as previstas pelo código.

flowchart LR
  P1["1 · Autenticar no IAM"] --> P2["2 · Ver a retenção vigente"]
  P2 --> P3["3 · Consultar a trilha"]
  P3 --> P4["4 · Se vier vazia, confirmar o consumidor"]
  P4 --> P5["5 · Provocar um evento"]
  P5 --> P6["6 · Achar a linha do evento"]

1. Autenticar no IAM

bash
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "root123456",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "root123456",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)

A variável TOKEN fica com o JWT. Ele carrega o organizationId — sem esse claim, todas as seis rotas devolvem 403.

2. Ver a retenção vigente

bash
API=https://audit-trail.bb.stg.catalisa.app/audit-trail/api/v1

curl -s "$API/config" -H "Authorization: Bearer $TOKEN" | jq
API=https://audit-trail.bb.stg.catalisa.app/audit-trail/api/v1

curl -s "$API/config" -H "Authorization: Bearer $TOKEN" | jq
json
{ "data": { "type": "audit-config", "attributes": { "retentionDays": 365 } } }
{ "data": { "type": "audit-config", "attributes": { "retentionDays": 365 } } }

Sem id e sem carimbos: a organização ainda não configurou nada e está no padrão.

3. Consultar a trilha

bash
curl -s "$API/logs?page%5Bsize%5D=5" -H "Authorization: Bearer $TOKEN" \
  | jq '.meta.totalItems, [.data[].attributes.eventType]'
curl -s "$API/logs?page%5Bsize%5D=5" -H "Authorization: Bearer $TOKEN" \
  | jq '.meta.totalItems, [.data[].attributes.eventType]'
text
0
[]
0
[]

Com o consumidor desligado — que é o estado dos ambientes publicados hoje — a resposta é exatamente esta: 200 com lista vazia.

4. Se totalItems for 0, confirme o consumidor

Trilha vazia quase sempre significa que o consumidor de eventos não está ligado. Ele é desligado por padrão e não está habilitado nos stacks publicados.

bash
# No host do serviço audit-trail
docker service inspect <stack>_audit-trail \
  --format '{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' \
  | grep AUDIT_CONSUMER_ENABLED
# No host do serviço audit-trail
docker service inspect <stack>_audit-trail \
  --format '{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' \
  | grep AUDIT_CONSUMER_ENABLED

Sem retorno, a variável não está definida e o consumidor está desligado. Ver §13.

5. Provocar um evento

Com o consumidor ligado, crie um produto:

bash
PRODUCT_ID=$(curl -s -X POST https://products.bb.stg.catalisa.app/products/api/v1/products \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"data":{"type":"products","attributes":{
        "name":"Teste de Auditoria","productType":"PERSONAL_LOAN",
        "description":"Produto criado para validar a trilha.",
        "minAmount":{"amount":1000,"currency":"BRL"},
        "maxAmount":{"amount":5000,"currency":"BRL"},
        "minInterestRate":0.01,"maxInterestRate":0.02,
        "minInstallments":3,"maxInstallments":12}}}' | jq -r '.data.id')
PRODUCT_ID=$(curl -s -X POST https://products.bb.stg.catalisa.app/products/api/v1/products \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"data":{"type":"products","attributes":{
        "name":"Teste de Auditoria","productType":"PERSONAL_LOAN",
        "description":"Produto criado para validar a trilha.",
        "minAmount":{"amount":1000,"currency":"BRL"},
        "maxAmount":{"amount":5000,"currency":"BRL"},
        "minInterestRate":0.01,"maxInterestRate":0.02,
        "minInstallments":3,"maxInstallments":12}}}' | jq -r '.data.id')

A criação responde na hora. A publicação do evento products.product.created acontece no mesmo fluxo, e o consumo vem depois — por isso o passo seguinte espera alguns segundos.

6. Achar a linha correspondente

bash
sleep 3   # o consumo é assíncrono

curl -s "$API/logs/resource/product/$PRODUCT_ID" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[].attributes.action'
sleep 3   # o consumo é assíncrono

curl -s "$API/logs/resource/product/$PRODUCT_ID" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[].attributes.action'
texto
"CREATE"
"CREATE"

Se o produto foi criado e a linha não apareceu, cheque nesta ordem: o consumidor está ligado? O evento traz organizationId na metadata? O Redis está acessível pelos dois serviços?


11

Receitas

Investigar "quem alterou este registro"

Objetivo. Reconstruir a linha do tempo de um recurso específico.

bash
curl -s "$API/logs/resource/product/$PRODUCT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | {quando: .attributes.createdAt,
                   quem:   .attributes.actorId,
                   tipo:   .attributes.actorType,
                   acao:   .attributes.action,
                   mudou:  .attributes.changes}'
curl -s "$API/logs/resource/product/$PRODUCT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | {quando: .attributes.createdAt,
                   quem:   .attributes.actorId,
                   tipo:   .attributes.actorType,
                   acao:   .attributes.action,
                   mudou:  .attributes.changes}'

Armadilhas.

  • O :type é o resourceType normalizado — product, não products nem products.product. Consulte a listagem geral primeiro para descobrir os valores em uso: jq '[.data[].attributes.resourceType] | unique'.
  • Sem paginação: recurso com histórico longo devolve tudo de uma vez.
  • actorId é o identificador do usuário no IAM. Para saber o nome, consulte GET /iam/api/v1/users/:userId — o Audit Trail não resolve o nome.
  • Quando actorType for api_client, o actorId é o identificador da organização, não de uma pessoa. É o comportamento esperado de token M2M.

Extrair a trilha de um período para auditoria

Objetivo. Todas as ações de um mês, paginadas, para entregar a um auditor.

Como não há rota de exportação, o caminho é percorrer as páginas até hasNextPage virar false:

flowchart LR
  S["PAGE=1"] --> G["GET /logs com filter[dateFrom], filter[dateTo]<br/>page[size]=100"]
  G --> W["Acrescenta .data ao arquivo trilha.jsonl"]
  W --> C{"meta.hasNextPage é true?"}
  C -->|"sim"| N["PAGE = PAGE + 1"]
  N --> G
  C -->|"não"| F["Fim — wc -l trilha.jsonl"]
bash
PAGE=1
: > trilha.jsonl
while : ; do
  R=$(curl -s -G "$API/logs" \
        --data-urlencode 'filter[dateFrom]=2026-07-01T00:00:00.000Z' \
        --data-urlencode 'filter[dateTo]=2026-07-31T23:59:59.999Z' \
        --data-urlencode "page[number]=$PAGE" \
        --data-urlencode 'page[size]=100' \
        -H "Authorization: Bearer $TOKEN")
  echo "$R" | jq -c '.data[]' >> trilha.jsonl
  [ "$(echo "$R" | jq -r '.meta.hasNextPage')" = "true" ] || break
  PAGE=$((PAGE+1))
done
wc -l trilha.jsonl
PAGE=1
: > trilha.jsonl
while : ; do
  R=$(curl -s -G "$API/logs" \
        --data-urlencode 'filter[dateFrom]=2026-07-01T00:00:00.000Z' \
        --data-urlencode 'filter[dateTo]=2026-07-31T23:59:59.999Z' \
        --data-urlencode "page[number]=$PAGE" \
        --data-urlencode 'page[size]=100' \
        -H "Authorization: Bearer $TOKEN")
  echo "$R" | jq -c '.data[]' >> trilha.jsonl
  [ "$(echo "$R" | jq -r '.meta.hasNextPage')" = "true" ] || break
  PAGE=$((PAGE+1))
done
wc -l trilha.jsonl

Armadilhas.

  • Não há endpoint de exportação. Paginar é o único caminho, e o máximo é 100 por página. Ver §15.
  • As datas precisam ser ISO 8601 completas, com hora. 2026-07-01 sozinho é reprovado com 400.
  • O token expira, por padrão, em uma hora. Extração longa precisa renovar no meio.
  • O arquivo gerado contém dado da sua organização, possivelmente pessoal. Trate-o como a base — não é porque saiu por API que deixou de ser dado sensível.

Descobrir por que um evento não apareceu na trilha

Objetivo. Diagnóstico em ordem, do mais provável ao menos.

flowchart TD
  Q["Publiquei o evento e ele não apareceu na trilha"] --> C1{"AUDIT_CONSUMER_ENABLED ligado<br/>e serviço em modo standalone?"}
  C1 -->|"não"| F1["Causa nº 1 — o consumidor não sobe.<br/>Em monolito ele nunca é iniciado"]
  C1 -->|"sim"| C2{"O evento chegou ao fluxo?<br/>XLEN iam-events"}
  C2 -->|"não"| F2["O serviço de origem não publica evento para essa operação"]
  C2 -->|"sim"| C3{"O evento traz metadata.organizationId?"}
  C3 -->|"não"| F3["Descartado em silêncio — confirma e ignora"]
  C3 -->|"sim"| C4{"XPENDING acusa mensagem parada?"}
  C4 -->|"sim"| F4["Falha de gravação, ou mais de 3 entregas.<br/>Procure Message exceeded max retries no log"]
  C4 -->|"não"| F5["Verifique o Redis e a conectividade dos dois serviços"]

1. O consumidor está ligado? É a causa nº 1. AUDIT_CONSUMER_ENABLED precisa ser verdadeiro no serviço audit-trail, e o serviço precisa estar em modo standalone. Confirme no log de inicialização:

bash
docker service logs <stack>_audit-trail 2>&1 | grep -i "audit consumer"
docker service logs <stack>_audit-trail 2>&1 | grep -i "audit consumer"
text
Audit consumer started successfully
Audit consumer started successfully

Se em vez disso aparecer Audit consumer is disabled (AUDIT_CONSUMER_ENABLED=false), pare por aqui: nada será gravado.

2. O evento chegou ao fluxo Redis?

bash
redis-cli XLEN iam-events
redis-cli XREVRANGE iam-events + - COUNT 5
redis-cli XLEN iam-events
redis-cli XREVRANGE iam-events + - COUNT 5

Se o XLEN não cresce quando você provoca a operação, o problema é na origem: aquele serviço não publica evento para essa operação.

3. O evento traz organizationId na metadata? Sem ele, o consumidor CONFIRMA e DESCARTA — silenciosamente.

bash
redis-cli XREVRANGE iam-events + - COUNT 1 | grep -o '"organizationId":"[^"]*"'
redis-cli XREVRANGE iam-events + - COUNT 1 | grep -o '"organizationId":"[^"]*"'

Sem retorno, o evento não tem tenant e nunca virará linha.

4. Há mensagem pendente no grupo do audit-trail?

bash
redis-cli XPENDING iam-events audit-trail-consumers
redis-cli XPENDING iam-events audit-trail-consumers

Contagem de pendentes crescendo indica falha de gravação — o consumidor não confirma a mensagem e ela volta na varredura.

Ordem de suspeita.

  1. AUDIT_CONSUMER_ENABLED desligado, ou serviço rodando em modo monolito — nesse modo o consumidor nunca é iniciado.
  2. Evento sem metadata.organizationId — descartado sem erro visível.
  3. O serviço de origem não publica evento para essa operação. Nem toda operação de todo building block publica; a publicação é escrita à mão em cada serviço.
  4. Mensagem entregue mais de três vezes e descartada. Procure Message exceeded max retries no log do audit-trail.

Definir retenção diferente por cliente

Objetivo. Cinco anos para os clientes enterprise, um ano para os demais.

bash
# Cinco anos para esta organização (o token define de qual organização se trata)
curl -s -X PUT "$API/config/retention" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"retentionDays": 1825}' | jq '.data.attributes'
# Cinco anos para esta organização (o token define de qual organização se trata)
curl -s -X PUT "$API/config/retention" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"retentionDays": 1825}' | jq '.data.attributes'
json
{ "retentionDays": 1825, "createdAt": "...", "updatedAt": "..." }
{ "retentionDays": 1825, "createdAt": "...", "updatedAt": "..." }

Armadilhas.

  • A configuração é sempre da organização do token. Não há rota para configurar a retenção de outra organização — cada uma precisa de um token próprio.
  • Declarar a retenção não executa nada. Sem POST /cleanup, a trilha cresce indefinidamente.
  • Aumentar a retenção não recupera o que já foi expurgado. O DELETE é físico.

Executar o expurgo com segurança

Objetivo. Aplicar a retenção sem apagar mais do que deveria.

1. SEMPRE confira a retenção vigente antes

bash
curl -s "$API/config" -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.retentionDays'
curl -s "$API/config" -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.retentionDays'
text
365
365

2. Meça o que existe fora do prazo (365 dias, no exemplo)

bash
CUT=$(date -u -d '365 days ago' +%Y-%m-%dT%H:%M:%S.000Z)
curl -s -G "$API/logs" --data-urlencode "filter[dateTo]=$CUT" \
  -H "Authorization: Bearer $TOKEN" | jq '.meta.totalItems'
CUT=$(date -u -d '365 days ago' +%Y-%m-%dT%H:%M:%S.000Z)
curl -s -G "$API/logs" --data-urlencode "filter[dateTo]=$CUT" \
  -H "Authorization: Bearer $TOKEN" | jq '.meta.totalItems'

O número que sai aqui é a ordem de grandeza do que o passo 3 vai apagar.

3. Só então execute

bash
curl -s -X POST "$API/cleanup" -H "Authorization: Bearer $TOKEN" | jq '.data.attributes'
curl -s -X POST "$API/cleanup" -H "Authorization: Bearer $TOKEN" | jq '.data.attributes'
json
{ "organizationId": "b0000000-0000-0000-0000-000000000001", "deletedCount": 12480, "retentionDays": 365 }
{ "organizationId": "b0000000-0000-0000-0000-000000000001", "deletedCount": 12480, "retentionDays": 365 }

Armadilhas.

  • Não há confirmação, não há simulação e não há volta. A chamada apaga fisicamente e devolve a contagem depois do fato.
  • Quem tem AUDIT_CONFIG_MANAGE pode reduzir a retenção para 1 dia e chamar o expurgo, removendo quase toda a trilha da própria organização em duas chamadas. Isso é limitação de projeto e está na §14 e na §15 — conceda essa permissão a poucas contas e trate como privilégio elevado.
  • O expurgo não registra a si mesmo na trilha. Não há linha dizendo que ele aconteceu.

12

Integração com outros building blocks

Building blockComo se relacionaObrigatório
IAMEmite o token com organizationId e as permissões AUDIT_*. Publica iam.user.*, iam.organization.* e iam.association.*Sim
CustomersPublica customers.person.created, .updated, .deletedNão
ProductsPublica products.product.created, .updated, .deletedNão
BillingPublica eventos de plano, produto, medidor e configuraçãoNão
API KeysPublica criação, rotação, revogação e uso de chaveNão
File StoragePublica criação, envio e exclusão de arquivoNão
Feature FlagsPublica criação e alternância de flagNão
Webhooks EngineLê o mesmo fluxo Redis, em grupo de consumidor próprio. Não depende do Audit Trail nem o alimentaNão
Demais building blocks que publicamAparecem na trilha sem alteração aqui: a ação e o tipo de recurso são derivados do nome do eventoNão
flowchart TD
  IAM["IAM"] --> ST
  CUS["Customers"] --> ST
  PRO["Products"] --> ST
  BIL["Billing"] --> ST
  FST["File Storage"] --> ST
  ETC["..."] --> ST
  ST(["Redis Stream iam-events<br/>publicação EXPLÍCITA — cada serviço chama publish"])
  ST -->|"grupo audit-trail-consumers"| AT["AUDIT TRAIL<br/>audit_logs"]
  ST -->|"grupo webhook-consumers"| WE["Webhooks Engine<br/>entrega externa"]
  AT -->|"AUDIT_LOGS_READ"| CLI["A empresa cliente consulta<br/>a PRÓPRIA trilha por API"]

Building blocks que publicam evento: 25 dos 32. Cobertura = o que os building blocks publicam. Nem tudo é publicado.

Este diagrama é o argumento comercial e o limite dele, na mesma imagem. O ganho é real: a trilha existe porque os blocos vizinhos já publicam, e um bloco novo entra sem trabalho de integração. O limite também é real: o que ninguém publica não aparece, e a trilha não tem como avisar que está incompleta. Diga as duas coisas na mesma frase — é o que sobrevive à auditoria técnica do comprador.


13

Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
AUDIT_CONSUMER_ENABLEDLiga o consumidor que popula a trilha. Sem ele verdadeiro, nada é gravadoNãofalse
DATABASE_URLPostgreSQL. O BB usa o schema auditSim—
REDIS_URLRedis. Fluxo iam-events e limite de taxaSim—
JWT_SECRETSegredo HS256 do IAM, mínimo 44 caracteresSim—
PORTPorta em modo standaloneNão3000 (a topologia usa 3008)
DEPLOYMENT_MODEmonolith ou standalone. O consumidor só inicia em standaloneNãomonolith
RATE_LIMIT_ENABLEDLiga o limite global de taxaNãotrue no código; os stacks definem false
CORS_ORIGINSOrigens permitidas, separadas por vírgulaNãovazio

Atenção — leia esta linha antes de operar. AUDIT_CONSUMER_ENABLED tem padrão false. Desde 10/09/2026, deploy/docker-stack.staging.yml e deploy/docker-stack.prod.yml a definem como 'true' só no serviço audit-trail (dois consumidores no mesmo grupo dividiriam as mensagens sem ganho). O consumidor também só é iniciado pelo main.ts de standalone — em monolito ele nunca sobe. Num ambiente montado à mão sem a variável, as rotas de leitura respondem e a trilha está vazia.

Cuidado adicional com o valor: a variável é interpretada por coerção de booleano. AUDIT_CONSUMER_ENABLED=false como texto não desliga — a forma de manter desligado é não definir a variável. Para ligar, use true e confirme no log de inicialização a linha Audit consumer started successfully.

Em desenvolvimento local, o orquestrador scripts/dev-standalone.ts força AUDIT_CONSUMER_ENABLED=true quando o serviço audit-trail está no conjunto selecionado — por isso a trilha é populada na sua máquina e não é nos ambientes publicados.

Dependências de infraestrutura

DependênciaPara quêSe cair
PostgreSQLSchema audit, tabelas audit_logs e organization_audit_configsLeitura e escrita falham com 500 INTERNAL
RedisFluxo iam-events, grupo audit-trail-consumersA trilha para de ser gravada; as rotas de leitura continuam funcionando. Os eventos permanecem no fluxo e são reprocessados quando o Redis voltar
IAMEmissão do token. A verificação é localSem token novo; tokens válidos seguem funcionando

Comportamento do consumidor

ParâmetroValorOnde
Fluxoiam-eventsaudit-consumer.ts
Grupo de consumidoraudit-trail-consumersaudit-consumer.ts
Nome do consumidoraudit-consumer-{pid}um por processo
Mensagens por leitura10COUNT
Espera bloqueante5.000 msBLOCK
Tempo ocioso para reivindicar pendente30.000 msvarredura de pendências
Máximo de entregas3acima disso a mensagem é confirmada e descartada

Limites

LimiteValor
Itens por página na listagem100
Linha do tempo por recursosem limite e sem paginação
Retenção1 a 3.650 dias
Retenção padrão365 dias
Tamanho do corpo1 MB

Catálogo de erros

StatusCódigoSignificaO que fazer
400—Filtro reprovado no ZodUUID malformado, ação fora do enum, data sem formato ISO completo
400—retentionDays fora de 1 a 3650Ajuste o valor
401—Token ausente, inválido ou expiradoRenove pelo refresh token do IAM
403—Falta AUDIT_LOGS_READ ou AUDIT_CONFIG_MANAGEConfira papel e permissões da organização no IAM
403—Token sem organizationIdAutentique informando a organização
429—Limite de taxa por IPRecuo exponencial; use o Retry-After
500INTERNALFalha de bancoVerifique o Postgres antes de investigar código

Note que não há 404 na trilha: consulta a recurso inexistente devolve 200 com lista vazia.

Observabilidade

  • GET /audit-trail/health reporta o processo. Não reporta banco nem estado do consumidor.
  • O consumidor loga a inicialização (Starting audit consumer, Created consumer group), cada mensagem processada em nível de depuração, e erros de gravação em nível de erro.
  • Mensagem descartada por excesso de tentativas gera Message exceeded max retries — vale um alerta, porque é perda silenciosa de trilha.
  • Cada mensagem é processada dentro de um span ligado ao trace da requisição original, através do traceId propagado na metadata do evento. Dá para ir da criação do produto até a linha da trilha no mesmo trace.
  • Para acompanhar acúmulo, monitore XPENDING iam-events audit-trail-consumers e o tamanho do fluxo com XLEN.

14

Segurança e compliance

Isolamento entre organizações

O organizationId é claim assinado do JWT. As seis rotas aplicam requireOrganization e passam o valor adiante (a ingestão carimba esse valor no evento e descarta qualquer organização vinda do corpo); findMany, findByResource, deleteExpired e a busca de configuração incluem a organização na cláusula where. No caminho de escrita, o isolamento é ainda mais estrito: evento sem metadata.organizationId não é gravado, porque não haveria tenant a que atribuí-lo. Não existe rota que devolva linha de outra organização, e não existe consulta cruzada — nem para conta com isRoot.

flowchart LR
  J["JWT assinado<br/>claim organizationId"] --> RD["Leitura<br/>findMany · findByResource · deleteExpired · config"]
  RD --> WH["where organizationId = claim"]
  EVT["Evento no fluxo"] --> CK{"metadata.organizationId?"}
  CK -->|"ausente"| NO["não é gravado — sem tenant a que atribuir"]
  CK -->|"presente"| WR["INSERT com o organizationId do evento"]
  WH --> ISO["Sem consulta cruzada — nem para conta com isRoot"]
  WR --> ISO

Autenticação e permissões

Bearer JWT do IAM, HS256 verificado localmente. Duas permissões, com pesos bem diferentes:

PermissãoConcedeRisco
AUDIT_LOGS_READLer a trilha e a configuraçãoLeitura de dado potencialmente pessoal — ver abaixo
AUDIT_CONFIG_MANAGEAlterar a retenção e executar o expurgoDestruição de trilha. Trate como privilégio elevado

Dado sensível na trilha

O maskSensitiveData roda antes do INSERT, é recursivo em objetos e listas, e substitui por [REDACTED] todo campo cujo nome contenha um destes termos, sem diferenciar maiúsculas:

CategoriaTermos que disparam o mascaramento
Senhapassword, passwordHash, password_hash
Segredosecret, clientSecret, client_secret, mfaSecret, mfa_secret
Tokentoken, accessToken, access_token, refreshToken, refresh_token
Chave e cabeçalhoapiKey, api_key, authorization
flowchart LR
  IN["changes e metadata do evento"] --> MK["maskSensitiveData<br/>recursivo em objetos e listas"]
  MK -->|"nome do campo contém password, secret, token, apiKey, authorization"| RE["[REDACTED]"]
  MK -->|"CPF, nome, e-mail, telefone, endereço, renda"| TX["gravado em texto"]
  RE --> DB[("audit.audit_logs")]
  TX --> DB

A lista não inclui dado pessoal. CPF, nome, e-mail, telefone, endereço e renda passam sem mascaramento e ficam gravados em texto na coluna changes. Isso é consequência direta do desenho — a trilha precisa registrar o antes e o depois, e um "antes" mascarado não serve para auditoria. Mas tem duas implicações que precisam estar claras:

  1. Conceder AUDIT_LOGS_READ é conceder acesso a dado pessoal, mesmo para quem não tem permissão no building block de origem. Alguém sem CUSTOMERS_PEOPLE_READ pode ver o CPF de um cliente lendo a trilha. Trate a permissão com o mesmo rigor que trataria o acesso ao cadastro.
  2. A trilha herda a obrigação de proteção do dado que registra. Ela é um repositório de dado pessoal como qualquer outro, sujeita às mesmas exigências de acesso, retenção e eliminação.

Não há cifragem de campo em audit_logs. Não há mascaramento de CPF na resposta.

O log é imutável? Parcialmente — e a diferença importa

O que é garantido:

  • Não existe rota de alteração. Nenhum endpoint faz UPDATE em AuditLog, e a tabela não tem updatedAt nem deletedAt.
  • Não existe rota que grave linha. POST /events publica no fluxo com a organização do token, e o consumidor é quem grava — ninguém forja uma linha de outra organização por HTTP, e o mascaramento de campos sensíveis vale para o evento externo como para o interno.
  • Não existe exclusão de linha individual. Não há DELETE /logs/:id.

O que não é garantido:

  • Não há imutabilidade criptográfica. Nenhuma assinatura, nenhum encadeamento com o registro anterior, nenhum selo temporal. Uma linha alterada direto no banco é indetectável pela aplicação.
  • Não há armazenamento WORM. As linhas ficam numa tabela PostgreSQL comum. Quem tem acesso administrativo ao banco pode UPDATE e DELETE à vontade, e isso não aparece na trilha.
  • Um administrador da organização pode apagar a própria trilha. Com AUDIT_CONFIG_MANAGE, duas chamadas bastam: PUT /config/retention com retentionDays: 1 e POST /cleanup. O DELETE é físico, definitivo, e não deixa registro na própria trilha de que aconteceu. Esta é a limitação de segurança mais séria do building block, está repetida na §15 e precisa ser dita a qualquer cliente que compre trilha de auditoria como controle de compliance. A mitigação disponível hoje é operacional: conceder AUDIT_CONFIG_MANAGE a pouquíssimas contas, monitorar quedas bruscas em totalItems e manter cópia de segurança do banco com retenção própria.
flowchart TD
  ADM["Conta com AUDIT_CONFIG_MANAGE"] --> P1["PUT /config/retention<br/>retentionDays: 1"]
  P1 --> P2["POST /cleanup"]
  P2 --> DEL["DELETE físico de quase toda a trilha da organização"]
  DEL --> NR["Sem confirmação, sem simulação, sem volta<br/>e sem registro na própria trilha"]
  MIT["Mitigação operacional"] -.-> M1["Conceder a pouquíssimas contas"]
  MIT -.-> M2["Monitorar quedas bruscas em totalItems"]
  MIT -.-> M3["Cópia de segurança do banco com retenção própria"]

Enquadramento regulatório

NormaO que exigeComo o BB se posiciona
LGPD, art. 37Controlador e operador devem manter registro das operações de tratamento de dados pessoais, especialmente sob legítimo interesse. Não fixa formato nem prazo — o art. 40 delega o tempo de guarda à autoridade nacionalProduz o registro para as operações que os building blocks publicam, com autor, instante, recurso e diferenças, separado por organização
LGPD, art. 16, IAutoriza conservar dado pessoal após o término do tratamento para cumprimento de obrigação legal ou regulatóriaÉ a base que permite manter a trilha mesmo depois de o titular exercer o direito de eliminação do art. 18, VI sobre o cadastro
Res. CMN 4.893/2021, art. 21, IInstituições financeiras devem instituir mecanismos de acompanhamento e controle incluindo "a definição de processos, testes e trilhas de auditoria"Contribui para o controle; não o satisfaz sozinho
Res. CMN 4.893/2021, art. 23, VIIIOs registros desses mecanismos ficam à disposição do Banco Central pelo prazo de cinco anosA retenção configurável vai a 3.650 dias, cobrindo os cinco anos com folga — mas o expurgo não é automático (§15)
Res. BCB 85/2021, arts. 21 e 23Mesma exigência, para instituições de pagamento, que a Res. CMN 4.893 exclui do escopoIdem

Atenção. Duas notas de precisão, porque erro de citação regulatória custa caro em proposta. A Circular BACEN 3.909/2018 trazia texto equivalente e teve os arts. 1º a 26 revogados pelo art. 26 da Resolução BCB 85/2021 — não a cite como norma vigente. E a Resolução Conjunta 6/2023 não é norma de Open Finance: ela trata de compartilhamento de indícios de fraude, e traz prazos ainda maiores (dez anos para os dados compartilhados, cinco para os registros dos mecanismos de controle).

O Audit Trail é matéria-prima de conformidade, não conformidade. A avaliação de suficiência, o mapeamento dos tratamentos e a política de retenção continuam sendo do encarregado de cada operação. E a ausência de imutabilidade forte, descrita acima, é justamente o ponto que um auditor rigoroso vai questionar — leve isso para a conversa antes que ele leve.

Não há certificação SOC 2, PCI-DSS ou ISO 27001 para este building block. Uma versão anterior desta documentação afirmava atendimento a LGPD, SOC 2 e PCI-DSS — essa afirmação não tem base e não deve ser repetida em proposta.

Retenção e eliminação

A retenção é declarada por organização e o expurgo é físico. Isso atende bem ao princípio de necessidade e ao art. 16 da LGPD, que trata do término do tratamento. Mas o expurgo não roda automaticamente — sem alguém chamar POST /cleanup, a retenção declarada é uma intenção, não um controle. Ver §15.


15

Limitações conhecidas

Bloqueia prometer trilha de auditoria hoje

LimitaçãoImpactoSituação
O consumidor continua desligado por padrão no códigoAUDIT_CONSUMER_ENABLED tem padrão false; os stacks publicados o ligam desde 10/09/2026, mas um ambiente montado à mão sem a variável grava nada e responde vazioConhecido — mitigado nos stacks
O expurgo não roda sozinhoRetentionCleanupJob.runForAllOrganizations() existe e não é chamado por nenhum agendador. A retenção só é aplicada quando alguém chama POST /cleanup, uma organização por vezNão implementado

Integridade e destruição da trilha

LimitaçãoImpactoSituação
Um administrador pode apagar a própria trilhaCom AUDIT_CONFIG_MANAGE, PUT /config/retention com 1 seguido de POST /cleanup remove fisicamente quase tudo. Não há confirmação, não há simulação, não há volta e o expurgo não se registra na trilhaLimitação séria — mitigação apenas operacional
Sem imutabilidade criptográficaNenhuma assinatura, encadeamento ou selo temporal. Alteração direta no banco é indetectável pela aplicaçãoNão implementado
Sem armazenamento WORM ou trilha secundáriaAs linhas vivem em tabela PostgreSQL comum. Quem tem acesso administrativo ao banco altera ou apaga sem rastroNão implementado

Especificado, não implementado

LimitaçãoImpactoSituação
Eventos de segurança não chegam à trilhaAuditAction declara LOGIN, LOGOUT, ACCESS_DENIED, TOKEN_ISSUED, TOKEN_FAILED e RATE_LIMITED, e o mapa de eventos prevê security.*. Mas o logSecurityEvent escreve apenas no log de aplicação e nada publica no fluxo Redis. Login, logout e acesso negado não aparecem na trilhaEspecificado, não implementado
ipAddress e userAgent nunca são preenchidosAs colunas existem e o tipo de entrada as aceita, mas o consumidor não as fornece porque o evento não as carrega. Vêm sempre nulasEspecificado, não implementado

Escolhas de projeto

LimitaçãoImpactoSituação
A cobertura depende de cada building block publicarNão há middleware, gatilho nem interceptador que audite sozinho. Operação sem chamada de publicação não aparece, e a trilha não tem como sinalizar a ausênciaPor design
Evento sem organizationId é descartado em silêncioO consumidor confirma a mensagem e não grava nada. Sem erro, sem métrica dedicadaPor design, com efeito colateral
Sem busca textualOs filtros são igualdade exata mais período. Não há busca livre em changes nem em metadataPor design
Sem alerta, correlação ou detecçãoO Audit Trail registra e devolve. Não avisa ninguém, não correlaciona e não detecta padrãoPor design — é papel de SIEM
Sem dado pessoal mascaradoA lista de campos mascarados cobre credencial, não dado pessoal. CPF, nome, e-mail e telefone ficam em texto em changesPor design, com implicação de acesso (§14)

Inconsistências e limites conhecidos

LimitaçãoImpactoSituação
Mensagem descartada após três entregasAcima de três tentativas a mensagem é confirmada e some. O código chama de fila de mensagens mortas, mas não existe fila — a trilha perde o evento, com um aviso no logConhecido
links.self aponta para rota inexistenteCada linha traz links.self para /audit-trail/api/v1/logs/{id}, e não há endpoint de busca por identificador. Seguir o link devolve 404Inconsistência conhecida
A linha do tempo por recurso não paginaGET /logs/resource/:type/:id devolve todas as linhas em um documento, sem limite. Recurso com histórico longo gera resposta grandeConhecido
resourceId pode capturar o campo erradoÉ o primeiro identificador encontrado numa ordem fixa. Carga que traga organizationId antes do identificador próprio grava a organização como recursoConhecido
A ingestão externa não tem chave de idempotênciaPOST /events devolve o id do fluxo como recibo; um produtor que repete a chamada após falha de rede grava a linha duas vezes. O painel só chama uma vez por passo de workflowConhecido
A sonda de saúde não reflete o estado realGET /health responde ok com o Postgres fora do ar e com o consumidor desligadoConhecido

Roadmap

LimitaçãoImpactoSituação
Sem exportaçãoNão há rota de exportação em CSV, JSON ou para armazenamento externo. A extração é paginar de 100 em 100Roadmap
Sem métrica de linhas gravadasNão há contador de eventos registrados nem de eventos descartados. Detectar que a trilha parou exige consultar a própria trilhaRoadmap

16

Perguntas frequentes

O Audit Trail captura tudo automaticamente?

Não, e essa é a pergunta mais importante deste documento. Cada building block publica explicitamente os eventos dele — a chamada está escrita linha a linha em cada serviço. Do lado do Audit Trail, o consumo é automático: qualquer evento publicado no fluxo é registrado sem alteração aqui. Então a resposta honesta é: automático para o que a plataforma publica, e cego para o que ela não publica. Vender como "auditoria automática de tudo" não sobrevive a uma auditoria técnica.

Minha trilha está vazia. O que aconteceu?

Quase certamente o consumidor está desligado. AUDIT_CONSUMER_ENABLED tem padrão false e não é definido nos stacks de staging nem de produção; em modo monolito o consumidor nem chega a ser iniciado. As rotas de leitura respondem 200 com lista vazia, o que parece uma trilha sem atividade. Siga a receita de diagnóstico da §11 antes de procurar outra causa.

O log é imutável? Alguém pode apagar?

Pela API, nenhuma linha pode ser alterada e nenhuma linha individual pode ser apagada — não existe rota para isso. Mas quem tem AUDIT_CONFIG_MANAGE pode reduzir a retenção para um dia e executar o expurgo, apagando fisicamente quase toda a trilha da própria organização em duas chamadas, sem deixar registro. E não há assinatura, encadeamento nem armazenamento WORM: quem tem acesso administrativo ao banco altera sem rastro. Se o seu requisito é imutabilidade forte, este building block não atende hoje. Diga isso ao cliente antes de assinar.

Login e tentativa de acesso negado aparecem na trilha?

Não. As ações existem na enumeração e o mapa de eventos as prevê, mas o registro de evento de segurança hoje vai apenas para o log de aplicação — nada é publicado no fluxo que o Audit Trail consome. Para investigar autenticação, a fonte é o log do IAM, não a trilha.

Consigo saber de qual IP veio a alteração?

Não. As colunas ipAddress e userAgent existem na tabela e nunca são preenchidas por este caminho, porque o evento de domínio não carrega essa informação. Vêm sempre nulas.

Como faço para exportar a trilha para um auditor?

Paginando GET /logs de 100 em 100 com filtro de período — há um script pronto na §11. Não existe rota de exportação, e o token expira em uma hora, então extração longa precisa renovar no meio.

Quanto tempo a trilha é guardada?

O que a organização declarar, de 1 a 3.650 dias, com padrão de 365. Mas atenção: declarar não apaga. O expurgo só acontece quando alguém chama POST /cleanup, e não há agendador fazendo isso. Na prática, hoje a trilha cresce até alguém executar o expurgo manualmente.

A trilha guarda dado pessoal?

Guarda. O campo changes carrega o antes e o depois do que mudou, e quando o recurso é uma pessoa, isso inclui CPF, nome, e-mail e telefone em texto. O mascaramento automático cobre senha, segredo, token e chave de API — não cobre dado pessoal. Conceder AUDIT_LOGS_READ é conceder acesso a esse conteúdo, inclusive para quem não tem permissão no building block de origem.

Isso me deixa em conformidade com a LGPD?

Não sozinho. O art. 37 da LGPD exige registro das operações de tratamento, e este building block produz esse registro para as operações que os building blocks publicam. A avaliação de suficiência, o mapeamento dos tratamentos, a base legal e a política de retenção continuam sendo do encarregado da sua operação. E há a ressalva da imutabilidade: um auditor rigoroso vai perguntar como você garante que a trilha não foi adulterada, e a resposta honesta hoje está na §14.

Se um cliente pedir a eliminação dos dados dele, tenho que apagar a trilha também?

Normalmente não, e apagar pode ser o erro. O direito de eliminação do art. 18, VI da LGPD tem exceção expressa no art. 16, que autoriza a conservação para cumprimento de obrigação legal ou regulatória. Para instituição regulada pelo BACEN, os cinco anos do art. 23 da Resolução CMN 4.893/2021 são exatamente essa obrigação. A leitura prática é: elimine o cadastro operacional, preserve a trilha pelo prazo regulatório, e documente a base legal da conservação. A decisão é do seu encarregado, não deste documento.

A trilha atende à exigência de cinco anos do BACEN?

Tecnicamente a retenção vai a 3.650 dias, o dobro do exigido. Operacionalmente, hoje, não: o expurgo é manual, o consumidor está desligado nos ambientes publicados, e não há imutabilidade forte. Antes de afirmar atendimento ao art. 21, I da Resolução CMN 4.893/2021, resolva os três itens da §15 marcados como bloqueantes.

Qual a diferença entre o Audit Trail e o Webhooks Engine?

Os dois leem o mesmo fluxo Redis, cada um no próprio grupo de consumidor. O Audit Trail guarda o evento para consulta posterior. O Webhooks Engine entrega o evento a um sistema externo, com nova tentativa e assinatura. Ligar ou desligar um não afeta o outro.

Audit TrailWebhooks Engine
Grupo de consumidoraudit-trail-consumerswebhook-consumers
O que faz com o eventoGuarda para consulta posteriorEntrega a um sistema externo
Recursos própriosFiltros, paginação, retenção e expurgoNova tentativa e assinatura da entrega
Depende do outro?NãoNão

Posso registrar um evento próprio da minha aplicação?

De dentro deste código, publicando um evento de domínio no fluxo com organizationId na metadata (sem ele o consumidor descarta em silêncio). De fora, por POST /audit-trail/api/v1/events com AUDIT_EVENTS_WRITE: o serviço carimba a organização do token e publica o mesmo evento no fluxo. Nos dois casos o type segue bloco.recurso.acao.


Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md