E-mail

Natanael Alves Gabriel · Brasil, UTC−3 · 4 anos colocando software em produção

Eu coloco o invariante onde ele não pode ser esquecido.

Engenheiro full-stack com foco em back end. Desenho esquemas que tornam o estado errado impossível e construo as aplicações em cima deles.

Aberto a vagas remotas. Brasil, UTC−3 — pego a tarde inteira na Europa e o dia inteiro de trabalho no leste dos EUA.

  1. Olipay Comércio de bebidas B2B + B2C — cinco aplicações sobre uma API NestJS. Desenvolvedor líder, 331 de 406 commits.
  2. TeamMove — pedidos & produtos O domínio de pedidos de um SaaS de força de vendas, do banco à tela. 99% dos commits.
  3. forty-monolito Plataforma de treino que funciona sem sinal. Dois níveis de row-level security no Postgres. Código aberto.
  4. Resumo dos Candidatos Transparência eleitoral sobre dados oficiais do TSE, com a incerteza modelada no schema. Código aberto.
  5. Nota 01 — multi-tenancy O mesmo mecanismo, e por que dois produtos decidiram o oposto sobre ele.
  6. Nota 02 — concorrência Reserva em duplicidade impedida por uma constraint de exclusão, não por uma verificação.

Trabalhos selecionados

2023 – 2026

Cinco projetos que, juntos, cobrem o alcance real do que eu faço: uma plataforma comercial de pagamentos, um domínio de vendas que foi meu de ponta a ponta num emprego, um produto multi-tenant cujo código você pode ler, um sistema de dados públicos e um jogo que foi lançado. Toda captura de tela desta página roda sobre dados de demonstração — nomes, placas, telefones e valores são fictícios.

Função
Desenvolvedor líder
Autoria
331 de 406 commits · monorepo
Superfícies
5 apps, 1 API

Olipay Comercial · código fechado

Plataforma de comércio B2B + B2C para distribuição de bebidas

Um catálogo só, atendendo consumidor de balcão e conta empresarial comprando a prazo — mais o estoque, a loja e o caixa por trás disso.

Cinco superfícies implantáveis rodam sobre uma única API NestJS: um app React Native, um PWA React para quem usa iPhone, um backoffice administrativo, um ponto de venda de loja física e a landing page. As cinco importam o mesmo pacote de contratos, então mudar um tipo de resposta quebra o build de qualquer app que ficou errado.

O que eu construí

A integração de pagamento Woovi (Pix), incluindo verificação de assinatura de webhook contra o formato de chave publicado e suporte a múltiplas contas recebedoras. O ponto de venda, adicionado ao monorepo como uma superfície nova. Todo o modelo de estoque — centros de armazenagem, escolha de depósito no checkout, embalagens de produto e incidentes de custo que separam imposto de custo de reposição. Um gerador de encarte promocional que monta o encarte dentro do app e exporta para imagem e PDF. Os endpoints de métricas e tendências por trás do dashboard, a validação de crédito B2B e o programa de pontos de fidelidade.

Como isso se sustenta

Os services dependem de interfaces de repositório ligadas por tokens de DI, em vez do Prisma direto, então os testes unitários mockam a interface e os de integração rodam contra um banco de verdade. Quatro guards de rota, autenticação em dois fatores, um filtro global de exceções, logging estruturado com rastreio de requisição e validadores próprios de CPF/CNPJ. Testado em quatro níveis, até execuções ponta a ponta com Maestro em dispositivo real.

O catálogo do consumidor e o dashboard por trás dele. Outras três superfícies — o app, o ponto de venda e a landing page — rodam sobre a mesma API.
  • NestJS 11
  • Prisma 7
  • PostgreSQL
  • React Native · Expo 52
  • React 19 · Vite
  • Turborepo
  • Pino
  • Maestro
Função
Autor único
Período
Mar 2023 – Abr 2026
Autoria
99% dos commits do módulo
Superfícies
Schema, API Java, cliente React

TeamMove — orders & products Empregador · código fechado

O domínio de pedidos de uma plataforma de vendas em campo, construído desde as tabelas

Durante oito meses em 2023 eu fui o único desenvolvedor da API Java — 440 dos 446 commits daquela janela são meus.

Um vendedor parado dentro da loja precisa lançar um pedido contra um catálogo que precifica diferente para cada cliente. Esse domínio fui eu que construí: pedidos, tipos de pedido, produtos, famílias e categorias de produto, tabelas de preço por cliente e os campos extras que um cliente acrescenta a um produto sem ninguém subir uma versão. Sete pacotes Java, 154 arquivos — 103 dos 104 commits que passaram por eles são meus.

A vertical inteira, em uma semana

Entrou de baixo para cima. Primeiro as tabelas e sua camada de acesso, depois os endpoints REST — listagem, detalhe, criação, atualização, remoção de item, mudanças de status — e as telas em React abriram no dia seguinte, em 28 de março de 2023. A mesma pessoa desenhou o schema, escreveu as queries em cima dele e construiu as telas que as leem, que é a única razão de um domínio desse tamanho ter entrado tão rápido: não havia contrato a negociar, porque não havia com quem negociar.

Pedido muda depois de aprovado

É essa a restrição em torno da qual a coisa toda gira. Um pedido editado depois da aprovação precisa conseguir dizer o que mudou, quem mudou e qual era o valor antes — então em julho de 2023 eu construí um livro-razão genérico de alterações por baixo dele: cinco tabelas registrando a operação, a tabela e a linha atingidas e o valor anterior e o novo de cada campo individual, atribuídos a um usuário e a um instante. Ele é agnóstico de tabela de propósito — carrega o nome da tabela como dado, então qualquer outra coisa no sistema passa a ser auditável apontando para ele em vez de escrever mais um log. Reconstruir o histórico de um pedido a partir de linhas de log em texto livre é chute; isso é uma query.

Depois eu mesmo tirei ele do Java

Dois anos depois a plataforma migrou para um back-end TypeScript e o meu próprio módulo estava na lista. Eu portei — o endpoint de listagem em maio de 2025, o de detalhe na semana seguinte — e continuei ajustando do outro lado, trocando o ORM por query bruta quando a listagem ficou lenta. A maior parte do que eu escrevi passou a ser mantida por outra pessoa. Esse é o pedaço que eu projetei, substituí e depois tive que conviver do outro lado.

E os outros três anos e meio

Pedidos e produtos é um módulo. Em volta dele está todo o resto do que eu fiz na mesma plataforma: a agenda e sua visão de mês, o construtor de checklists e o runtime que preenche um deles, o fluxo de chamados com suas etapas de briefing e aprovação, os rankings de campanha, o cálculo de metas dividido entre totais do dia e do mês, rotinas agendadas, o pipeline de impressão que transforma um checklist preenchido num PDF assinável e uma API externa para parceiros. Quatro anos em quatro repositórios — o cliente React, a API Java, o back-end TypeScript que a substituiu e a API externa para parceiros. Eu era o mantenedor principal do cliente e do domínio de pedidos; a API Java já tinha seis anos quando eu cheguei. O escopo completo está abaixo →

  • Java · WildFly
  • JDBC
  • PostgreSQL
  • React
  • Redux
  • Express
  • Prisma
  • TypeScript
Função
Autor único
Tamanho
252 arquivos
Código
~67 mil linhas TS
Status
Público

forty-monolito

Plataforma de treino para personal trainers

Dois apps sobre uma API: o personal toca o negócio e o aluno registra cada série na academia — onde não pega sinal.

O personal recebe agenda, CRM de clientes e controle de receita. O aluno recebe um app mobile-first que funciona offline first, porque isso não é um luxo no subsolo de uma academia: as séries são gravadas localmente e sincronizam quando a conexão volta.

O isolamento entre tenants é imposto em dois níveis de row-level security do PostgreSQL — por personal e por aluno — para que um WHERE esquecido não vaze a carteira de clientes de outro personal. O app guarda respostas de questionário de saúde, o que torna essa fronteira uma obrigação de LGPD, não uma preferência. Os testes de integração rodam contra um Postgres real via Testcontainers e verificam o isolamento diretamente; o Playwright cobre os fluxos de ponta a ponta.

À esquerda, o lado do personal. À direita, o que o aluno vê na academia — a mesma API, outro trabalho.
  • Next.js 16
  • NestJS 11
  • PostgreSQL 17 · RLS
  • Prisma 7
  • Better-Auth
  • Tailwind v4
  • Playwright
  • Testcontainers
Função
Autor único
Código
~3 mil linhas Python
Testes
16, em 5 arquivos
Status
Público

Resumo dos Candidatos

Transparência eleitoral sobre dados abertos oficiais

Só mostra o histórico de votações de um parlamentar quando consegue provar que o candidato e o mandatário são a mesma pessoa. Quando não consegue, avisa.

Uma plataforma pública para as eleições gerais de 2026. Para cada candidato, agrega as propostas publicadas e — para quem já tem mandato e busca reeleição — o histórico real de votações, presença e gastos, inteiramente a partir dos dados abertos oficiais do TSE e da Câmara dos Deputados.

A parte interessante é o CandidateMandateLink: uma aresta materializada e auditável que afirma “esta candidatura de 2026 é a mesma pessoa que ocupa este mandato”, carregando o método de correspondência, um índice de confiança e sua procedência. Abaixo do limiar aceito, a página exibe incumbência não confirmada em vez de anexar um histórico que pode ser de outra pessoa. Atribuir os votos de um político a outro é o único bug que este projeto não pode lançar, então a incerteza é modelada no schema em vez de ser resolvida no chute.

O registro de candidatos de 2026 só existe depois que o prazo de registro fecha, então o sistema inteiro foi construído e validado contra os dados de 2022 e 2024 — mesmos schemas — e passa a apontar para 2026 mudando um único valor de configuração. Os coletores são idempotentes e amigáveis a cron, escrevendo num livro-razão de ingestão bruta que mantém cada fato derivado rastreável até o arquivo de onde veio.

A tabela é o argumento. Nada sobre a correspondência fica implícito:

CREATE TABLE candidate_mandate_link (
  id                      uuid PRIMARY KEY,
  sq_candidato            varchar(32)      NOT NULL REFERENCES candidacy,
  mandate_id              uuid             NOT NULL REFERENCES mandate,
  person_id               uuid                      REFERENCES person,
  match_method            matchmethod      NOT NULL, -- cpf_exact | titulo_exact
                                                     -- | probabilistic | manual
  confidence_score        double precision NOT NULL,
  confidence_tier         confidencetier   NOT NULL, -- auto_strong | auto_weak
                                                     -- | review
  is_incumbent_reelection boolean          NOT NULL,
  pipeline_version        varchar(32),
  resolver                varchar(64),
  resolved_at             timestamptz      NOT NULL DEFAULT now(),
  UNIQUE (sq_candidato, mandate_id)
);

O que a página lê é o confidence_tier, não o confidence_score: uma linha em review aparece como incumbência não confirmada em vez de ser anexada em silêncio. match_method e pipeline_version fazem com que um vínculo errado possa ser reencontrado depois e decidido de novo, e a constraint única em (sq_candidato, mandate_id) é o que impede duas execuções do resolvedor de discordarem dentro da mesma tabela.

  • Python 3.11
  • FastAPI
  • SQLAlchemy 2
  • Alembic
  • Postgres · pg_trgm
  • polars
  • rapidfuzz
  • Typer
Função
Autor único
Engine
Godot · GDScript
Lançado
2025
Status
Publicado, v0.4.0

Detetive Sonora

Jogo lançado — itch.io e GameJolt

Sete pistas são os sete graus da escala. Um acorde toca; você voa para a pista que corresponde a ele. Ouvido absoluto nunca é necessário — só o intervalo.

Um jogo de treinamento auditivo disfarçado de história de detetive, feito como o meu TCC. Nove músicas e 121 acordes são escritos como dado puro, três por nível de dificuldade e sorteados a cada partida. Cada nível troca a tonalidade e o instrumento — dó no piano, sol no violão, ré na guitarra — então decorar altura absoluta não serve de nada e só a audição relativa se aproveita. A janela de reação fecha de cinco segundos para três, e abaixo de 70% o nível se repete.

Três atos, cada um abrindo com uma ligação inteiramente dublada do ministro da defesa, sobre um detetive atrás de uma aeronave roubada. No terceiro ato você descobre que o detetive é cego — que é o argumento que o design inteiro já estava fazendo.

Uma partida em andamento, e a capa. O avião fica na pista que corresponde ao acorde que está tocando; as sete pistas são os sete graus da escala.
  • Godot 4.4
  • GDScript
  • Windows
  • pt-BR

Quer detalhe de algum destes, ou das partes que eu não posso mostrar em público? Me pergunta →

Onde eu entreguei

Emprego · código fechado

Duas empresas cujos produtos fui pago para construir e que não me pertencem. Sem código e sem capturas de tela — então o que vem aqui é escopo: o domínio, as frentes em que trabalhei e as restrições que moldaram o código. Uma parte disso, o domínio de pedidos que foi meu de ponta a ponta, está detalhada acima.

Função
Desenvolvedor full-stack
Período
Fev 2022 – Abr 2026
Superfícies
Cliente web, 3 back-ends
Mercado
Brasil

TeamMove Empregador · código fechado

SaaS de vendas em campo e trade marketing

Durante dois anos, a API Java que estava sendo substituída e a API TypeScript que a substituía estiveram as duas em produção. Eu commitei nas duas no mesmo mês, em vinte e quatro de vinte e cinco meses.

Software para equipes cujo trabalho acontece fora do escritório — vendedores e promotores visitando lojas. Agendamento de visitas e agenda, checklists dinâmicos preenchidos no local, um fluxo de chamados com etapas de briefing e aprovação, metas de venda acompanhadas por dia e por mês, rankings de campanha, planos de ação e clusterização de clientes que define quem enxerga o quê.

Trabalhando em cima da migração

Entrei na API REST em Java e fiquei durante a mudança para uma API em TypeScript. Nenhuma das duas foi desligada de uma vez: as funcionalidades caíam no lado que fosse dono delas, o que significava manter os dois schemas na cabeça e garantir que uma query de relatório e sua substituta respondessem à mesma pergunta. Essa é a parte sem glamour de uma migração e é a maior parte do trabalho — os dois últimos anos do meu histórico lá correm pelos dois trilhos em paralelo.

  • React · Vite
  • TypeScript
  • Express
  • Prisma
  • PostgreSQL
  • NestJS
  • Java · WildFly
  • AWS S3
Função
Desenvolvedor full-stack
Período
Abr 2026 – hoje
Base de código
Java EE, desde 2012
Mercado
Municípios brasileiros

Celk Empregador · código fechado

Sistemas de saúde pública para secretarias municipais

Um campo obrigatório numa ficha de investigação não é preferência de interface. É o que as regras de notificação dizem que precisa ser coletado, e a ficha está errada até concordar com elas.

Software de saúde municipal — vigilância sanitária, farmácia e controle de estoque, vacinação — usado no dia a dia pelas secretarias municipais de saúde. Um monolito Java EE com telas Wicket renderizadas no servidor, com o primeiro commit em 2012 e hoje bem acima de cinquenta mil commits, dividido em mais de dez módulos Maven. — com o trabalho novo indo para serviços em Quarkus escritos ao lado dele. Eu trabalho dos dois lados dessa fronteira, que é a mesma emenda da migração da TeamMove ali em cima: duas stacks em produção ao mesmo tempo, e cada funcionalidade caindo na que for dona dela.

No que eu trabalho

Fichas de investigação de agravos: fichas estendidas, os campos sem os quais um caso não pode ser encerrado e o cálculo de período por semana epidemiológica em vez de mês do calendário. O lado de licenciamento da vigilância sanitária — taxas de requerimento, licenças de transporte, registro de atividades veterinárias e as conferências que precisam passar antes de um relatório sequer ser gerado. Do lado da farmácia: quantidades decimais para medicamento prescrito e dispensado, juntada de registros duplicados de medicamento, seleção de lote que carrega a própria validade, relatórios de saldo e de análise de estoque, e a codificação de produto que o envio federal espera.

O que o domínio cobra de você

Toda mudança chega como um chamado sobre um software que uma cidade já está rodando, e sai por code review antes de chegar em alguém. Alterações de schema vão como migrações Flyway presas a um padrão de nomenclatura e DDL, porque um script fora do padrão não quebra o seu build — quebra uma atualização. A maioria das minhas correções aqui é pequena e exata pelo mesmo motivo: um rótulo errado num lote de vacina ou uma conferência faltando numa licença não é bug cosmético nesse domínio.

  • Java EE
  • Quarkus
  • Apache Wicket
  • EJB
  • Hibernate
  • PostgreSQL
  • Flyway
  • JasperReports
  • JBoss

Produtos feitos para vender

Comercial · código fechado

Três sistemas que eu projetei e desenvolvo como produto, e não como trabalho avulso de cliente. O código continua fechado; o raciocínio, não. Toda captura de tela abaixo roda sobre dados de demonstração.

Modelo
Multi-tenant compartilhado
Isolamento
RLS do Postgres
Mercado
Brasil
Status
Ainda sem cliente pagante

shop-schedule Privado

Software de agendamento para negócios que vendem horários

Dois clientes podem tocar no último horário das 15:00 no mesmo segundo. Quem impede é o banco de dados — não uma verificação na aplicação.

Agenda, cadastro de clientes, catálogo de serviços, lembretes e receita por regime de caixa, para qualquer negócio cujo produto é um horário marcado. Vários estabelecimentos dividem uma instância, isolados por row-level security.

A disponibilidade é calculada a cada leitura, nunca armazenada. Não existe tabela de horários: o tempo livre é derivado como horário de funcionamento ∩ horário do recurso − folgas − agendamentos ativos, e então fatiado pela duração do serviço. Uma cópia armazenada significa que uma folga cancelada deixa um horário velho na página pública e alguém marca um atendimento que o estabelecimento não tem como honrar. O gerador vive em um pacote compartilhado importado tanto pela API quanto pelo front-end, então a página pública de agendamento e o balcão não têm como discordar sobre o que está livre.

As duas telas que nunca podem discordar. Repare nos títulos das colunas — o que se agenda é uma pessoa, uma sala ou uma máquina — e note que 09:00, 09:30 e 10:00 ficam encostados sem se sobrepor.
  • NestJS
  • Prisma
  • PostgreSQL 17
  • btree_gist
  • React · Vite
  • pnpm workspaces
Formato
Template reutilizável
Consumidores
3 apps, 1 contrato
Mercado
Brasil
Status
Ainda sem cliente pagante

ecommerce-base Privado

Base de e-commerce feita para o Brasil, clonada por cliente

Um endpoint não consegue retornar um campo que o contrato dele não declara. Não por convenção — o campo é removido na saída.

Uma API NestJS, uma vitrine Next.js e um admin React em um monorepo, lidando com Pix, cartão, boleto, CPF/CNPJ e frete por CEP. É um template: clonado por cliente e depois configurado em vez de modificado — marca, identidade jurídica, regras de checkout, provedor de pagamento e transportadora são todos dados.

Um schema Zod por contrato vive em um pacote compartilhado. O nestjs-zod transforma cada um em um DTO, então uma única definição comanda a validação da requisição, a serialização da resposta e o documento OpenAPI publicado, enquanto os dois front-ends inferem seus tipos da mesma fonte. O serializador remove tudo o que o contrato não declara, o que significa que um hash de senha pego por um select descuidado do Prisma nunca chega na rede. Segurança por construção, não por revisão de código.

Um clone, vestido como uma loja chamada Ateliê. A marca, os meios de pagamento oferecidos e as regras de frete são todos configuração — nada aqui é um fork.
  • NestJS 11
  • Next.js 16
  • React 19
  • Zod · nestjs-zod
  • Prisma 7
  • Redis
  • MinIO
Modelo
Uma instância por oficina
Código
~19 mil linhas de TS
Testes
305, todos passando
Mercado
Brasil
Status
Ainda sem cliente pagante

smart-workshop Privado

Sistema de gestão para oficinas de veículos

O cliente aprovou R$ 393,70. No meio do serviço o mecânico descobre que as pastilhas de freio acabaram. Ninguém entrega a moto por R$ 592,70 até o cliente dizer sim uma segunda vez.

Clientes e seus veículos, ordens de serviço, um livro-razão de peças, o caixa do balcão e o contas a receber do mês — para o tipo de oficina brasileira que ainda funciona no bloquinho de papel. Um único schema cobre mecânica geral, funilaria, pintura e auto elétrica, o que só funciona porque os fluxos de seguradora ficaram de fora por decisão: sem eles, um serviço de funilaria é um serviço de mecânica que demora mais.

Uma lei, compilada em uma trava

O Código de Defesa do Consumidor é explícito: o cliente não pode ser cobrado por serviço que não autorizou. Aqui isso não é um checkbox. Uma aprovação é uma linha — quem aprovou, por qual canal, em qual valor, carregando um snapshot em JSON de todos os itens como estavam — e uma ordem não chega a entregue enquanto o total atual passar do último valor aprovado. A comparação é estritamente maior, porque cobrar menos que o aprovado nunca foi o problema. A verificação é uma função pura no pacote compartilhado, então o navegador avisa usando exatamente o predicado que a API impõe, e APPROVED é inalcançável a não ser pela chamada que grava o registro — um status que dá para setar direto é justamente o booleano que este desenho se recusa a ser.

Isolamento por ausência

Os outros sistemas multi-tenant desta página colocam uma policy no banco e um id de tenant em cada linha. Este não tem nem um nem outro, de propósito: cada oficina ganha o próprio banco e a própria instância, então o raio de alcance de um erro é um fato sobre o deploy e não uma query que alguém precisa acertar toda vez. A aplicação nunca resolve um tenant e a palavra não aparece no schema. O custo é real e foi escrito antes de ser aceito — N bancos para migrar em vez de um. O mesmo mecanismo da Nota 01, defendido pelo outro lado.

Duas afirmações tornadas visíveis. A ordem foi aprovada em R$ 393,70 pelo WhatsApp e agora soma R$ 592,70, então o Entregar está desabilitado e o aviso diz por quê. A pastilha que causou isso mostra 11 em estoque, 1 reservado e 10 disponível — três números recalculados a partir das quatro movimentações logo abaixo, nunca incrementados.
  • NestJS 11
  • Prisma 7
  • PostgreSQL 17
  • React 19 · Vite 8
  • Zod 4
  • TanStack Query
  • Tailwind v4
  • pdfmake

Notas técnicas

Três decisões, em detalhe

Os três mecanismos que mais aparecem no meu trabalho, e o que eu de fato aprendi construindo em cima deles.

Nota 01 · Multi-tenancy

Dois produtos, um mecanismo, decisões opostas

Tanto o forty-monolito quanto o shop-schedule isolam tenants com row-level security do PostgreSQL, pelo mesmo motivo: a ameaça não é um atacante, é uma consulta que esqueceu o WHERE. Filtrar na aplicação transforma cada consulta em uma nova chance de vazamento. Uma policy no banco torna a fronteira independente do código que roda acima dela.

A policy em si não tem nada de especial — esta é a do forty-monolito, aplicada em toda tabela com escopo de tenant:

CREATE POLICY tenant_isolation ON "appointment"
  USING      ("trainerId" = app_current_trainer())
  WITH CHECK ("trainerId" = app_current_trainer());

O que vale registrar é que os dois projetos então tomaram a decisão oposta sobre FORCE ROW LEVEL SECURITY — e os dois estavam certos.

Onde o isolamento de fato mora

Uma policy só prende um role que não consegue contorná-la. Três coisas precisam valer, ou o isolamento é decorativo:

  1. O role de runtime não é dono de nada. Donos de tabela furam a RLS silenciosamente, e um role com BYPASSRLS ignora toda policy do banco. As migrations rodam como dono; a aplicação conecta com um role separado, criado NOSUPERUSER NOBYPASSRLS, que não é dono de tabela nenhuma.
  2. O contexto do tenant é definido com SET LOCAL, dentro de uma transação. O Prisma faz pool de conexões, então um SET solto sobrevive à requisição e a próxima — de outro tenant — herda o valor. Essa é a falha que parece funcionar em desenvolvimento e vaza sob carga. Existe um teste de regressão que intercala duas requisições e verifica que a segunda não enxerga as linhas da primeira.
  3. Contexto ausente falha fechado. A policy compara contra current_setting('app.tenant_id', true)::uuid, que é NULL quando não está definido — e tenant_id = NULL nunca é verdadeiro, então uma consulta sem escopo retorna nada em vez de tudo. Nunca escreva a saída de emergência OR current_setting(...) IS NULL; ela converte a falha segura na falha catastrófica.

A divergência

FORCE ROW LEVEL SECURITY estende as policies também ao dono da tabela. O forty-monolito ativa isso e concede BYPASSRLS explicitamente ao role de migration — apropriado ali, porque aquele sistema guarda respostas de questionário de saúde e a redundância paga o próprio custo.

O shop-schedule deliberadamente não ativa, e o motivo está escrito na especificação para que ninguém “conserte” isso depois. O FORCE se aplica ao dono, que é justamente quem roda migrations, seeds e a administração da plataforma. O seed precisa criar um estabelecimento antes que qualquer contexto de tenant possa existir, então sob FORCE ele simplesmente não roda. O único caminho de volta é conceder BYPASSRLS ao dono — o que torna o FORCE inócuo de novo. Não compra nada contra a ameaça que aparenta endereçar e custa ao operador as próprias ferramentas.

A proteção que é real nos dois casos é estrutural: o role da aplicação não é o dono. O FORCE é uma segunda fechadura numa porta cuja primeira fechadura é a que está fazendo o trabalho — vale colocar quando o dado é sensível, não vale quebrar o seu script de seed por causa dela.

Nota 02 · Concorrência

Deixe o banco dizer não

Dois clientes abrem a página de agendamento no mesmo instante e os dois pegam o último horário das 15:00. Isso não é hipótese — é o bug mais caro que um produto de agendamento pode ter, porque ninguém descobre até as duas pessoas estarem paradas no balcão.

O instinto é verificar se há conflito e então inserir. Isso não tem como funcionar no nível de isolamento que um ORM usa por padrão: as duas transações leem um resultado vazio, as duas concluem que o horário está livre, as duas escrevem. A janela é pequena e completamente real. Repetir a tentativa, travar a tabela ou serializar o endpoint — todas trocam um problema de correção por um de vazão.

A garantia pertence ao schema:

CREATE EXTENSION IF NOT EXISTS btree_gist;

ALTER TABLE "Appointment" ADD CONSTRAINT appointment_no_overlap
  EXCLUDE USING gist (
    "resourceId" WITH =,
    tstzrange("startsAt", "endsAt", '[)') WITH &&
  )
  WHERE (status NOT IN ('CANCELLED', 'NO_SHOW'));

Quatro detalhes ali sustentam a estrutura, e eu errei dois deles na primeira vez:

  • O intervalo é semiaberto'[)'. Um atendimento das 15:00–15:30 e outro das 15:30–16:00 não se sobrepõem. Intervalos fechados custam ao estabelecimento um horário por hora.
  • endsAt é armazenado, não calculado na leitura. Uma exclusion constraint precisa de uma coluna real, e toda consulta de sobreposição quer um índice sobre ela.
  • A cláusula WHERE é o motivo de cancelar liberar o horário sem apagar a linha — o histórico fica, o tempo volta.
  • O Prisma não consegue expressar isso. Vai para a migration como SQL escrito à mão, e a suíte de integração verifica que a constraint existe — porque um schema produzido por db push a derrubaria em silêncio, e o produto pareceria funcionar.

A aplicação continua verificando disponibilidade antes de inserir, para que quem perde a corrida receba uma mensagem legível em vez de uma violação de constraint. Mas a verificação é uma cortesia. A constraint é o que é verdade.

Esse é o formato ao qual eu sempre volto: coloque o invariante onde ele não pode ser esquecido, e deixe a camada de aplicação cuidar de ergonomia em vez de correção. Código que você precisa lembrar de escrever é código que uma hora não é escrito.

Nota 03 · Estoque

Um contador só está certo se ninguém esquecer

No smart-workshop, o estoque é um livro-razão que só recebe inserções. Saldo e reservado não são colunas que alguém incrementa: são projeções recalculadas a partir das movimentações a cada escrita. O motivo é que um contador só se mantém correto se todo caminho que mexe numa linha lembrar do decremento correspondente — e quando um esquece, nada quebra. A conta simplesmente desanda, e você descobre quando a mesma peça já foi prometida duas vezes.

Dois detalhes fazem o trabalho de verdade. A reserva é liberada porque a ordem saiu do conjunto de status que reserva, então não existe passo de liberação para alguém esquecer. E uma linha do razão só pode ser consumida uma vez porque esse vínculo é um índice único, não um método de serviço bem escrito. Os dois são o mesmo movimento das Notas 01 e 02: a garantia mora num lugar onde não dá para pular, e o código acima dela pode se preocupar com ergonomia.

O painel de estoque nas imagens do smart-workshop é esta nota visível — 11 em mãos, 1 reservado, 10 disponíveis, os três derivados das quatro movimentações listadas abaixo deles. Voltar para o smart-workshop →

Com o que eu trabalho

A partir dos projetos acima

Nada está nesta lista por eu ter lido a documentação uma vez. Cada item aparece em um projeto desta página, e a linha embaixo de cada grupo diz onde.

Linguagens
  • TypeScript
  • JavaScript
  • Java
  • Python
  • SQL
  • GDScript

TypeScript na maior parte dos sistemas desta página. Java é a API REST original da TeamMove e o monolito de 2012 da Celk — entreguei código em produção nos dois.

Back-end
  • Node.js
  • NestJS
  • Express
  • FastAPI
  • Java EE
  • EJB
  • Apache Wicket
  • REST · OpenAPI

Seis dos sistemas desta página são NestJS. Wicket e EJB são do que é feito o monolito Java EE da Celk — uma base de código com mais de cinquenta mil commits na qual eu entrego mudanças.

Front-end e mobile
  • React
  • Next.js
  • React Native · Expo
  • Vite
  • Tailwind
  • TanStack Query
  • three.js

Uma vitrine web, um app nativo e um ponto de venda de loja física, todos tipados pelo mesmo pacote de contratos compartilhado — então não têm como discordar sobre o que a API devolve.

Dados e persistência
  • PostgreSQL
  • Prisma
  • SQLAlchemy
  • Hibernate
  • Alembic
  • Flyway
  • Redis
  • RLS
  • btree_gist
  • pg_trgm
  • polars
  • rapidfuzz

Postgres embaixo de todo sistema desta página. RLS e btree_gist estão na lista porque as Notas 01 e 02 são sobre o que eles tornam impossível.

Testes
  • Playwright
  • Testcontainers
  • Maestro

Integração contra um Postgres de verdade em container, e não contra um mock dele, porque um mock não consegue impor a policy que o teste existe para provar.

Ferramental e o resto
  • Turborepo
  • pnpm workspaces
  • Maven
  • Zod · nestjs-zod
  • Better-Auth
  • Pino
  • Typer
  • AWS S3
  • MinIO
  • JBoss · WildFly
  • JasperReports
  • pdfmake
  • Godot

Encanamento de monorepo, armazenamento de objetos e três pipelines de PDF distintos — porque um checklist, uma ordem de serviço ou um encarte que ninguém consegue imprimir é funcionalidade que não foi entregue.

Protótipos jogáveis

Clique em qualquer quadro para jogar

Fatias verticais, cada uma feita para responder a uma pergunta de design antes de qualquer conteúdo ser produzido. Arquivo único, sem dependências, sem etapa de build — então rodam aqui mesmo, nesta página.

Protótipo de furtividade em vista de cima: o cone de visão de um guarda varre uma planta escura enquanto o jogador se esconde na sombra, com um medidor de alerta enchendo no topo.

Signal & Shadow

2D · IA de furtividade

Guardas que genuinamente não sabem onde você está. Enxergam só o que cai dentro do cone e ouvem só o que é alto o bastante, e então saem para conferir uma memória que já está errada. Dois arquivos, do começo ao fim.

Protótipo de furtividade em 3D: o jogador agachado na sombra de uma fase greybox iluminada de azul, enquanto dois guardas patrulham com cones de visão visíveis e um objetivo âmbar brilha dentro de um cofre.

Signal & Shadow 3D

three.js · zero dependências

A mesma pergunta, com uma câmera atrás da qual dá para ficar. Entrega um único arquivo HTML autocontido que roda direto do sistema de arquivos, com um servidor de desenvolvimento em umas quarenta linhas e uma suíte de testes de simulação — e nenhuma dependência.

Protótipo de reconhecimento em primeira pessoa em monocromia âmbar: a vista pelos olhos de um soldado por um beco, com uma lista de quatro soldados e uma linha do tempo navegável do passado registrado por eles.

Borrowed Eyes

Reconhecimento em segunda pessoa

Toda câmera é a cabeça de um soldado. Vista os olhos dele, volte no que ele já viu e capture o mensageiro vivo. Construído especificamente para vencer um modo de falha nomeado — reconhecimento degenerando em caça a objeto escondido — antes de qualquer fase ser feita.

Prólogo monocromático: um quadrado preto se inclina para a esquerda sobre uma grade clara, deixando rastro de movimento, com uma legenda que diz “segure para inclinar — é tudo que você pode fazer”.

Bystander

Doze segundos

Um prólogo em que você só pode se inclinar e observar. A legenda nunca lista uma tecla que você ainda não tem permissão de apertar, porque no começo você pode fazer exatamente uma coisa. Três arquivos.