Audit Trail
BetaQuem fez o quê, quando e o que mudou — para toda a plataforma, por organização
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.
- 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
- 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
- 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
7 endpoints em 5 recursos.
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"]| Atributo | Valor |
|---|---|
| Identificador | audit-trail |
| Categoria | Plataforma |
| Escopo | Tenant (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 PostgreSQL | audit |
| Status | Beta |
| Depende de | PostgreSQL, Redis (fluxo de eventos), IAM |
| Permissões | AUDIT_LOGS_READ, AUDIT_CONFIG_MANAGE |
O problema
negócioO 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
*_historyaqui, um campoupdated_byali, 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.
| Norma | Artigo | O que manda | Prazo de guarda |
|---|---|---|---|
| LGPD (Lei 13.709/2018) | art. 37 | Manter registro das operações de tratamento de dados pessoais | Não fixa — o art. 40 delega o tempo de guarda à autoridade nacional |
| Res. CMN 4.893/2021 | art. 21, I | Mecanismos de acompanhamento e controle, incluindo trilhas de auditoria | — |
| Res. CMN 4.893/2021 | art. 23, VIII | Registros desses mecanismos à disposição do Banco Central | Cinco anos |
| Res. BCB 85/2021 | arts. 21 e 23 | Mesma exigência, para instituição de pagamento | Cinco 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.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| "Quem alterou isso?" termina em suposição | GET /logs/resource/:tipo/:id devolve a linha do tempo |
| O antes foi sobrescrito | O evento carrega changes.before e changes.after |
| Cinco fontes de log, cinco formatos | Uma tabela, um formato, um filtro |
| Trilha por empresa cliente é projeto | Cada organização lê a própria com o token que já tem |
| Retenção é o que sobrou no disco | Retençã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.
Casos de uso reais
negócioCaso 1 — A alteração de taxa ganha autor e horário Cenário ilustrativo
Financeira de crédito com quatro produtos ativos e uma equipe de produto com cinco pessoas autorizadas a alterar parâmetro comercial.
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.
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}"]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
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 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.
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 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
Operação com contratos de níveis diferentes: clientes enterprise exigem cinco anos de trilha, os demais não exigem nada.
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.
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"]
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
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).
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.
É 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 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.
Mercado e diferenciais
negócioPanorama. "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ério | Catalisa Audit Trail | WorkOS Audit Logs | AWS CloudTrail | Datadog Audit Trail | Splunk |
|---|---|---|---|---|---|
| O que audita | Domínio dos seus building blocks | Eventos que você emite | Chamadas de API da conta AWS | Requisições ao próprio Datadog e log enviado | Qualquer log ingerido |
| Trilha exposta ao seu cliente final | Sim, nativo | Sim, com portal e exportação | Não | Não | Você constrói |
| Captura sem instrumentar | Não — cada BB publica (§7) | Não | Sim, automática | Parcial | Não |
| Antes e depois da mudança | Sim, changes | Sim, se você enviar | Parâmetros da chamada | Depende do log | Depende do log |
| Retenção por organização | Sim, 1 a 3.650 dias | Por plano | Por trilha, não por tenant | Padrão de 90 dias | Por índice |
| Imutabilidade forte (WORM, selo) | Não (§14) | Não localizada | Com bloqueio de objeto no S3 | Não localizada | Com complemento |
| Busca e correlação | Filtros e paginação | Busca no painel | Consulta básica, ou via Lake | Busca completa | O melhor da amostra |
| Alerta e detecção | Não | Não | Via EventBridge | Sim | Sim |
| Exportação | Não (§15) | Sim | Sim, para S3 | Sim | Sim |
| Dado fica no seu domínio | Sim | Não | Sim | Não | Depende da implantação |
| Custo público | Precificação em definição | US$ 125/mês por conexão SIEM; US$ 99/mês por 1 M de eventos retidos | Primeira cópia de eventos de gestão sem custo; US$ 2,00 por 100 mil eventos nas cópias adicionais | Sem preço unitário próprio publicado | Sem 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
- A trilha nasce multi-tenant e o dado não sai do seu domínio. O
organizationIdé claim assinado e entra na cláusulawherede 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. - O evento é de domínio porque a plataforma é a mesma.
customers.person.updatedexiste porque o Customers o publica, com ochangesjá 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. - 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 é | Escolha | Por quê |
|---|---|---|
| Auditar infraestrutura — quem abriu porta no grupo de segurança, quem assumiu qual papel | AWS CloudTrail | Ele faz isso automaticamente, e este building block não faz nada disso |
| Detecção, correlação e alerta sobre a trilha | Um SIEM: Splunk ou Datadog | O Audit Trail não substitui nenhum dos dois |
| Imutabilidade forte — armazenamento WORM, selo temporal, prova criptográfica de não adulteração | Uma trilha em armazenamento com bloqueio de objeto | O Audit Trail não atende hoje (§14 e §15) |
| O mesmo caso de uso, com a stack fora da Catalisa | WorkOS Audit Logs | Entrega 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.
Modelo de cobrança e ROI
negócioPrecificaçã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
| Driver | Por quê |
|---|---|
| Eventos registrados por mês | É o volume de escrita e a base do armazenamento |
| Período de retenção contratado | Cinco anos de trilha custam cinco vezes o armazenamento de um ano |
| Consultas à trilha | Filtro 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:
| Fornecedor | Base de cálculo pública | Ordem de grandeza |
|---|---|---|
| Catalisa Audit Trail | Precificação em definição | — |
| WorkOS Audit Logs | Por evento retido e por conexão SIEM | US$ 99/mês por 1 milhão de eventos retidos; US$ 125/mês por conexão SIEM |
| AWS CloudTrail | Por evento registrado | Primeira cópia de eventos de gestão sem custo; US$ 2,00 por 100 mil eventos nas cópias adicionais |
| Datadog Audit Trail | Dentro de Log Management | Sem preço unitário próprio publicado |
| Splunk | Workload, ingestão ou entidade | Sem 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.
| Linha | O que se evita | Tem número? |
|---|---|---|
| Tempo de investigação | Horas a dias de pessoas caras por cada pergunta "quem alterou o quê" | Depende da frequência da sua operação |
| Atendimento a auditoria | Extração manual recorrente do suporte, e o vazamento que ela pode causar | Depende do número de pedidos por cliente |
| Risco regulatório | Descumprimento do art. 37 da LGPD | Nã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.
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 --> DBO 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
endQuem 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 blocks | Consumo, aqui | |
|---|---|---|
| É automático? | Não — a chamada é escrita à mão em cada serviço | Sim, desde que o consumidor esteja ligado |
| Cobertura atual | 25 dos 32 building blocks, cerca de 87 tipos de evento | Todo evento que chegar ao fluxo |
| Building block novo | Precisa escrever a publicação nele | Aparece sem alterar uma linha aqui |
| Falha silenciosa? | Sim — operação não publicada não deixa rastro | Sim — 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_ENABLEDtem padrãofalseno schema de configuração; desde 10/09/2026 os stacks de staging e de produção o definem como'true'no serviçoaudit-trail. Além disso, ele só é iniciado pelomain.tsdo modo standalone — em modo monolito,src/main.tsnão chamastartAuditConsumer(). 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:
createdviraCREATE,updatedviraUPDATE,deletedviraDELETE, e assim por diante. Um evento com sufixo desconhecido cai no padrãoACCESS. 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". ParaDELETE, vira o "antes". Quando o evento não trazchanges, o consumidor sintetiza: criação gravachanges.aftercom a carga toda, exclusão gravachanges.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.
maskSensitiveDataroda antes doINSERTe é 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. ipAddresseuserAgentexistem na tabela e nunca são preenchidos por este caminho.CreateAuditLogInputaceita 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.
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| Evento de domínio | Mensagem 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. |
changes | JSONB com before e after. Em criação, só after. Em exclusão, só before. |
| Retenção | Dias 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 consumidor | audit-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 Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
AuditLog | audit.audit_logs | Uma linha por evento registrado | organizationId, eventType, action, actorId, actorType, resourceType, resourceId, changes (JSONB), ipAddress, userAgent, metadata (JSONB), createdAt |
OrganizationAuditConfig | audit.organization_audit_configs | Retenção da organização | organizationId (ú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
| Enum | Valores | Observação |
|---|---|---|
AuditAction | CREATE · UPDATE · DELETE · LOGIN · LOGOUT · ACCESS_DENIED · TOKEN_ISSUED · TOKEN_FAILED · RATE_LIMITED · EXPORT · ACCESS | Os cinco relacionados a segurança não ocorrem hoje — ver §15 |
ActorType | user · 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:
{
"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 noteReferê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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /audit-trail/api/v1/logs | Lista a trilha da organização, paginada e filtrável | AUDIT_LOGS_READ |
GET | /audit-trail/api/v1/logs/resource/:type/:id | Linha do tempo completa de um recurso | AUDIT_LOGS_READ |
Ingestão — /audit-trail/api/v1/events
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /audit-trail/api/v1/events | Recebe 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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /audit-trail/api/v1/config | Retenção vigente da organização | AUDIT_LOGS_READ |
PUT | /audit-trail/api/v1/config/retention | Define a retenção, de 1 a 3.650 dias | AUDIT_CONFIG_MANAGE |
Expurgo — /audit-trail/api/v1/cleanup
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /audit-trail/api/v1/cleanup | Remove fisicamente as linhas fora do prazo de retenção | AUDIT_CONFIG_MANAGE |
Saúde do serviço
| Método | Rota | Descrição |
|---|---|---|
GET | /audit-trail/health | Status 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âmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
page[number] | inteiro | 1 | Página, mínimo 1 |
page[size] | inteiro | 20 | Itens 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
{
"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
| Status | Quando |
|---|---|
400 | Filtro reprovado no Zod — UUID malformado, ação fora do enum, data fora do formato ISO |
401 | Token ausente, inválido ou expirado |
403 | Falta 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âmetro | Descrição |
|---|---|
:type | O resourceType normalizado: person, product, user, organization, file, ... Não é o nome da tabela nem o prefixo do evento |
:id | O resourceId. Precisa ser UUID válido, senão 400 |
Resposta 200
{
"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:
{ "data": { "type": "audit-config", "attributes": { "retentionDays": 365 } } }{ "data": { "type": "audit-config", "attributes": { "retentionDays": 365 } } }Depois de configurada:
{
"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:
{ "data": { "attributes": { "retentionDays": 1825 } } }{ "data": { "attributes": { "retentionDays": 1825 } } }{ "retentionDays": 1825 }{ "retentionDays": 1825 }| Campo | Tipo | Regra |
|---|---|---|
retentionDays | inteiro | De 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
{
"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.
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
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
API=https://audit-trail.bb.stg.catalisa.app/audit-trail/api/v1
curl -s "$API/config" -H "Authorization: Bearer $TOKEN" | jqAPI=https://audit-trail.bb.stg.catalisa.app/audit-trail/api/v1
curl -s "$API/config" -H "Authorization: Bearer $TOKEN" | jq{ "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
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]'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.
# 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_ENABLEDSem 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:
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
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'"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?
Receitas
Investigar "quem alterou este registro"
Objetivo. Reconstruir a linha do tempo de um recurso específico.
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é oresourceTypenormalizado —product, nãoproductsnemproducts.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, consulteGET /iam/api/v1/users/:userId— o Audit Trail não resolve o nome.- Quando
actorTypeforapi_client, oactorIdé 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"]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.jsonlPAGE=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.jsonlArmadilhas.
- 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-01sozinho é reprovado com400. - 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:
docker service logs <stack>_audit-trail 2>&1 | grep -i "audit consumer"docker service logs <stack>_audit-trail 2>&1 | grep -i "audit consumer"Audit consumer started successfullyAudit consumer started successfullySe 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?
redis-cli XLEN iam-events
redis-cli XREVRANGE iam-events + - COUNT 5redis-cli XLEN iam-events
redis-cli XREVRANGE iam-events + - COUNT 5Se 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.
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?
redis-cli XPENDING iam-events audit-trail-consumersredis-cli XPENDING iam-events audit-trail-consumersContagem de pendentes crescendo indica falha de gravação — o consumidor não confirma a mensagem e ela volta na varredura.
Ordem de suspeita.
AUDIT_CONSUMER_ENABLEDdesligado, ou serviço rodando em modo monolito — nesse modo o consumidor nunca é iniciado.- Evento sem
metadata.organizationId— descartado sem erro visível. - 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.
- Mensagem entregue mais de três vezes e descartada. Procure
Message exceeded max retriesno log do audit-trail.
Definir retenção diferente por cliente
Objetivo. Cinco anos para os clientes enterprise, um ano para os demais.
# 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'{ "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
curl -s "$API/config" -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.retentionDays'curl -s "$API/config" -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.retentionDays'3653652. Meça o que existe fora do prazo (365 dias, no exemplo)
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
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'{ "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_MANAGEpode 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.
Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token com organizationId e as permissões AUDIT_*. Publica iam.user.*, iam.organization.* e iam.association.* | Sim |
| Customers | Publica customers.person.created, .updated, .deleted | Não |
| Products | Publica products.product.created, .updated, .deleted | Não |
| Billing | Publica eventos de plano, produto, medidor e configuração | Não |
| API Keys | Publica criação, rotação, revogação e uso de chave | Não |
| File Storage | Publica criação, envio e exclusão de arquivo | Não |
| Feature Flags | Publica criação e alternância de flag | Não |
| Webhooks Engine | Lê o mesmo fluxo Redis, em grupo de consumidor próprio. Não depende do Audit Trail nem o alimenta | Não |
| Demais building blocks que publicam | Aparecem na trilha sem alteração aqui: a ação e o tipo de recurso são derivados do nome do evento | Nã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.
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
AUDIT_CONSUMER_ENABLED | Liga o consumidor que popula a trilha. Sem ele verdadeiro, nada é gravado | Não | false |
DATABASE_URL | PostgreSQL. O BB usa o schema audit | Sim | — |
REDIS_URL | Redis. Fluxo iam-events e limite de taxa | Sim | — |
JWT_SECRET | Segredo HS256 do IAM, mínimo 44 caracteres | Sim | — |
PORT | Porta em modo standalone | Não | 3000 (a topologia usa 3008) |
DEPLOYMENT_MODE | monolith ou standalone. O consumidor só inicia em standalone | Não | monolith |
RATE_LIMIT_ENABLED | Liga o limite global de taxa | Não | true no código; os stacks definem false |
CORS_ORIGINS | Origens permitidas, separadas por vírgula | Não | vazio |
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ência | Para quê | Se cair |
|---|---|---|
| PostgreSQL | Schema audit, tabelas audit_logs e organization_audit_configs | Leitura e escrita falham com 500 INTERNAL |
| Redis | Fluxo iam-events, grupo audit-trail-consumers | A trilha para de ser gravada; as rotas de leitura continuam funcionando. Os eventos permanecem no fluxo e são reprocessados quando o Redis voltar |
| IAM | Emissão do token. A verificação é local | Sem token novo; tokens válidos seguem funcionando |
Comportamento do consumidor
| Parâmetro | Valor | Onde |
|---|---|---|
| Fluxo | iam-events | audit-consumer.ts |
| Grupo de consumidor | audit-trail-consumers | audit-consumer.ts |
| Nome do consumidor | audit-consumer-{pid} | um por processo |
| Mensagens por leitura | 10 | COUNT |
| Espera bloqueante | 5.000 ms | BLOCK |
| Tempo ocioso para reivindicar pendente | 30.000 ms | varredura de pendências |
| Máximo de entregas | 3 | acima disso a mensagem é confirmada e descartada |
Limites
| Limite | Valor |
|---|---|
| Itens por página na listagem | 100 |
| Linha do tempo por recurso | sem limite e sem paginação |
| Retenção | 1 a 3.650 dias |
| Retenção padrão | 365 dias |
| Tamanho do corpo | 1 MB |
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | — | Filtro reprovado no Zod | UUID malformado, ação fora do enum, data sem formato ISO completo |
400 | — | retentionDays fora de 1 a 3650 | Ajuste o valor |
401 | — | Token ausente, inválido ou expirado | Renove pelo refresh token do IAM |
403 | — | Falta AUDIT_LOGS_READ ou AUDIT_CONFIG_MANAGE | Confira papel e permissões da organização no IAM |
403 | — | Token sem organizationId | Autentique informando a organização |
429 | — | Limite de taxa por IP | Recuo exponencial; use o Retry-After |
500 | INTERNAL | Falha de banco | Verifique 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/healthreporta 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
traceIdpropagado 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-consumerse o tamanho do fluxo comXLEN.
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 --> ISOAutenticação e permissões
Bearer JWT do IAM, HS256 verificado localmente. Duas permissões, com pesos bem diferentes:
| Permissão | Concede | Risco |
|---|---|---|
AUDIT_LOGS_READ | Ler a trilha e a configuração | Leitura de dado potencialmente pessoal — ver abaixo |
AUDIT_CONFIG_MANAGE | Alterar a retenção e executar o expurgo | Destruiçã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:
| Categoria | Termos que disparam o mascaramento |
|---|---|
| Senha | password, passwordHash, password_hash |
| Segredo | secret, clientSecret, client_secret, mfaSecret, mfa_secret |
| Token | token, accessToken, access_token, refreshToken, refresh_token |
| Chave e cabeçalho | apiKey, 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 --> DBA 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:
- Conceder
AUDIT_LOGS_READé conceder acesso a dado pessoal, mesmo para quem não tem permissão no building block de origem. Alguém semCUSTOMERS_PEOPLE_READpode ver o CPF de um cliente lendo a trilha. Trate a permissão com o mesmo rigor que trataria o acesso ao cadastro. - 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
UPDATEemAuditLog, e a tabela não temupdatedAtnemdeletedAt. - Não existe rota que grave linha.
POST /eventspublica 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
UPDATEeDELETEà 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/retentioncomretentionDays: 1ePOST /cleanup. ODELETEé 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: concederAUDIT_CONFIG_MANAGEa pouquíssimas contas, monitorar quedas bruscas emtotalItemse 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
| Norma | O que exige | Como o BB se posiciona |
|---|---|---|
| LGPD, art. 37 | Controlador 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 nacional | Produz 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, I | Autoriza 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, I | Instituiçõ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, VIII | Os registros desses mecanismos ficam à disposição do Banco Central pelo prazo de cinco anos | A 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 23 | Mesma exigência, para instituições de pagamento, que a Res. CMN 4.893 exclui do escopo | Idem |
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.
Limitações conhecidas
Bloqueia prometer trilha de auditoria hoje
| Limitação | Impacto | Situação |
|---|---|---|
| O consumidor continua desligado por padrão no código | AUDIT_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 vazio | Conhecido — mitigado nos stacks |
| O expurgo não roda sozinho | RetentionCleanupJob.runForAllOrganizations() existe e não é chamado por nenhum agendador. A retenção só é aplicada quando alguém chama POST /cleanup, uma organização por vez | Não implementado |
Integridade e destruição da trilha
| Limitação | Impacto | Situação |
|---|---|---|
| Um administrador pode apagar a própria trilha | Com 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 trilha | Limitação séria — mitigação apenas operacional |
| Sem imutabilidade criptográfica | Nenhuma assinatura, encadeamento ou selo temporal. Alteração direta no banco é indetectável pela aplicação | Não implementado |
| Sem armazenamento WORM ou trilha secundária | As linhas vivem em tabela PostgreSQL comum. Quem tem acesso administrativo ao banco altera ou apaga sem rastro | Não implementado |
Especificado, não implementado
| Limitação | Impacto | Situação |
|---|---|---|
| Eventos de segurança não chegam à trilha | AuditAction 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 trilha | Especificado, não implementado |
ipAddress e userAgent nunca são preenchidos | As 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 nulas | Especificado, não implementado |
Escolhas de projeto
| Limitação | Impacto | Situação |
|---|---|---|
| A cobertura depende de cada building block publicar | Nã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ência | Por design |
Evento sem organizationId é descartado em silêncio | O consumidor confirma a mensagem e não grava nada. Sem erro, sem métrica dedicada | Por design, com efeito colateral |
| Sem busca textual | Os filtros são igualdade exata mais período. Não há busca livre em changes nem em metadata | Por design |
| Sem alerta, correlação ou detecção | O Audit Trail registra e devolve. Não avisa ninguém, não correlaciona e não detecta padrão | Por design — é papel de SIEM |
| Sem dado pessoal mascarado | A lista de campos mascarados cobre credencial, não dado pessoal. CPF, nome, e-mail e telefone ficam em texto em changes | Por design, com implicação de acesso (§14) |
Inconsistências e limites conhecidos
| Limitação | Impacto | Situação |
|---|---|---|
| Mensagem descartada após três entregas | Acima 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 log | Conhecido |
links.self aponta para rota inexistente | Cada linha traz links.self para /audit-trail/api/v1/logs/{id}, e não há endpoint de busca por identificador. Seguir o link devolve 404 | Inconsistência conhecida |
| A linha do tempo por recurso não pagina | GET /logs/resource/:type/:id devolve todas as linhas em um documento, sem limite. Recurso com histórico longo gera resposta grande | Conhecido |
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 recurso | Conhecido |
| A ingestão externa não tem chave de idempotência | POST /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 workflow | Conhecido |
| A sonda de saúde não reflete o estado real | GET /health responde ok com o Postgres fora do ar e com o consumidor desligado | Conhecido |
Roadmap
| Limitação | Impacto | Situação |
|---|---|---|
| Sem exportação | Não há rota de exportação em CSV, JSON ou para armazenamento externo. A extração é paginar de 100 em 100 | Roadmap |
| Sem métrica de linhas gravadas | Não há contador de eventos registrados nem de eventos descartados. Detectar que a trilha parou exige consultar a própria trilha | Roadmap |
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 Trail | Webhooks Engine | |
|---|---|---|
| Grupo de consumidor | audit-trail-consumers | webhook-consumers |
| O que faz com o evento | Guarda para consulta posterior | Entrega a um sistema externo |
| Recursos próprios | Filtros, paginação, retenção e expurgo | Nova tentativa e assinatura da entrega |
| Depende do outro? | Não | Nã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