fenecdb and PGlite

PGlite is PostgreSQL compiled to WebAssembly, run in the browser, Node.js and Bun, with extensions such as pgvector. fenecdb is a document database with vector and full-text search, built small for the page, which also runs in apps and as a server.

The facts about PGlite on this page were checked on 2026-10-02 against the pages each one links to, and its sizes measured from version 0.5.8. fenecdb is at version 0.1.8, and is alpha software.

Which to choose

Choose PGlite when

  • You want PostgreSQL itself in the page: its SQL, joins, types and interactive transactions (API).
  • You want its extensions: pgvector, pg_textsearch, pg_trgm, PostGIS and others (extensions).
  • The page and the server should share schema and queries with a PostgreSQL server, or an ORM such as Drizzle (Drizzle).
  • You want live queries that send only what changed (live.incrementalQuery, live.changes).

Choose fenecdb when

  • Download size matters: its module is about a twentieth of PGlite's files (below).
  • You search by meaning and by words in one query, fused by rank, with nothing to install (Hybrid search).
  • The page writes while offline and syncs back to a server, which PGlite's sync does not do yet (Sync).
  • You keep the database in a file in Safari too: fenecdb's file of the origin private file system works there (Keeping a database).
  • The same database runs in an iOS, Android or Flutter app (Mobile and native apps).

Feature by feature

 fenecdbPGlite + pgvector
Where it runs Browser, Node, Cloudflare Workers, Rust, iOS, Android, Flutter, a server over HTTP (Where it runs) Browser, Node.js and Bun (about)
Browser storage IndexedDB, or a file of the origin private file system in a worker, Safari included (Keeping a database) IndexedDB, recommended; an OPFS file system in a worker that does not work in Safari (file systems)
Data model Typed documents in collections, json fields (Objects and paths) PostgreSQL's tables and types
Query language FenecQL and a typed query builder (The query builder) PostgreSQL's SQL, $1 parameters (API)
Vector index HNSW, built in (The vector clause) pgvector's HNSW and IVFFlat, an extension (extensions, pgvector)
Quantization f16 storage; int8 and bit codes, re-ordered by the full vectors (Quantized indexes) halfvec, bit, binary quantization by expression index (pgvector)
Filtered vector search Planned with the filter, an exact fallback when the filter leaves too few (A filter and a vector together) pgvector's iterative index scans (pgvector)
Full text BM25, built in (The text clause) BM25 through the pg_textsearch extension (extensions)
Highlights and facets highlight(), snippet() and facet (Highlights, Facets) PostgreSQL's ts_headline over a tsquery; a facet is a GROUP BY of its own
Hybrid search fuse, reciprocal rank fusion (Hybrid search) No operator; written in SQL
Transactions One writer; a run of several statements lands whole or not at all (Single writer) Interactive transactions, .transaction() (API); one connection, tabs share it through a leader (multi-tab worker)
Live queries db.live: the rows again after each write to a collection the query reads (State in the page) live.query, live.incrementalQuery, live.changes (live queries)
Sync and offline A replica of a fenecdb server: offline reads, writes queued and sent back (Sync) Electric sync, alpha, server to page only: "We don't yet support local writes being synced out" (sync)
Schema and migrations Tables declared in code, Drizzle's way, checked at every open (Schema in code) PostgreSQL's DDL; Drizzle and other ORMs (Drizzle)
Clients JS/TS, React; Swift, Kotlin, Dart in apps (Your language) TypeScript and JavaScript, with React and Vue bindings (about)
Download, brotli 236 KB (below) 3 740 KB, before pgvector (below)
Licence Apache-2.0 Apache-2.0 and the PostgreSQL Licence (about)
Maturity Version 0.1.8, first released in 2026 Version 0.5.8 (releases), on PostgreSQL
Details

PGlite's IndexedDB file system loads every file of the database into memory at the start and flushes them after each query that changed them; its OPFS file system needs a worker, and Safari's limit of 252 open sync access handles stops it working there, "due to a standard Postgres install consisting of over 300 files" (file systems). fenecdb's persist writes the image once and then only the writes since, and openFile keeps one file (Keeping a database).

PGlite is "single connection only"; tabs elect a leader that runs the database and the others reach it through the leader (multi-tab worker). A synced fenecdb replica elects a leader among tabs as well (Multiple tabs).

What fenecdb leaves out to stay small is in Limits: no SQL and no join, one writer, and no transaction held open while the page reads between its writes.

What the browser downloads

The WebAssembly and data files each needs before its first query, as each project publishes them, compressed the way a static file is served. PGlite's files are about 21 times fenecdb's module.

 brotligzip
fenecdb's module, fenec.wasm 0.1.8236 KB285 KB
PGlite 0.5.8: pglite.wasm2 585 KB3 310 KB
… pglite.data1 038 KB1 816 KB
… initdb.wasm117 KB141 KB
PGlite 0.5.8, the three3 740 KB5 267 KB
  • PGlite's own figure is "under 3MB gzipped" (about); the files of 0.5.8 measure more.
  • pgvector adds a 45 KB archive, gzipped as published.
Method and details

Measured on 2026-10-02. fenecdb's module as make wasm builds it; PGlite's files from the npm package @electric-sql/pglite 0.5.8, whose client starts downloading all three when a database opens. Each file compressed with brotli -q 11 and gzip -9; a KB is 1 024 bytes. The JavaScript beside each is left out of both. pgvector is dist/vector.tar.gz of @electric-sql/pglite-pgvector 0.0.9, 46 549 bytes; PGlite's extensions page gives 42.9 KB (extensions).

Not measured yet: the time to start, load and search in either, and the memory each holds in the page.

Moving from PGlite

A table becomes a collection, a query becomes FenecQL or a builder chain, and parameters stay $1, $2. Opening a database kept in IndexedDB, with a vector and a text index:

// PGlite
import { PGlite } from '@electric-sql/pglite';
import { vector } from '@electric-sql/pglite-pgvector';

const pg = new PGlite('idb://notes', { extensions: { vector } });
await pg.exec(`
  CREATE EXTENSION IF NOT EXISTS vector;
  CREATE TABLE IF NOT EXISTS notes (id serial PRIMARY KEY, title text, body text, embed vector(384));
  CREATE INDEX IF NOT EXISTS notes_embed ON notes USING hnsw (embed vector_cosine_ops);
`);
// fenecdb
import { Fenec, restore } from './fenec.js';
import { fenecTable, text, vector, index } from './schema.js';

const notes = fenecTable('notes', {
  title: text(),
  body: text(),
  embed: vector({ dimensions: 384 }),
}, (t) => [
  index('notes_body').using('bm25', t.body),
  index('notes_embed').using('hnsw', t.embed.op('vector_cosine_ops')),
]);

const db = await Fenec.open('./fenec.wasm', { schema: { notes } });
await restore(db, 'notes');   // what persist(db, 'notes') kept, if anything

A write and a nearest-neighbour search:

// PGlite
await pg.query('INSERT INTO notes (title, body, embed) VALUES ($1, $2, $3)',
  [title, body, JSON.stringify(embedding)]);
const { rows } = await pg.query(
  'SELECT title FROM notes ORDER BY embed <=> $1 LIMIT 5', [JSON.stringify(queryVector)]);
// fenecdb, the builder
await db.from(notes).insert({ title, body, embed: embedding });
const rows = await db.from(notes).select('title').near('embed', queryVector).limit(5).rows();

// or FenecQL
const same = db.rows('get notes select title near embed $1 limit 5', [queryVector]);

Words and meaning in one ranking, which in PGlite is a query to write over pg_textsearch and pgvector:

const hits = await db.from(notes).select('title')
  .match('body', 'offline sync')
  .near('embed', queryVector)
  .fuse()
  .limit(10)
  .rows();

A live query:

// PGlite, with the live extension
const ret = pg.live.query('SELECT title FROM notes ORDER BY id DESC LIMIT 20', [], (res) => draw(res.rows));
await ret.unsubscribe();
// fenecdb
const stop = db.live(db.from(notes).select('title').order('id', 'desc').limit(20), draw);
stop();
Details

There is no importer from PGlite. Its data can be read out with a query and written with insert, an array at a time, or, from a PostgreSQL server holding the same tables, fenec import reads them over the network.

What does not carry over: a join becomes lookup or in (get …) (The lookup clause), a .transaction() that reads between its writes becomes a run of several statements, which lands whole but cannot branch on what it read, and an extension has no counterpart unless fenecdb has the feature built in. A numeric has no counterpart either: money is an int of cents.