Pular para o conteúdo principal

Sistema de Auditoria — Rastreamento de Eventos no SuperApp

O sistema de auditoria do SuperApp permite rastrear ações de usuários sobre recursos do sistema de forma modular, segura e extensível. Qualquer produto ou feature pode registrar eventos de auditoria sem alterar o schema do banco — basta definir os valores de resource_type e action no TypeScript.

Visão geral​

O sistema foi projetado para responder perguntas como:

  • Quem visualizou o relatório X no dia Y?
  • Quais usuários exportaram dados da conta Z no último mês?
  • Quando e por quem o recurso ABC foi deletado?

Eventos de auditoria são imutáveis por design: usuários autenticados podem apenas inserir registros (e somente com o próprio user_id). A leitura é feita exclusivamente via uma função RPC com SECURITY DEFINER, acessível apenas pelo service_role (admin client). Isso garante que nenhum usuário consiga ler, alterar ou deletar o histórico de outro.

Quando usar​

  • Ações sensíveis sobre dados de negócio (visualização de relatórios, exportação de dados, aprovações)
  • Rastreamento de acesso a recursos por usuário
  • Compliance e trilha de auditoria por conta (team account)

Design modular​

A tabela audit_events não tem enum no banco para resource_type ou action. Esses valores são texto livre, e os tipos válidos são controlados pela camada de aplicação via TypeScript (AuditAction, resourceType como string). Isso significa que adicionar um novo tipo de recurso ou ação não exige nenhuma migration.


Arquitetura​

Tabela audit_events​

create table if not exists public.audit_events (
id uuid unique not null default extensions.uuid_generate_v4(),
account_id uuid references public.accounts(id) on delete cascade not null,
user_id uuid references auth.users(id) on delete cascade not null,
action text not null,
resource_type text not null,
resource_id text not null,
resource_name text,
metadata jsonb default '{}'::jsonb not null,
created_at timestamp with time zone default now() not null,
primary key (id)
);
CampoTipoDescrição
iduuidIdentificador único do evento
account_iduuidConta (team ou personal) à qual o evento pertence
user_iduuidUsuário que executou a ação
actiontextTipo de ação: view, create, update, delete, download, etc.
resource_typetextTipo de recurso afetado: ex. report, lead, vehicle
resource_idtextID do recurso afetado
resource_nametextNome legível do recurso (opcional, para exibição)
metadatajsonbContexto adicional livre (filtros usados, IP, etc.)
created_attimestamptzTimestamp do evento (UTC)

Índices​

A tabela tem índices otimizados para as queries mais comuns:

  • Por account_id (listagem por conta)
  • Por user_id (listagem por usuário)
  • Por created_at desc (ordenação cronológica)
  • Por resource_type e action (filtros)
  • Composto (account_id, created_at desc) — o mais usado em produção

RLS — Controle de acesso​

-- Usuários autenticados só podem inserir o próprio user_id
create policy "audit_events_insert" on public.audit_events
for insert to authenticated
with check (user_id = (select auth.uid()));

-- Leitura, update e delete: apenas service_role
revoke all on public.audit_events from authenticated, service_role;
grant insert on table public.audit_events to authenticated;
grant select, insert, update, delete on table public.audit_events to service_role;

Isso significa que:

  • Um usuário autenticado pode inserir eventos, mas nunca para outro user_id
  • Nenhum usuário pode ler, alterar ou deletar eventos via cliente padrão
  • Toda leitura passa obrigatoriamente pelo service_role (admin client)

RPC get_audit_events​

A leitura de eventos é feita via uma função RPC com SECURITY DEFINER, que executa com privilégios de service_role independentemente de quem a chama — mas como o grant execute é concedido apenas ao service_role, na prática só o admin client consegue invocá-la.

create or replace function public.get_audit_events(
p_account_id uuid,
p_resource_type text default null,
p_action text default null,
p_start date default null,
p_end date default null,
p_limit int default 50,
p_offset int default 0
)
returns table(
id uuid,
user_email text,
action text,
resource_type text,
resource_id text,
resource_name text,
metadata jsonb,
created_at text
)
language sql
security definer
set search_path = ''

A função faz um LEFT JOIN com auth.users para resolver o user_email a partir do user_id, sem expor a tabela auth.users diretamente ao cliente.


Tipos TypeScript​

Os tipos centrais do sistema estão em apps/web/lib/server/audit/audit.types.ts.

AuditAction​

export type AuditAction =
| 'view'
| 'create'
| 'update'
| 'delete'
| 'download'
| 'upload'
| 'share'
| 'export'
| 'approve'
| 'reject';

O tipo aceita tanto os valores predefinidos quanto qualquer string, permitindo ações customizadas sem alterar o tipo base.

TrackAuditEventParams​

export interface TrackAuditEventParams {
accountId: string;
action: AuditAction | string;
resourceType: string;
resourceId: string;
resourceName?: string;
metadata?: Record<string, unknown>;
}

AuditFilters​

export interface AuditFilters {
accountId: string;
resourceType?: string;
action?: string;
start?: string; // formato: 'YYYY-MM-DD'
end?: string; // formato: 'YYYY-MM-DD'
limit?: number; // padrão: 50
offset?: number; // padrão: 0
}

Como registrar um evento​

O ponto de entrada recomendado para registrar eventos a partir de componentes ou Server Actions é o trackAuditEventAction, definido em apps/web/lib/server/audit/audit.actions.ts.

'use server';

import { trackAuditEventAction } from '~/lib/server/audit/audit.actions';

Exemplos por tipo de ação​

Visualização de relatório​

await trackAuditEventAction({
accountId: team.id,
action: 'view',
resourceType: 'report',
resourceId: reportId,
resourceName: 'Relatório de Acessos — Junho 2026',
metadata: {
filters: { start: '2026-06-01', end: '2026-06-30' },
},
});

Criação de recurso​

await trackAuditEventAction({
accountId: team.id,
action: 'create',
resourceType: 'lead',
resourceId: newLead.id,
resourceName: newLead.name,
});

Atualização de recurso​

await trackAuditEventAction({
accountId: team.id,
action: 'update',
resourceType: 'vehicle',
resourceId: vehicle.plate,
resourceName: vehicle.plate,
metadata: {
changedFields: ['owner', 'status'],
},
});

Exclusão de recurso​

await trackAuditEventAction({
accountId: team.id,
action: 'delete',
resourceType: 'document',
resourceId: document.id,
resourceName: document.filename,
});

Download / exportação​

await trackAuditEventAction({
accountId: team.id,
action: 'download',
resourceType: 'report',
resourceId: reportId,
resourceName: 'Ranking por Marca — Mai/2026',
metadata: {
format: 'xlsx',
rowCount: 1420,
},
});

O que o trackAuditEventAction faz internamente​

  1. Obtém o usuário autenticado via getSupabaseServerClient().auth.getUser()
  2. Verifica se o usuário é membro da accountId informada consultando accounts_memberships
  3. Se ambas as checagens passarem, chama AuditService.trackEvent() com o userId resolvido
  4. Qualquer erro é capturado silenciosamente (padrão fire-and-forget)

Como ler eventos​

A leitura é feita via createAuditService().getEvents(), que usa o admin client internamente. Use isso em Server Components, Server Actions ou Route Handlers onde o contexto de servidor está disponível.

import { createAuditService } from '~/lib/server/audit/audit.service';

const svc = createAuditService();

const events = await svc.getEvents({
accountId: team.id,
resourceType: 'report',
action: 'view',
start: '2026-06-01',
end: '2026-06-30',
limit: 100,
offset: 0,
});

O retorno é um array de AuditEvent[]:

export interface AuditEvent {
id: string;
userEmail: string; // email resolvido via JOIN com auth.users
action: AuditAction | string;
resourceType: string;
resourceId: string;
resourceName: string | null;
metadata: Record<string, unknown>;
createdAt: string; // string ISO 8601
}

Filtros disponíveis​

FiltroTipoPadrãoDescrição
accountIdstring—Obrigatório. Filtra por conta.
resourceTypestringnullFiltra por tipo de recurso (ex. report)
actionstringnullFiltra por ação (ex. view)
startstringnullData inicial no formato YYYY-MM-DD
endstringnullData final no formato YYYY-MM-DD
limitnumber50Número máximo de registros retornados
offsetnumber0Offset para paginação

Segurança​

Por que authenticated só pode INSERT?​

O objetivo da auditoria é produzir um registro imutável e confiável. Se usuários pudessem ler, alterar ou deletar eventos, o histórico perderia valor como evidência. A política de RLS garante que:

  • Um usuário só insere eventos com o próprio user_id (enforçado pelo with check (user_id = auth.uid()))
  • Nenhum usuário autenticado consegue ler os eventos — nem os seus próprios — pelo cliente padrão
  • Toda leitura passa pelo service_role, que é chamado apenas em contexto de servidor

Por que a leitura usa o admin client?​

A função RPC get_audit_events tem grant execute apenas para service_role. O cliente padrão (authenticated) não consegue executá-la. Por isso, o AuditService.getEvents() usa getSupabaseServerAdminClient() — que nunca é exposto ao browser.

// audit.service.ts — leitura usa admin client
const admin = getSupabaseServerAdminClient() as any;
const { data, error } = await admin.rpc('get_audit_events', { ... });

Validação de membership antes de inserir​

O trackAuditEventAction verifica se o usuário é membro da conta antes de registrar o evento:

const { data: membership } = await client
.from('accounts_memberships')
.select('account_id')
.eq('account_id', params.accountId)
.eq('user_id', user.id)
.single();

if (!membership) return;

Isso impede que um usuário autenticado registre eventos em contas das quais não faz parte — mesmo que ele tente forjar uma chamada diretamente para a action.


Padrão fire-and-forget​

O trackAuditEventAction captura todos os erros silenciosamente:

try {
// ... lógica de auditoria
} catch {
// fire-and-forget: erros são silenciosos para não bloquear a ação principal
}

Quando usar esse padrão​

Use o padrão fire-and-forget quando a auditoria é secundária à ação principal. Por exemplo:

  • O usuário clica em "Exportar relatório" — a exportação deve acontecer mesmo que o registro de auditoria falhe
  • O usuário visualiza uma página — a visualização não deve ser bloqueada por um timeout no banco

Quando NÃO usar​

Se o registro de auditoria for parte do requisito de compliance (ex: o evento de auditoria precisa ser garantido antes de liberar o acesso ao recurso), não use fire-and-forget. Nesse caso, trate o erro explicitamente e decida se a ação principal deve ser bloqueada.


Extensibilidade — adicionando novos tipos​

Como resource_type e action são texto livre no banco, adicionar suporte a um novo recurso é simples:

1. Definir o novo resourceType como constante​

// apps/web/lib/server/audit/audit-resource-types.ts
export const AUDIT_RESOURCE_TYPES = {
REPORT: 'report',
LEAD: 'lead',
VEHICLE: 'vehicle',
DOCUMENT: 'document',
// Adicione aqui:
INSURANCE_POLICY: 'insurance_policy',
} as const;

export type AuditResourceType = typeof AUDIT_RESOURCE_TYPES[keyof typeof AUDIT_RESOURCE_TYPES];

2. Usar na chamada​

await trackAuditEventAction({
accountId: team.id,
action: 'create',
resourceType: AUDIT_RESOURCE_TYPES.INSURANCE_POLICY,
resourceId: policy.id,
resourceName: policy.number,
});

3. Nenhuma migration necessária​

O banco aceita qualquer string. A consistência é garantida pelo TypeScript — nenhuma alteração de schema é necessária.


Visualização em desenvolvimento​

Existe uma rota exclusiva para desenvolvimento em apps/web/app/[locale]/(dev)/auditoria/page.tsx. Ela retorna 404 em produção e não exige autenticação — usa o admin client diretamente para exibir todos os eventos do sistema.

http://localhost:3000/pt-BR/auditoria

A página exibe:

  • Seletor de conta (filtra por account_id)
  • Campo de tipo de recurso (filtra por resource_type)
  • Seletor de ação (filtra por action)
  • Tabela com os últimos 100 eventos: data, ação, tipo de recurso, recurso, time, usuário e metadados

Parâmetros de URL disponíveis:

ParamExemploDescrição
accountId?accountId=uuidFiltra por conta
resourceType?resourceType=reportFiltra por tipo de recurso
action?action=viewFiltra por ação

Limitações e próximos passos​

Ainda não implementado​

ItemStatusObservação
UI de produção (fora do protótipo)PendenteA rota /auditoria existe mas ainda não tem design final aprovado
Permissão granular por papel de membroPendenteHoje qualquer membro da conta pode acionar trackAuditEventAction; a leitura é restrita ao servidor
Retenção e arquivamento de eventos antigosPendenteEventos crescem indefinidamente; avaliar política de retenção (ex: 90 dias)
Tipagem automática da RPC via typegenPendenteO cast as any no AuditService.getEvents() será removido quando o Supabase CLI suportar tipagem de RPCs customizadas
Testes PgTAP para a RLS e a RPCPendenteAdicionar testes automatizados na suite supabase/tests/
Paginação no frontendPendenteO serviço suporta limit/offset, mas a UI ainda não expõe controles de paginação

Débito técnico conhecido​

O getSupabaseServerAdminClient() é convertido para any no AuditService.getEvents() porque o typegen do Supabase não gera tipos para RPCs customizadas automaticamente. Quando isso for resolvido (via tipagem manual ou atualização do CLI), o cast deve ser removido.