@askalf/pgflex
v0.0.2
Published
One Postgres API. Two modes. Real PostgreSQL (pg) for production, PGlite (in-process WASM) for standalone / dev. Same SQL, same query shape, drop the server when you don't need it.
Maintainers
Readme
@askalf/pgflex
One Postgres API. Two modes. Same SQL.
Switch between a real PostgreSQL server (pg) and in-process PostgreSQL (PGlite, WASM) with one line of config. Production runs on real Postgres; dev / standalone / "no-Docker mode" runs in-process. Same SQL, same query shape, same parameter style — drop the server when you don't need it.
npm install @askalf/pgflexWhy
Most apps need Postgres. Most dev environments don't want the overhead of running a Postgres server. Most CI environments REALLY don't want it. PGlite (electric-sql/pglite) gives you full PostgreSQL — JSONB, ON CONFLICT, RETURNING, triggers, functions, even pgvector — running in WASM, in your Node process, with no server, no port, no Docker.
pgflex is the thin adapter on top: one DatabaseAdapter interface, two backends, mode flips via config or env var. Your SQL, your transactions, and your codepaths stay identical between modes.
Use it
Direct
import { createAdapter } from '@askalf/pgflex';
// Production — real Postgres server
const db = await createAdapter({
mode: 'pg',
connectionString: process.env.DATABASE_URL!,
});
// Dev / standalone — PGlite, no server needed
const db = await createAdapter({
mode: 'pglite',
dataDir: '~/.myapp/data', // or 'memory://' for ephemeral
});
const users = await db.query<{ name: string }>(
'SELECT name FROM users WHERE active = $1',
[true],
);From environment
import { createAdapterFromEnv } from '@askalf/pgflex';
// PGFLEX_MODE=pglite → pglite at $PGFLEX_DATA_DIR (or ~/.pgflex/data)
// otherwise → pg at $DATABASE_URL
const db = await createAdapterFromEnv();You can rename any of the env vars:
const db = await createAdapterFromEnv({
modeEnvVar: 'MYAPP_MODE',
connectionStringEnvVar: 'MYAPP_DB_URL',
dataDirEnvVar: 'MYAPP_DATA_DIR',
pgliteExtensions: ['vector'],
});Transactions
const transferred = await db.transaction(async (tx) => {
await tx.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [30, 1]);
await tx.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [30, 2]);
return 30;
});Auto-commits on return. Auto-rolls-back on throw.
The interface
interface DatabaseAdapter {
query<T>(text: string, params?: unknown[]): Promise<T[]>;
queryOne<T>(text: string, params?: unknown[]): Promise<T | null>;
transaction<T>(fn: (client: TransactionClient) => Promise<T>): Promise<T>;
close(): Promise<void>;
readonly mode: 'pg' | 'pglite';
}That's it. Same shape across both backends. db.mode is exposed if some piece of your app needs to branch on the runtime — but most don't.
Extensions (pglite mode)
const db = await createAdapter({
mode: 'pglite',
dataDir: 'memory://',
extensions: ['vector'],
});
await db.query('CREATE TABLE docs (id INT, embedding vector(1536))');v0.0.1 wires vector (pgvector) end-to-end — both the JS-side WASM hooks and CREATE EXTENSION vector happen during init().
Other PGlite contrib extensions (uuid-ossp, pgcrypto, tsm_system_rows, etc.) need their own JS-side import to register the WASM hooks. Listing them in the extensions array currently only runs CREATE EXTENSION IF NOT EXISTS <name>, which is enough for extensions baked into PGlite's core WASM but not enough for the contrib ones. Open an issue if you need one wired up; they're ~5 lines each.
In pg mode, extensions are the database admin's responsibility — they're either there or they aren't.
Optional dependency
@electric-sql/pglite is in optionalDependencies, so:
- If you only ever use
pgmode, you can install with--no-optionaland skip the WASM bytes. - If you use
pglitemode, it gets installed by default.
If pglite mode is selected and the package isn't installed, init() throws a clear error telling you what to install.
Statement timeout
In pg mode, every connection gets SET statement_timeout = 30000 automatically. Protects the pool from runaway queries. Override at the SQL level (SET statement_timeout = ...) or open an issue if you need it tunable from the adapter config.
Escape hatch
import { PgAdapter } from '@askalf/pgflex';
const adapter = new PgAdapter(process.env.DATABASE_URL!);
const pool = adapter.getPool(); // raw pg.Pool — for LISTEN/NOTIFY etc.Use sparingly. Code that touches the underlying pool won't work in pglite mode.
What it isn't
- Not an ORM. It's a thin adapter. Bring your own query builder, schema-validator, migration tool. It composes with anything that can take a
query(text, params)function. - Not a connection pooler.
pgmode usespg.Pooldirectly;pglitemode is single-process by design. - Not magic. If you write
pg-only SQL (e.g.pg_sleep, server-side functions you've installed yourself, advisory locks), it'll fail in pglite mode the same waypgwould fail without those features.
License
MIT — see LICENSE.
Also by askalf
| Project | What it does | |---------|-------------| | dario | Use your Claude Max/Pro subscription as an API. Local OAuth proxy that works with any Anthropic or OpenAI SDK. | | brio | Capability layer for AI workloads — semantic cache, cost-aware tiering, policy. Sits in front of any Anthropic-compat endpoint. | | hands | Cross-platform computer-use agent. |
