Construindo sistemas em tempo real no PostgreSQL: além do ORM

Equipe Atlas Verified
·
13 de fevereiro de 2026
·
8 min de leitura
Construindo sistemas em tempo real no PostgreSQL: além do ORM

Quando sua aplicação precisa de atualizações em tempo real, a resposta padrão em 2025 é recorrer a um serviço gerenciado: Firebase Realtime Database, Supabase Realtime, Pusher, Ably ou uma camada hospedada de WebSocket. Esses serviços abstraem a complexidade de enviar atualizações aos clientes conectados. Funcionam bem — até deixarem de funcionar.

No Atlas Verified, começamos com uma plataforma gerenciada de banco de dados que incluía assinaturas em tempo real. Conforme nosso sistema cresceu, atingimos os limites dessa abstração. Restrições de pools de conexões, problemas de confiabilidade em tempo real, necessidade de consultas espaciais PostGIS e desejo de lógica personalizada de gatilhos nos levaram na mesma direção: assumir o controle da camada de banco de dados.

Migramos para PostgreSQL auto-hospedado e construímos nosso sistema em tempo real usando seu mecanismo nativo LISTEN/NOTIFY, combinado com Server-Sent Events. Este artigo aborda por que tomamos essa decisão, como a arquitetura funciona e o que aprendemos.

Por que deixamos a plataforma gerenciada

Plataformas gerenciadas de banco de dados oferecem uma proposta atraente: autenticação, segurança em nível de linha, assinaturas em tempo real, APIs geradas automaticamente e painel — tudo pronto para uso. Para produtos em estágio inicial, isso é muito valioso. É possível construir uma aplicação funcional sem escrever uma linha de código de backend.

Os problemas surgem conforme a aplicação amadurece e os requisitos divergem das premissas da plataforma.

Limites de conexão. Plataformas gerenciadas frequentemente encaminham conexões por um gerenciador de pools que impõe seus próprios limites. Quando vários serviços de backend precisam de seus próprios pools — um servidor de API, um sistema de tarefas em segundo plano, um ouvinte em tempo real — as conexões disponíveis se esgotam rapidamente. Acabamos contornando as limitações do gerenciador em vez de resolver problemas reais do produto.

Confiabilidade em tempo real. O sistema em tempo real da plataforma gerenciada usava Change Data Capture no Write-Ahead Log do PostgreSQL. É uma arquitetura elegante, mas introduziu modos de falha que não podíamos controlar. A replicação baseada em WAL tem latência inerente. O serviço às vezes ficava atrasado, entregando atualizações desatualizadas. A reconexão após interrupções de rede era pouco confiável. E depurar esses problemas exigia acesso a infraestrutura que não controlávamos.

Requisitos de extensões. Nossa aplicação processa documentos comerciais com dados geográficos: localizações de portos, rotas de transporte, posições de navios. O PostGIS — a extensão espacial do PostgreSQL — é essencial para essas consultas. A plataforma gerenciada o suportava, mas precisávamos de versões e configurações específicas que nem sempre estavam disponíveis.

Lógica personalizada de gatilhos. Precisávamos de gatilhos de banco de dados que disparassem eventos específicos da aplicação — não apenas notificações genéricas de "linha alterada". Quando o status de processamento de um documento muda, precisamos notificar o cliente conectado específico que o enviou. O tempo real da plataforma era no nível da tabela; precisávamos do nível da entidade.

A decisão de migrar não foi por custo ou princípio. Foi pragmática: gastávamos mais tempo contornando as restrições da plataforma do que gastaríamos construindo os recursos.

LISTEN/NOTIFY: o mecanismo oculto de publicação/assinatura do PostgreSQL

O PostgreSQL inclui um mecanismo integrado de publicação/assinatura desde a versão 9.0, lançada em 2010. Apesar de ter mais de 15 anos, o LISTEN/NOTIFY é surpreendentemente pouco utilizado. A maioria dos desenvolvedores não sabe que existe.

O mecanismo é simples. Qualquer conexão pode executar LISTEN em um canal nomeado para assiná-lo. Qualquer conexão pode executar NOTIFY para enviar uma mensagem a todos os ouvintes daquele canal. A carga é uma string, limitada a 8,000 bytes.

O que torna LISTEN/NOTIFY poderoso para aplicações em tempo real:

Nenhuma sobrecarga de infraestrutura. Não há intermediário de mensagens separado para implantar, monitorar ou escalar. Se você tem PostgreSQL, tem publicação/assinatura. Sem Redis, RabbitMQ ou Kafka — para este caso de uso específico.

Garantias transacionais. Mensagens NOTIFY só são entregues quando a transação que as contém é confirmada. Se uma transação insere uma linha e envia uma notificação, mas é revertida, a notificação nunca é entregue. Essa propriedade não existe em intermediários externos sem orquestração cuidadosa.

Integração com gatilhos. Gatilhos do PostgreSQL podem chamar pg_notify durante a execução. Isso permite que o banco notifique ouvintes automaticamente sempre que dados específicos mudam, sem código da aplicação no caminho de gravação.

As limitações são reais e precisam ser compreendidas:

Sem persistência. Se ninguém estiver ouvindo quando um NOTIFY dispara, a mensagem se perde. Não há reprodução, fila de mensagens ou garantias de entrega para clientes desconectados. Os ouvintes precisam tratar reconexão e recuperação de estado.

Tamanho da carga. O limite de 8,000 bytes das notificações impede o envio de objetos grandes por NOTIFY. Enviamos cargas mínimas — nome da tabela, tipo de operação e chave primária — e deixamos o cliente buscar os dados completos se necessário.

Escopo de um único banco. LISTEN/NOTIFY funciona em uma única instância do PostgreSQL. Não abrange réplicas ou clusters. Em arquiteturas nas quais todas as gravações vão para um único primário, isso não é uma limitação.

Nossa arquitetura: gatilhos, SSE e multiplexação de canais

Veja como as peças se encaixam.

Gatilhos de banco de dados são instalados nas tabelas onde atualizações em tempo real importam: documentos, mensagens, conversas, notificações, verificações de remessas e outras. Quando uma linha é inserida, atualizada ou excluída, o gatilho dispara e chama pg_notify com uma carga JSON contendo o nome da tabela, tipo de operação, chave primária e colunas de filtro.

Uma conexão dedicada de escuta no nosso servidor de API executa LISTEN no canal de notificações e recebe todos os eventos de gatilhos. É uma conexão PostgreSQL dedicada e de longa duração — separada do pool de consultas — que apenas escuta notificações.

Um barramento de eventos recebe notificações do ouvinte e as encaminha aos clientes conectados com base nas assinaturas. Um cliente inscrito em documentos de um ID específico de remessa recebe apenas notificações sobre documentos daquela remessa.

Server-Sent Events entregam notificações ao navegador. Cada cliente conectado abre uma única conexão SSE ao servidor de API, especificando os canais e filtros desejados. O barramento compara notificações recebidas com as assinaturas dos clientes e envia os eventos correspondentes pelas conexões SSE adequadas.

Por que SSE em vez de WebSockets

Essa decisão tem nos deixado consistentemente satisfeitos. Server-Sent Events são mais simples que WebSockets em todas as dimensões relevantes ao nosso caso de uso.

Unidirecional por projeto. Nossas necessidades em tempo real vão do servidor ao cliente: o servidor informa quando dados mudam. O cliente não precisa enviar mensagens em tempo real ao servidor — usa solicitações HTTP padrão. O modelo unidirecional de SSE corresponde ao nosso requisito real.

Nativo de HTTP. Conexões SSE são solicitações HTTP padrão com um tipo de conteúdo específico. Funcionam por qualquer proxy HTTP, balanceador de carga e CDN sem configuração especial. WebSockets exigem HTTP Upgrade, que alguns proxies não tratam corretamente e alguns firewalls corporativos bloqueiam por completo.

Reconexão automática. A API EventSource do navegador trata a reconexão nativamente. Se a conexão cair, o navegador reconecta automaticamente com espera exponencial. Com WebSockets, você implementa a lógica de reconexão — e erra pelo menos uma vez.

A única área em que WebSockets vencem — comunicação bidirecional — simplesmente não é necessária para nós. Para aplicações que exigem mensagens em tempo real do cliente ao servidor, WebSockets são a escolha certa. Para aplicações que enviam notificações do servidor ao cliente, SSE é a escolha certa.

Construindo uma camada de consultas sem ORM

Quando deixamos a plataforma gerenciada, também deixamos sua API de consultas gerada automaticamente. Precisávamos de uma camada de acesso ao banco para nosso servidor de API.

As opções padrão são ORMs como Prisma, TypeORM ou Drizzle — ou SQL direto com um driver. Escolhemos um terceiro caminho: um construtor leve de consultas que oferece uma API encadeável enquanto gera e executa SQL direto internamente.

API familiar. Nosso código existente usava a biblioteca cliente da plataforma gerenciada. Uma API compatível significou alterações mínimas durante a migração. Consultas que antes passavam pelo cliente da plataforma passaram a usar nosso construtor com sintaxe idêntica.

Sem mágica. ORMs geram SQL que você não vê e às vezes não controla. Quando uma consulta é lenta, é preciso ler o SQL gerado, que frequentemente pouco se parece com a chamada do ORM que o produziu. Nosso construtor gera SQL previsível — cada método encadeado corresponde a uma cláusula SQL específica, e sempre é possível inspecionar a consulta gerada.

Junções incorporadas. Dados comerciais são relacionais. Uma remessa tem documentos, que têm nós, que têm verificações. Expressar seleções aninhadas e receber um resultado aninhado — sem escrever manualmente uma consulta JOIN complexa a cada vez — melhora significativamente a experiência do desenvolvedor.

O resultado é uma camada de consultas com poucas centenas de linhas, que trata a grande maioria das interações com o banco e produz SQL que sempre podemos ler e otimizar.

Orçamento de pools de conexões

Ao hospedar PostgreSQL por conta própria, você controla o orçamento de conexões. A configuração max_connections define o teto absoluto, e cada conexão consome memória.

Descobrimos que pensar nas conexões como um orçamento — um recurso finito compartilhado entre serviços — evita uma classe de problemas que atinge equipes sem planejamento.

Cada serviço declara seu número máximo de conexões. A soma deve permanecer bem abaixo do limite do PostgreSQL, com margem para conexões pontuais — sessões psql, scripts de migração, ferramentas de monitoramento. Parece óbvio, mas a maioria das equipes só faz isso após esgotar conexões em produção. Ao planejar esse orçamento antecipadamente, evitamos aquele chamado específico às 3 da manhã.

Migrações seguras em escala

Migrar de uma plataforma gerenciada para PostgreSQL auto-hospedado significou mover centenas de tabelas, funções, gatilhos e índices. Mas o maior desafio foi construir um sistema de migração que continuasse funcionando conforme a aplicação evoluísse.

Nosso sistema automatizado funciona assim: arquivos de migração são nomeados com registros de data e hora e ficam em um diretório dedicado. Um executor examina o diretório, compara com uma tabela que registra as migrações aplicadas e executa as pendentes em ordem. Cada migração ocorre dentro de uma transação — se falhar, é revertida integralmente e bloqueia a implantação até a correção.

O executor de migrações roda automaticamente em cada implantação, antes de a aplicação iniciar. Assim, alterações de esquema são aplicadas como parte da implantação, sem etapas manuais. Um desenvolvedor cria um arquivo SQL com data e hora, envia ao repositório e a próxima implantação o aplica.

Decisões importantes para manter isso seguro: migrações usam proteções idempotentes para não falhar se parcialmente aplicadas. Cada migração deve ser um script SQL autossuficiente que execute sem intervenção humana. E, se uma migração falhar, a transação é revertida por completo — a aplicação não inicia com uma alteração de esquema parcialmente aplicada.

O que ganhamos

Migrar para PostgreSQL auto-hospedado com tempo real personalizado exigiu um investimento significativo de engenharia. Mas os retornos foram substanciais:

Confiabilidade. Notificações em tempo real chegam em milissegundos após a mudança nos dados. Sem atraso de replicação WAL, sem indisponibilidade de serviços de terceiros no caminho crítico.

Facilidade de depuração. Quando algo dá errado, podemos rastrear todo o caminho: gatilho dispara, notificação é enviada, barramento encaminha, SSE entrega. Cada etapa está no nosso código, nos nossos registros, na nossa infraestrutura.

Flexibilidade. Adicionar atualizações em tempo real a uma nova tabela exige uma única migração SQL que instala um gatilho. Sem configuração de plataforma, painel de terceiros ou chamado de suporte.

Desempenho. Conexões diretas ao PostgreSQL com um pool ajustado superam conexões encaminhadas pela infraestrutura de uma plataforma gerenciada. A latência das consultas caiu significativamente após a migração.

A contrapartida é a responsabilidade operacional. Gerenciamos o banco, fazemos backups, monitoramos a replicação e planejamos capacidade. Para uma equipe familiarizada com operações de PostgreSQL, isso é administrável. Para uma que não é, as abstrações da plataforma gerenciada podem compensar suas limitações.


O Atlas Verified opera em PostgreSQL auto-hospedado com infraestrutura personalizada em tempo real, processando milhares de verificações diárias de conformidade comercial na cadeia global de suprimentos.

Gostou deste artigo?

Compartilhe com sua rede