Pular para o conteúdo
Voltar aos projetos

Luhmera

Sócio e arquiteto de software – atual

Visão geral

Plataforma SaaS multi-tenant para clínicas, com back-end em NestJS e PostgreSQL e front-end em React. Fui responsável pela arquitetura e pela implementação de ponta a ponta: isolamento entre clientes, processamento assíncrono confiável, auditoria, tempo real, segurança e testes.

  • Isolamento entre clientes provado por testes de integração contra Postgres e Redis reais.
  • Eventos entregues de forma confiável com outbox transacional e filas BullMQ.
  • No back-end: 37 módulos, cerca de 315 endpoints HTTP, 62 migrations versionadas e cerca de 730 casos de teste, além de um front-end com mais de 100 rotas.

Arquitetura

  1. Browser · React SPA

  2. HTTPS / WebSocket

    API NestJS · N instâncias

    Guards: auth · tenant · RBAC

  3. Dados e armazenamento

    PostgreSQL

    Redis

    Cache · Rate limit · Filas BullMQ · Adapter Socket.io

    S3

    URLs pré-assinadas

  4. Filas BullMQ

    Processamento assíncrono

    Workers

    gravam o resultado no PostgreSQL

    PDF · Puppeteer

Do navegador à API, com o PostgreSQL, o Redis e o S3 por baixo e os workers cuidando do processamento assíncrono.

Multi-tenant e isolamento

Um tenant por clínica
Cada clínica é um tenant, com os dados isolados por tenant_id e verificação em guards na entrada de cada requisição.
Isolamento provado
Uma suíte de testes de integração passa pela pilha HTTP real (guards, pipes e filtros) sobre Postgres e Redis reais e prova que uma clínica nunca lê nem altera dados de outra.
Acesso revogado na hora
Remover ou rebaixar um usuário vale já na requisição seguinte, sem esperar o token expirar.
Administração da plataforma
O time da plataforma tem uma área própria de administração dos tenants, com acesso controlado.

Eventos, outbox e filas

Outbox transacional
Outbox transacional: a mensagem é gravada na mesma transação da operação de negócio, então nada é publicado se a transação falhar.
Relay acordado pelo Postgres
Um relay leva as mensagens para o BullMQ, acordado por LISTEN/NOTIFY do Postgres e com polling de segurança a cada segundo.
Várias instâncias, sem duplicar
Lotes travados com FOR UPDATE SKIP LOCKED: várias instâncias podem rodar o relay ao mesmo tempo sem publicar a mesma mensagem duas vezes.
Publicação idempotente
Publicação idempotente (o id do job é o id da mensagem), ordem preservada e retentativa automática em caso de falha.
Filas por domínio
Filas BullMQ separadas por domínio (auditoria, documentos fiscais, notificações etc.), com workers que rodam em processos dedicados.
Cinco workers
Cinco workers BullMQ (auditoria, atendimentos, dicionário, documentos fiscais e notificações). Uma flag liga ou desliga workers e relay por processo, o que permite réplicas só de API.
Sem emissão em duplicidade
O worker de emissão de documentos fiscais usa o id do documento como chave de idempotência no provedor, então uma nova tentativa do job nunca emite duas vezes; o provedor é injetável, com uma versão falsa para testes.

Outbox

  1. Cliente

  2. requisição

    API

  3. BEGIN · COMMIT

    PostgreSQL

    grava a entidade e a mensagem outbox na mesma transação

  4. NOTIFY outbox (ou polling a cada 1 s)

    Relay

    SELECT ... FOR UPDATE SKIP LOCKED

  5. publica (jobId = id da mensagem)

    BullMQ

  6. processa

    Worker

    grava o resultado no PostgreSQL

A mensagem nasce na mesma transação da operação e só depois é publicada, sem duplicar mesmo com várias instâncias do relay.
private async publishBatch(): Promise<number> {
  return this.dataSource.transaction(async (manager) => {
    const rows: PendingRow[] = await manager.query(
      `SELECT id, queue, job_name, payload FROM infra.outbox
        WHERE published_at IS NULL
        ORDER BY seq
        LIMIT $1
        FOR UPDATE SKIP LOCKED`,
      [OUTBOX_BATCH_SIZE],
    );
    const published: string[] = [];
    for (const row of rows) {
      try {
        await withTimeout(
          this.queues.publish(row.queue, row.job_name, row.payload, { jobId: row.id }),
          PUBLISH_TIMEOUT_MS,
        );
        published.push(row.id);
      } catch (error) {
        await manager.query(
          'UPDATE infra.outbox SET attempts = attempts + 1, last_error = $2 WHERE id = $1',
          [row.id, (error as Error).message],
        );
        break;
      }
    }
    if (published.length > 0) {
      await manager.query('UPDATE infra.outbox SET published_at = now() WHERE id = ANY($1)', [
        published,
      ]);
    }
    return published.length === rows.length ? rows.length : 0;
  });
}
O relay do outbox: lê o lote com FOR UPDATE SKIP LOCKED, publica com o id da mensagem como jobId e, se uma publicação falha, para o lote para manter a ordem e tenta de novo na próxima passada.

Auditoria

Gravada junto com a escrita
Toda escrita (criar, alterar, excluir, restaurar) passa por um repositório base que grava o evento de auditoria no outbox, na mesma transação da operação: o evento existe se, e somente se, a escrita foi confirmada, e sobrevive a uma queda ou a uma falha do Redis antes de chegar à fila.
O que cada evento guarda
O evento guarda quem fez (usuário ou sistema), qual registro, a operação, o que mudou, quando a mudança aconteceu (não quando foi processada) e um id de correlação compartilhado por todas as escritas de uma mesma ação do usuário.
Delta, não diff
Na alteração, o delta é exatamente o que o chamador informou; na criação, o retrato inicial. Não há diff calculado nem leitura extra do registro.
Falhas também deixam rastro
Escritas que falham também deixam rastro: um evento com success=false e a mensagem do erro, gravado em uma conexão própria (a transação original já foi abortada pelo Postgres) e sem esconder o erro verdadeiro. Dá para desligar por chamada.
Exportação auditada
A exportação dos dados da clínica também é um ato auditado (operação EXPORT): o evento guarda o autor e as contagens, nunca os dados exportados.
Worker ordenado e idempotente
O worker de auditoria roda com concorrência 1, para a versão de cada registro (MAX + 1) seguir a ordem do commit, e o INSERT é idempotente por (id, occurred_at): uma reentrega da fila não duplica nada.
Tabela append-only e particionada
A tabela é append-only: sem exclusão lógica, nunca editada nem apagada. É particionada por mês em occurred_at (chave primária (id, occurred_at)), com partição default como rede de segurança e uma rotina idempotente, sob advisory lock, que garante o mês atual e os três seguintes. A migração converteu a tabela existente copiando os dados e tem reversão.
Segredos mascarados duas vezes
Segredos (senhas, hashes, tokens, certificados, credenciais) são mascarados em qualquer profundidade do JSON na gravação e mascarados de novo na leitura, para cobrir eventos antigos.
Duas formas de leitura
Leitura em duas formas: o histórico de um registro, que repete as mudanças em ordem para mostrar o valor anterior de cada campo, e a lista das últimas mudanças de um tipo, com paginação por cursor (occurred_at, id) e filtros de período (padrão: 90 dias no calendário da clínica, o que limita as partições lidas), operação e autor.
Só o que pode ser auditado
A consulta só devolve eventos bem-sucedidos da própria clínica e de 30 tipos de registro de uma lista explícita; qualquer outro tipo é recusado. Os nomes dos registros vêm do retrato de criação, mesmo os excluídos depois, e os ids de papel viram nomes de papel.
Conteúdo protegido por permissão
O conteúdo é protegido por permissão: quem tem audit.read vê quem fez o quê e quando, mas os campos dos registros sensíveis só aparecem para quem também pode ler o próprio registro. Um usuário e o vínculo dele com a clínica formam uma só linha do tempo.
Painel Histórico no front
No front, um botão Histórico (relógio) nas listas e nas linhas abre um painel com filtros de período e de operação, o valor anterior riscado ao lado do novo, um cadeado quando o conteúdo está oculto e o carregamento de mais itens. O botão só aparece para quem tem audit.read.
Testes com Postgres real
Testes de integração contra Postgres real cobrem a reconstrução do valor anterior, a ocultação de escritas com falha, o isolamento entre clínicas, a permissão por tipo de registro, o cursor, a linha do tempo do usuário, as partições e o worker pela fila; um teste unitário cobre o mascaramento.

Do escrita ao registro de auditoria

  1. Requisição

    um id de correlação para todas as escritas da ação

  2. COMMIT

    Repositório base

    grava o registro e o evento no outbox, na mesma transação

    Escrita com falha (etapa planejada)

    evento success=false em conexão própria, e o erro original segue

  3. LISTEN/NOTIFY, com polling de segurança

    Relay do outbox

  4. jobId = id do evento

    Fila audit

  5. INSERT ... ON CONFLICT DO NOTHING

    Worker de auditoria

    concorrência 1: versão = MAX + 1 na ordem do commit; reentrega é no-op

  6. audit_events

    append-only, particionada por mês em occurred_at

O evento nasce na mesma transação da escrita e só vira linha de auditoria depois de passar pelo outbox, pela fila e pelo worker.

Do pedido ao painel de histórico

  1. Pedido com permissão audit.read

  2. Filtros na consulta

    Clínica

    Tipos permitidos

    lista explícita; outro tipo é recusado

    Período e cursor

    só as partições do período são lidas

  3. eventos bem-sucedidos

    Reconstrução e máscara

    repete as mudanças em ordem para achar o valor anterior; segredos mascarados de novo

  4. JSON paginado

    Painel Histórico

    conteúdo escondido sem a permissão de leitura do registro

A leitura filtra por clínica, tipo permitido e período, reconstrói o valor anterior de cada campo e esconde o conteúdo de quem não pode lê-lo.
export async function writeAuditEvent(
  manager: EntityManager,
  event: AuditEventInput,
): Promise<void> {
  await manager.query(
    `INSERT INTO infra.audit_events
       (id, tenant_id, entity, entity_id, operation, changes, actor_id, version, success, correlation_id, occurred_at)
     SELECT $1::uuid, $2::uuid, $3::varchar, $4::varchar, $5::infra.audit_events_operation_enum, $6::jsonb,
            $7::varchar, COALESCE(MAX(version), 0) + 1, $8::boolean, $9::varchar, $10::timestamptz
       FROM infra.audit_events
      WHERE entity = $3::varchar AND entity_id = $4::varchar
     ON CONFLICT (id, occurred_at) DO NOTHING`,
    [/* id, tenantId, entity, entityId, operation, changes, actorId, success, correlationId, occurredAt */],
  );
}
O único ponto que grava a auditoria: a versão é MAX + 1 por registro e o INSERT é idempotente por (id, occurred_at), então uma reentrega da fila não duplica nada.
const SENSITIVE_KEY = /password|secret|token|hash|pfx|certificate|credential|credenciais/i;

export function redactSensitive(value: Record<string, unknown>): Record<string, unknown> {
  const walk = (node: unknown): unknown => {
    if (Array.isArray(node)) return node.map(walk);
    if (node === null || typeof node !== 'object') return node;
    return Object.fromEntries(
      Object.entries(node as Record<string, unknown>).map(([key, child]) => [
        key,
        SENSITIVE_KEY.test(key) && child !== null && child !== undefined ? REDACTED : walk(child),
      ]),
    );
  };
  return walk(value) as Record<string, unknown>;
}
O mascaramento, aplicado na gravação e de novo na leitura: percorre o JSON em qualquer profundidade e troca o valor das chaves que parecem segredo.
await this.dataSource.transaction(async (manager) => {
  await manager.query(`SELECT pg_advisory_xact_lock(hashtext('audit_events_partitions'))`);
  for (let offset = 0; offset <= AUDIT_PARTITION_MONTHS_AHEAD; offset++) {
    const from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + offset, 1));
    const to = new Date(Date.UTC(from.getUTCFullYear(), from.getUTCMonth() + 1, 1));
    const name = `audit_events_${from.toISOString().slice(0, 7).replace('-', '_')}`;
    await manager.query(
      `CREATE TABLE IF NOT EXISTS "infra"."${name}" PARTITION OF "infra"."audit_events" FOR VALUES FROM ('${from.toISOString()}') TO ('${to.toISOString()}')`,
    );
  }
});
A rotina de partições: sob um advisory lock, cria de forma idempotente o mês atual e os três seguintes, sem que réplicas concorram.

Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

Painel lateral de histórico de usuários, com filtros de período e de mudança e a lista de quem fez o quê e quando.
O histórico de uma lista: quem fez o quê e quando, com filtros de período e de tipo de mudança e carregamento por cursor.
Painel de histórico filtrado por exclusões, mostrando que o administrador removeu o acesso de um usuário.
Os filtros de período, de tipo de mudança e de autor: aqui só as exclusões, que também deixam rastro.
Painel de histórico de um usuário, mostrando a mudança de papel de Recepção para Financeiro, com o valor anterior riscado.
O histórico de um registro: cada mudança traz o valor anterior riscado ao lado do novo, e o id do papel aparece como o nome do papel.

Tempo real

Namespaces e salas
Atualizações em tempo real com Socket.io organizadas por namespaces e salas.
Escala horizontal
Com o adaptador Redis, um evento emitido em qualquer instância da API chega aos usuários conectados em todas as outras, o que permite escalar horizontalmente.
Emissão desacoplada
Qualquer serviço emite para uma sala sem depender do módulo do gateway.

Tempo real

  1. Serviço na instância 1

  2. Hub de tempo real

  3. Redis adapter

  4. Instância 1 · sockets

    Instância 2 · sockets

    Instância N · sockets

Um evento emitido em qualquer instância chega aos sockets de todas as outras pelo adaptador Redis.

Segurança

Identidade
Autenticação com JWT, MFA por TOTP (app autenticador, com QR code) e senhas com Argon2.
Segredos em repouso
Segredos de MFA e credenciais de integrações criptografados no banco com AES-256-GCM.
RBAC verificado por teste
RBAC com permissões por recurso e ação, e um teste que falha se alguma rota da API ficar sem permissão declarada.
Proteção da borda
Rate limiting distribuído em Redis, Helmet e validação de entrada com Zod; configuração validada na inicialização.
Força bruta em duas camadas
Anti-força-bruta em duas camadas: um limite por par IP + conta e outro, mais alto, só por conta, para o dono legítimo não ser bloqueado por quem ataca de outro IP.
Sessões em Redis
Sessões em Redis guardam só o hash do refresh token e têm idade máxima absoluta: renovar não prolonga a sessão para sempre. O access token leva apenas ids; as permissões vêm de uma consulta à sessão viva.
Token só no cabeçalho
O token viaja só no cabeçalho Authorization, nunca em cookie, o que elimina a superfície de CSRF por desenho. CORS por ambiente e CSP via Helmet.

Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

Lista de usuários da clínica, com nome, e-mail, papel, data de entrada e as ações de histórico, edição e remoção de acesso.
Os acessos por papel: cada pessoa tem um papel, que é um conjunto de permissões, e o botão de histórico só aparece para quem tem a permissão de auditoria.

Front-end

SPA com cadeia de guards
SPA em React com Vite e React Router, com cadeia de guards por rota: sessão, área da plataforma, clínica e permissão.
Estado do servidor e global
Dados do servidor com TanStack Query, com cache e política de retentativa configuradas; estado global leve com Jotai.
Formulários padronizados
Formulários com react-hook-form e Zod sobre um hook próprio, que padroniza carregamento, validação e envio.
Design system e temas
Design system com Radix/shadcn e Tailwind, com temas visuais configuráveis por clínica.
Detalhes de UX
Detalhes de UX: paleta de comandos, exclusão com desfazer, aviso de alterações não salvas e listas paginadas.
Roteamento declarativo
Roteamento declarativo: uma tabela de rotas, um executor de guards e um de layouts, com guards compostos (sessão, plataforma, clínica, permissão). Usa o roteador de dados do React Router para bloquear a navegação com alterações não salvas.
Token fora do React
O estado do token fica fora do React, em um store com subscribe lido por useSyncExternalStore, para o interceptor do axios não depender de hooks. Hooks próprios padronizam lista paginada, debounce, carga de formulário e a proteção contra perda de edição.

Guards do front

  1. Rota

  2. Sessão válida?

    não: vai para o login

  3. Área da plataforma?

  4. Clínica selecionada?

  5. Tem permissão?

  6. sim / não

    Página

    Acesso negado (etapa planejada)

Cada rota passa pela cadeia de guards: sessão, área da plataforma, clínica e permissão.

Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

Paleta de comandos aberta com a busca "usu", listando a tela de usuários e os campos da tela.
A paleta de comandos (Ctrl K): busca telas e também campos das configurações.

Qualidade e entrega

Testes com contêineres reais
Testes de integração com Testcontainers (PostgreSQL e Redis), Supertest e dados gerados com Faker; testes e2e do front com Cypress.
Migrations sempre em sincronia
Teste que garante que as migrations estão sempre em sincronia com as entidades.
API documentada e monitorada
Documentação da API em OpenAPI, publicada com Scalar; health checks de banco e Redis com Terminus.
PDFs e arquivos
Geração de PDFs no servidor com Puppeteer e arquivos no S3 com URLs pré-assinadas.
Docker e AWS
Docker e deploy na AWS com CloudFormation.
OpenAPI cruzada com o código
Um teste cruza a documentação OpenAPI com o código: cada rota precisa estar documentada, e a permissão declarada no documento tem de ser igual à do decorator do handler; rota sem permissão nem exceção explícita falha o teste.
CI e entrega
CI com lint, typecheck, build, testes de isolamento e de migrations e varredura de segredos (gitleaks fixado por hash), com cancelamento de execuções antigas e permissões mínimas. O deploy usa GitHub OIDC e SSM, sem credenciais estáticas.
Git hooks versionados
Git hooks versionados: no commit, lint só dos arquivos alterados e varredura de segredos; no push, typecheck do projeto e só os testes relacionados ao que mudou.
Operação
Operação: encerramento limpo (shutdown hooks), variáveis de ambiente validadas na subida, migrations e seed idempotentes antes de a API iniciar, e um comando para remover e restaurar uma clínica com confirmação explícita.
it('every endpoint declares a permission or opts out explicitly', () => {
  expect(endpoints.filter((e) => e.access === 'MISSING').map(label)).toEqual([]);
});

it.each(endpoints.map((e) => [label(e), e]))('%s', (_name, endpoint) => {
  const e = endpoint as EndpointInfo;
  const operation = doc.paths[e.path]?.[e.method];
  expect(operation).toBeDefined();
  const expected = e.access === 'public' || e.access === 'authenticated' ? undefined : e.access;
  expect(operation!['x-required-permission']).toBe(expected);
});
O teste que cruza a documentação com o código: toda rota declara uma permissão (ou uma exceção explícita) e a permissão documentada tem de ser a do handler.

Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

Documentação OpenAPI publicada com o Scalar, mostrando a rota de histórico de auditoria, um exemplo de chamada e a resposta.
A documentação da API, publicada com o Scalar a partir dos arquivos OpenAPI que o teste acima mantém fiéis ao código.