PGlite: PostgreSQL rodando diretamente no browser e em Node.js

1. O que é o PGlite e por que ele existe?

Historicamente, desenvolvedores web que precisavam de um banco de dados relacional no frontend recorriam ao IndexedDB (API nativa do browser) ou ao SQLite via WebAssembly (como o SQL.js). O PostgreSQL, apesar de ser um dos bancos mais poderosos e amplamente adotados no backend, nunca havia sido uma opção viável no navegador — até agora.

O PGlite é uma implementação do PostgreSQL compilada para WebAssembly (WASM) com bindings JavaScript leves. Diferente de soluções que apenas emulam SQL, o PGlite executa o motor real do PostgreSQL dentro do ambiente do browser ou em runtime Node.js, oferecendo suporte a sintaxe completa, tipos de dados, funções agregadas e transações ACID.

Casos de uso principais incluem:
- Aplicações offline-first que precisam de consultas SQL complexas
- Prototipação rápida sem necessidade de configurar servidores de banco de dados
- Testes unitários e de integração que rodam em memória, eliminando dependências externas
- Ferramentas educacionais e ambientes de aprendizado interativos

2. Instalação e primeiros passos

Setup no Node.js

npm install @electric-sql/pglite
import { PGlite } from '@electric-sql/pglite'

const db = new PGlite() // instância em memória

await db.exec(`
  CREATE TABLE usuarios (
    id SERIAL PRIMARY KEY,
    nome TEXT NOT NULL,
    email TEXT UNIQUE,
    criado_em TIMESTAMP DEFAULT NOW()
  );
`)

await db.exec(`
  INSERT INTO usuarios (nome, email) VALUES ('Ana Silva', 'ana@exemplo.com');
`)

const resultado = await db.query('SELECT * FROM usuarios;')
console.log(resultado.rows)
// [{ id: 1, nome: 'Ana Silva', email: 'ana@exemplo.com', criado_em: ... }]

Setup no browser (via CDN)

<script type="module">
import { PGlite } from 'https://cdn.jsdelivr.net/npm/@electric-sql/pglite/dist/index.js';

const db = new PGlite({ 
  dataDir: 'idb://meu-banco' // persistência opcional via IndexedDB
});

await db.exec(`
  CREATE TABLE tarefas (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    descricao TEXT,
    concluida BOOLEAN DEFAULT FALSE
  );
`);

await db.exec(`INSERT INTO tarefas (descricao) VALUES ('Estudar PGlite');`);
</script>

3. Diferenças fundamentais entre PGlite e PostgreSQL tradicional

O PGlite mantém compatibilidade com a maior parte da sintaxe SQL do PostgreSQL, mas possui limitações importantes:

  • Extensões C nativas: extensões como PostGIS, pgvector e TimescaleDB não funcionam, pois exigem binários compilados para arquitetura nativa. Apenas extensões puramente SQL ou WASM são suportadas.
  • Performance e concorrência: o PGlite opera em single-thread, diferentemente do PostgreSQL tradicional que usa múltiplos processos. Isso impacta workloads pesados com muitas conexões simultâneas.
  • Persistência: por padrão, os dados ficam em memória. No browser, é possível persistir via IndexedDB usando o prefixo idb:// no dataDir. Em Node.js, o salvamento pode ser feito exportando o banco como dump SQL ou arquivo binário.

4. PGlite no browser: integração com aplicações web

Exemplo com React (hook personalizado)

import { useState, useEffect } from 'react';
import { PGlite } from '@electric-sql/pglite';

function usePGlite() {
  const [db, setDb] = useState(null);
  const [tarefas, setTarefas] = useState([]);

  useEffect(() => {
    const init = async () => {
      const instance = new PGlite({ dataDir: 'idb://app-tarefas' });
      await instance.exec(`
        CREATE TABLE IF NOT EXISTS tarefas (
          id SERIAL PRIMARY KEY,
          texto TEXT NOT NULL,
          concluida BOOLEAN DEFAULT FALSE
        );
      `);
      setDb(instance);
      carregarTarefas(instance);
    };
    init();
  }, []);

  const carregarTarefas = async (instance) => {
    const result = await instance.query('SELECT * FROM tarefas ORDER BY id;');
    setTarefas(result.rows);
  };

  const adicionarTarefa = async (texto) => {
    await db.exec(`INSERT INTO tarefas (texto) VALUES ($1);`, [texto]);
    carregarTarefas(db);
  };

  return { tarefas, adicionarTarefa };
}

Estratégia de backup e restore

// Exportar banco como SQL dump
const dump = await db.dumpDataAsSQL();
const blob = new Blob([dump], { type: 'application/sql' });
const url = URL.createObjectURL(blob);

// Importar dump
await db.exec(dump);

5. PGlite em Node.js: ambientes serverless e testes

Exemplo com Vitest

import { describe, it, expect, beforeAll } from 'vitest';
import { PGlite } from '@electric-sql/pglite';

let db;

beforeAll(async () => {
  db = new PGlite();
  await db.exec(`
    CREATE TABLE produtos (
      id SERIAL PRIMARY KEY,
      nome VARCHAR(100),
      preco DECIMAL(10,2)
    );
  `);
  await db.exec(`
    INSERT INTO produtos (nome, preco) VALUES 
      ('Teclado', 150.00),
      ('Mouse', 80.00),
      ('Monitor', 1200.00);
  `);
});

it('deve retornar produtos com preço acima de 100', async () => {
  const result = await db.query('SELECT * FROM produtos WHERE preco > 100;');
  expect(result.rows.length).toBe(2);
  expect(result.rows[0].nome).toBe('Teclado');
});

Uso em Cloudflare Workers

// worker.js
import { PGlite } from '@electric-sql/pglite';

export default {
  async fetch(request) {
    const db = new PGlite();
    await db.exec('CREATE TABLE IF NOT EXISTS visitas (id SERIAL, data TIMESTAMP DEFAULT NOW());');
    await db.exec('INSERT INTO visitas DEFAULT VALUES;');
    const count = await db.query('SELECT COUNT(*) as total FROM visitas;');
    return new Response(JSON.stringify(count.rows[0]), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

6. Comparação com alternativas: SQL.js, DuckDB-WASM e SQLite em Node

Característica PGlite SQL.js (SQLite WASM) DuckDB-WASM
Sintaxe SQL PostgreSQL SQLite DuckDB (SQL extendido)
Tamanho do binário ~12MB ~1.5MB ~8MB
Tipos de dados PostgreSQL completos SQLite (limitados) Tipos analíticos
Transações ACID Sim Sim Sim
Suporte a JSON Nativo (JSONB) Limitado Nativo
Ideal para OLTP, apps web Embarcado leve Analytics, OLAP

O PGlite se destaca quando você precisa de fidelidade ao ecossistema PostgreSQL — funções como array_agg, string_agg, ROW_NUMBER() e tipos JSONB funcionam exatamente como no PostgreSQL tradicional.

7. Limitações, performance e boas práticas

Performance comparativa

// Benchmark simples: inserir 1000 registros
console.time('pglite-insert');
for (let i = 0; i < 1000; i++) {
  await db.exec(`INSERT INTO teste (valor) VALUES ($1);`, [i]);
}
console.timeEnd('pglite-insert');
// ~150ms no Node.js vs ~30ms no PostgreSQL nativo

Limitações conhecidas

  • Sem suporte a triggers avançados ou funções escritas em PL/pgSQL que dependam de bibliotecas C
  • Sem replicação nativa ou autenticação de usuários
  • Operações DDL pesadas (ALTER TABLE em tabelas grandes) podem causar pausas perceptíveis no browser

Boas práticas

// Sempre use transações para operações atômicas
await db.exec('BEGIN;');
try {
  await db.exec('UPDATE contas SET saldo = saldo - 100 WHERE id = 1;');
  await db.exec('UPDATE contas SET saldo = saldo + 100 WHERE id = 2;');
  await db.exec('COMMIT;');
} catch (e) {
  await db.exec('ROLLBACK;');
}

// Gerencie o ciclo de vida: feche a conexão quando não for mais necessária
await db.close();

8. O futuro do PGlite e casos de uso emergentes

O roadmap do PGlite inclui:
- Suporte a extensões WASM customizadas (permitindo portar extensões PostgreSQL para o browser)
- Melhorias de performance via instruções SIMD no WebAssembly
- Persistência otimizada com snapshotting incremental

Casos emergentes já em desenvolvimento:
- PWAs com dados relacionais offline: aplicações de inventário, CRM mobile e editores colaborativos
- Ferramentas de BI no navegador: dashboards que executam consultas SQL diretamente nos dados do usuário
- Ambientes educacionais: plataformas de aprendizado de SQL que rodam PostgreSQL real no próprio navegador

Para contribuir, acesse o repositório oficial no GitHub e participe da comunidade no Discord do ElectricSQL.


Referências