@molecule/api-database-pglite
Provider bond · database · API (Node) · v1.2.2 · Apache-2.0
PGlite database provider for molecule.dev — real Postgres compiled to WebAssembly, in-process: in memory, on disk, or in the browser (IndexedDB). The dev and browser profile; production swaps the postgresql bond in with the same app code. One connection at a time.
npm install @molecule/api-database-pglitenpm · Source on GitHub · Implements @molecule/api-database
How it works
@molecule/api-database-pglite is a provider bond on the API (Node) side: it implements the database core interface (@molecule/api-database) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { setPool, setStore } from '@molecule/api-database'
import { createMigrator, pool, store } from '@molecule/api-database-pglite'
// Data directory comes from PGLITE_DATA_DIR (default ./data/pglite under Node).
setPool(pool)
setStore(store)
// Same migrations folder the postgresql bond reads; runs on the same pool.
await createMigrator(new URL('../migrations', import.meta.url).pathname)()Works with: @molecule/api-bond, @molecule/api-database, @molecule/api-secrets
Secrets: PGLITE_DATA_DIR (optional)
Live demos
Running apps built with Molecule that install this package — open the app or study the template.
Reference
PGlite database provider for molecule.dev — real Postgres (compiled to WebAssembly) running inside your own process, with no database server.
This is the development / in-browser profile of the database: the same
Postgres SQL, the same $1 placeholders, the same error codes and the same
DataStore SQL as @molecule/api-database-postgresql, so app code written
against @molecule/api-database runs unchanged on both. Develop on PGlite;
in production bond the postgresql provider instead — swap the import, keep
the app code and the migrations.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { setPool, setStore } from '@molecule/api-database'
import { createMigrator, pool, store } from '@molecule/api-database-pglite'
// Data directory comes from PGLITE_DATA_DIR (default ./data/pglite under Node).
setPool(pool)
setStore(store)
// Same migrations folder the postgresql bond reads; runs on the same pool.
await createMigrator(new URL('../migrations', import.meta.url).pathname)()
// Explicit pools: in memory (tests), on disk, or in IndexedDB.
import { applyMigrations, createPool, createStore } from '@molecule/api-database-pglite'
const testPool = createPool({ dataDir: 'memory://' })
const diskPool = createPool({ dataDir: './data/pglite' })
const browserPool = createPool({ dataDir: 'idb://my-app' })
// Without a filesystem, pass the SQL in (e.g. bundled with Vite's ?raw).
await applyMigrations(browserPool, [{ name: '0001_init.sql', sql: initSql }])
const store = createStore(browserPool)
// Production: the same app code against the real server.
// import { pool, store } from '@molecule/api-database-postgresql'
Type
provider
Installation
npm install @molecule/api-database-pglite @electric-sql/pglite @molecule/api-bond @molecule/api-database @molecule/api-secrets
API
Interfaces
PgliteInstance
One opened database: the PGlite instance, the lock over its single connection, and how many pools currently use it.
interface PgliteInstance {
/** The PGlite instance. */
db: PGlite
/** FIFO lock over the instance's one connection. */
lock: ConnectionLock
/** The resolved data directory (absolute path for Node filesystem paths). */
dataDir: string
/** Pools currently using this instance. */
refs: number
/** Registry key for shared (persistent) instances; `null` for in-memory ones. */
key: string | null
}
PgliteMigration
One migration: a name (for ordering and logs) and its SQL.
interface PgliteMigration {
/** File name, e.g. `0001_init.sql`. Migrations run sorted by name. */
name: string
/** One or more SQL statements. */
sql: string
}
PgliteConfig
Configuration for a PGlite-backed pool.
interface PgliteConfig {
/**
* Where the database lives:
*
* - `'memory://'` — in memory; gone when the pool ends or the process exits.
* - a filesystem path (Node only), e.g. `'./data/pglite'` — a Postgres data
* directory; parent directories are created for you.
* - `'idb://<name>'` — the browser's IndexedDB (needs an `indexedDB` global).
*
* Defaults to the `PGLITE_DATA_DIR` env var, then `'./data/pglite'` when a
* Node runtime is present, else `'memory://'`.
*/
dataDir?: string
/**
* How long (ms) a query, `connect()` or `transaction()` waits for the single
* connection before failing with an error that says who is holding it.
* Defaults to 10 000 (the postgresql bond's `connectionTimeoutMillis`).
*/
acquireTimeoutMillis?: number
/**
* Return `int8`/`bigint` values (`COUNT(*)`, `SUM(int)`, `bigint` columns) as
* strings, exactly like node-postgres. PGlite's own default is a JS number,
* or a `BigInt` above 2^53 — which `JSON.stringify` cannot serialize.
* Defaults to `true` so app code behaves the same here and on the
* postgresql bond.
*/
int8AsString?: boolean
/**
* Extra options handed to the `PGlite` constructor as-is (extensions,
* `relaxedDurability`, custom `parsers`, `debug`, …). `dataDir` is set by
* the field above; a `parsers` entry here wins over `int8AsString`.
*/
pgliteOptions?: Omit<PGliteOptions, 'dataDir'>
}
PglitePool
A DatabasePool backed by PGlite, plus the two things only this bond has.
interface PglitePool extends DatabasePool {
/**
* Begins a transaction on the single connection (always implemented here).
*
* @returns The open transaction; end it with `commit()` or `rollback()`.
*/
transaction(): Promise<DatabaseTransaction>
/**
* Runs SQL that may hold several statements (a migration file) through
* Postgres's simple-query protocol. Takes no parameters.
*
* @param sql - One or more SQL statements separated by `;`.
*/
exec(sql: string): Promise<void>
/**
* The data directory this pool resolved to (`'memory://'` for in-memory).
* Known only after first use; `undefined` before that.
*/
readonly dataDir: string | undefined
}
ProcessEnv
Environment variables consumed by the PGlite database provider.
interface ProcessEnv {
PGLITE_DATA_DIR?: string
}
Classes
ConnectionLock
Serializes every use of ONE PGlite instance.
PGlite is a single Postgres backend: BEGIN/COMMIT/ROLLBACK apply to the
same session every other query uses. Without this lock an unrelated
pool.query() issued while a transaction is open runs INSIDE it and is
thrown away by that transaction's rollback (verified against PGlite 0.5.8).
Plain queries hold the lock for one statement; connect() and
transaction() hold it until they are released.
Waiting is bounded: a waiter that cannot get the lock within its timeout is removed from the queue and rejected with an error naming the likely cause (code holding a connection while calling the pool), instead of hanging forever.
class ConnectionLock {
private locked = false
private holder: string | null = null
private waiters: Array<{ grant: () => void }> = []
/**
* Number of callers currently waiting for the lock.
*
* @returns The queue length.
*/
get waiting(): number {
return this.waiters.length
}
/**
* Whether the lock is currently held.
*
* @returns `true` when held.
*/
get isLocked(): boolean {
return this.locked
}
/**
* Waits for the lock in FIFO order. The returned release function MUST be
* called exactly once; extra calls are ignored.
*
* @param purpose - What the caller will do with the lock (`'query'`,
* `'connect()'`, `'transaction()'`), used in the timeout message.
* @param timeoutMillis - Maximum wait; `0` or less waits forever.
* @returns A release function.
*/
acquire(purpose: string, timeoutMillis: number): Promise<() => void> {
let released = false
const release = (): void => {
if (released) return
released = true
this.holder = null
const next = this.waiters.shift()
if (next) {
next.grant()
} else {
this.locked = false
}
}
if (!this.locked) {
this.locked = true
this.holder = purpose
return Promise.resolve(release)
}
return new Promise<() => void>((resolve, reject) => {
let timer: ReturnType<typeof setTimeout> | undefined
const waiter = {
grant: (): void => {
if (timer !== undefined) clearTimeout(timer)
this.locked = true
this.holder = purpose
resolve(release)
},
}
this.waiters.push(waiter)
if (timeoutMillis > 0) {
timer = setTimeout(() => {
const index = this.waiters.indexOf(waiter)
if (index === -1) return
this.waiters.splice(index, 1)
reject(
new Error(
`PGlite: a ${purpose} waited ${timeoutMillis} ms for the database's single ` +
`connection, which is held by an open ${this.holder ?? 'operation'}. PGlite has ` +
`exactly one connection: release() a connect()ed connection and commit()/rollback() ` +
`a transaction before calling the pool or the store again, and run every statement ` +
`of a transaction on its own conn.query().`,
),
)
}, timeoutMillis)
}
})
}
}
Functions
getPoolInstance()
Returns the default pool, created on first call from PGLITE_DATA_DIR
(read then, not at import, so env loading can run first).
function getPoolInstance(): PglitePool
Returns: The shared default pool.
chooseDataDir(config)
Picks the data directory from config, then PGLITE_DATA_DIR, then the
runtime default.
function chooseDataDir(config?: PgliteConfig): string
config— Pool configuration.
Returns: The data directory as given (not yet resolved to an absolute path).
acquireInstance(config)
Opens (or joins) the instance for a pool. In-memory pools always get their own instance; pools on the same persistent directory share one.
function acquireInstance(config?: PgliteConfig): Promise<PgliteInstance>
config— Pool configuration.
Returns: The instance, with a reference taken for the caller.
releaseInstance(instance)
Drops a pool's reference; closes the instance when no pool uses it.
function releaseInstance(instance: PgliteInstance): Promise<void>
instance— The instance to release.
isIdempotencyError(err)
Whether an error is the "already exists" kind an IF NOT EXISTS migration
is expected to hit on a re-run. Matches the SQLSTATE code first, then the
message text.
function isIdempotencyError(err: unknown): boolean
err— The error thrown while applying a migration.
Returns: true if it is safe to warn and continue.
applyMigrations(pool, migrations)
Applies migrations to a pool, in name order. Works in any runtime (the SQL is passed in, nothing is read from disk) — use this in a browser, with the SQL bundled by your build tool.
function applyMigrations(pool: PglitePool, migrations: PgliteMigration[]): Promise<void>
pool— The PGlite pool to migrate.migrations— The migrations to apply.
Returns: Resolves when every migration has been applied.
createMigrator(migrationsDir, pool)
Returns a runMigrations() bound to a migrations directory (Node only).
function createMigrator(migrationsDir: string, pool?: PglitePool): () => Promise<void>
migrationsDir— Absolute path to the directory of ordered*.sqlfiles, e.g.join(new URL('.', import.meta.url).pathname, '../../migrations')from the app'sscripts/migrate.ts.pool— The pool to migrate. Defaults to the package's default pool (the onepool/storeuse), so migrations land in the database the app reads.
Returns: A no-arg runMigrations() that applies every *.sql file in lexical order. A missing directory counts as "no migrations".
createPool(config)
Creates a DatabasePool backed by PGlite.
The database opens on first use, not here, so creating a pool is free and
synchronous. See PgliteConfig for where data is kept.
function createPool(config?: PgliteConfig): PglitePool
config— Data directory, wait timeout and PGlite options.
Returns: A pool over one PGlite connection.
createStore(pool)
Creates a DataStore backed by a PGlite (or any Postgres) DatabasePool.
function createStore(pool: DatabasePool): DataStore
pool— TheDatabasePoolto use for queries.
Returns: A DataStore that translates CRUD operations to SQL queries.
Constants
pool
The default PGlite pool. Bond it with setPool(pool).
const pool: PglitePool
store
The default DataStore, backed by the default pool. Bond it with setStore(store).
const store: DataStore
MEMORY_DATA_DIR
The in-memory data directory.
const MEMORY_DATA_DIR: 'memory://'
DEFAULT_NODE_DATA_DIR
Default on-disk data directory under a Node runtime.
const DEFAULT_NODE_DATA_DIR: './data/pglite'
MIGRATION_LEDGER
The table that records which migration files have run.
const MIGRATION_LEDGER: '_molecule_migrations'
databasePgliteSecretDefinitions
Secret definitions read by the PGlite database bond.
const databasePgliteSecretDefinitions: SecretDefinition[]
Core Interface
Implements @molecule/api-database interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setPool, setStore } from '@molecule/api-database'
import { pool, store } from '@molecule/api-database-pglite'
export function setupDatabasePglite(): void {
setPool(pool)
setStore(store)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-database^1.1.0@molecule/api-secrets^1.0.1
Environment Variables
PGLITE_DATA_DIR(optional) — PGlite data directory — default:./data/pglite- Setup: A filesystem path (Node), idb://<name> (browser IndexedDB), or memory:// (in memory, lost on exit).
- Example:
./data/pglite
Runtime Dependencies
-
@electric-sql/pglite -
@molecule/api-bond -
@molecule/api-database -
@molecule/api-secrets -
Exactly ONE connection. PGlite is a single Postgres backend, so the pool serializes everything: plain
query()calls queue in order;connect()andtransaction()hold the connection untilrelease()/commit()/rollback(). Never callpool.query()or a store helper (create,findMany, …) while YOUR code holds a connection or an open transaction — on Postgres that runs on a second connection, here it waits for the one you hold. AfteracquireTimeoutMillis(default 10 s) it fails with an error saying a connection is held, instead of hanging. Inside a transaction run every statement on that transaction's ownquery(), and always release infinally(the same rule the postgresql bond needs). -
All-or-nothing writes:
withTransaction(async () => { … })from@molecule/api-database. Every call inside it — the CRUD helpers (create,findMany,updateById, …) andquery()— joins the one transaction; it commits when the callback resolves, rolls back when it throws. -
Where data lives (
dataDir, elsePGLITE_DATA_DIR): a filesystem path (Node; default./data/pglite, parent folders created),idb://<name>(browser IndexedDB), ormemory://(gone when the process ends; the default when no Node runtime is present). Pools on the same directory in one process share one instance; never point two PROCESSES at one data directory — PGlite does not lock it and they will corrupt each other. Different directories (and everymemory://pool) are fully separate databases. -
Returned types match node-postgres:
numericcomes back as a string ('12.50'),int8/bigintincludingCOUNT(*)as a string (setint8AsString: falsefor PGlite's number/BigInt),json/jsonbparsed,booleanas boolean,timestamptzasDate. Known differences:byteais aUint8Array(pg:Buffer),intervalis a string (pg: an object), anddateis midnight UTC (pg: local midnight). -
Errors are Postgres errors with the same fields as pg's
DatabaseError—code('23505'unique violation,'23503'foreign key,'42P01'missing table),constraint,table,detail— so error handling written for the postgresql bond works unchanged. -
query(text)with NO parameters may hold several statements (like node-postgres) and returns the LAST statement's result; with parameters it must be one statement. -
A transaction's bare
release()(without commit/rollback) ROLLS BACK before freeing the connection, so the next caller never inherits an open transaction. -
Browser use: every
@molecule/api-*package — this one and@molecule/api-databaseincluded — refuses to load in a plain browser page (no Node runtime). Run the API in a Node-compatible in-browser runtime; PGlite itself needs only WebAssembly. Load-time cost: PGlite starts in about 1–2 s, once per instance. -
Same SQL dialect as production, so migrations are plain Postgres
.sqlfiles inmigrations/(no translation layer, unlike the sqlite bond), andlike/ilikebehave exactly as on the postgresql bond. Each file runs ONCE (the_molecule_migrationstable records it), as on the postgresql bond.