Drivers
Import connect from the driver you installed.
Import connect from the driver you installed. The calls on the client do not change.
| Import | Driver | Returns |
|---|---|---|
okmodel/pg/postgresjs | postgres.js | The client. A topology is a promise. |
okmodel/pg/pg | node-postgres | The client. A topology is a promise. |
okmodel/pg/bun | Bun.sql | The client. A topology is a promise. Another runtime cannot load this entry. |
okmodel/pg/pglite | PGlite | A promise. Opening the database is async. |
okmodel/pg/neon | Neon | A promise. Opening the pool is async. |
A string or a pool returns the client for postgres.js, node-postgres, and Bun.sql. { primary, replicas } returns a promise. That object is Topology. PGlite returns a promise for a string, a pool, omitted memory, and a topology. Neon returns a promise for a string, a pool, and a topology.
When the schema names no floor, a server below 15 is OKM1803. minPostgres is that floor. The test suite runs on 15, 16, 17, and 18.
db.capabilities is that adapter's frozen flags: transactions, stream, listen, and cancel. A table named capabilities is OKM1023, because the client writes this key after the tables on the root, on a scoped client, and inside tx(). prepared and describe were removed from DriverCapabilities. That removal is breaking. Prepared-statement mode stays a connect option (prepared: "named" | "unnamed" | "none"). A driver that can describe a statement still exposes pool.describe.
postgres.js
pool.describe is present. capabilities.cancel is true. The peer package is postgres. A pool opened with ssl still uses the driver's socket and its 30 s idle timer. A plain pool (no ssl) exits on its own after the last query. close() or await using releases a TLS pool without waiting.
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
const app = schema({ tables: [notes] });
export const db = connect(process.env.DATABASE_URL!, { schema: app });node-postgres
There is no pool.describe. It cannot describe a statement without running it. capabilities.cancel is true. The peer package is pg. The pool sets allowExitOnIdle and idleTimeoutMillis from idleTimeoutMs (default 30 seconds; 0 keeps idle connections).
import { connect } from "okmodel/pg/pg";
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
const app = schema({ tables: [notes] });
export const db = connect(process.env.DATABASE_URL!, { schema: app });Bun.sql
Another runtime cannot load the bun import. There is no package to install. Minimum Bun is 1.4. There is no pool.describe. RAISE NOTICE is not surfaced. capabilities.cancel is false. On Bun 1.4, Query.cancel() does not abort an in-flight backend statement.
import { connect } from "okmodel/pg/bun";
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
const app = schema({ tables: [notes] });
export const db = connect(process.env.DATABASE_URL!, { schema: app });PGlite
PGlite runs in this process. The pool size is one. A statement issued on the outer client while a tx() is open waits until that transaction ends, unless timeouts.acquire is set. Use the client the callback received. pool.describe is present. capabilities.cancel is false. The peer package is @electric-sql/pglite. A string is a data directory. A pool and omitted memory are promises too. Omitted opens memory://.
import { connect } from "okmodel/pg/pglite";
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
const app = schema({ tables: [notes] });
export const db = await connect("./data", { schema: app }); Neon
The peer package is @neondatabase/serverless. connect and open return a promise. mode is websocket or http. Omitted is websocket.
WebSocket mode is a pool. transactions is interactive, so tx() works on a direct connection. prepared defaults to unnamed. stream, listen, and cancel are false, and there is no pool.describe. A positive timeout is refused.
HTTP mode sends one request per statement. transactions is batch. batch() is one atomic HTTP transaction. tx() is OKM1111 (Transactions). There is no reserve. prepared defaults to none.
A transaction-mode pooler does not promise session state. Advisory locks, SET, LISTEN, and prepared statements belong to one server session. With one small compute, that pooler reused a backend: a custom SET, LISTEN, and PREPARE were still visible on the next statement, and a second connection saw the SET. That reuse is not a guarantee. Do not rely on it. prepared: "named" is not for that pooler.
A compute can be suspended. The first connection is a cold start. OKModel does not treat that wait as a query result.
A read replica can be behind the primary. After a write, a later read on that replica can miss the row until the replica replays it. route: "replica" still goes to a replica. A miss is a miss. OKModel does not report the row as present, and it does not move that call to the primary. Session consistency can keep an automatic read on the primary until the replica has caught up (fallback:behind or fallback:position-unknown).
okm driver test neon marks COMMIT loss unsupported (no socket to cut). The harness cut is a TCP relay, and this driver speaks WebSocket or HTTP. A result that never arrives is still outcome_unknown (OKM1401). Neon has its own connect gate and is not a column in the compatibility table. postgres.js and node-postgres pointed at a Neon endpoint stay unclassified.
import { connect } from "okmodel/pg/neon";
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
const app = schema({ tables: [notes] });
export const db = await connect(process.env.DATABASE_URL!, { schema: app }); timeout
Bun.sql, PGlite, and Neon refuse a positive timeout on a call, on tx(), and on batch(). The keys and the messages are on Timeouts.
A lost connection
The first call after a backend kill on an idle connection is unavailable and retryable. Bun.sql retries a client read once at transaction depth 0. postgres.js and node-postgres do not. A reserved Bun.sql handle whose backend was killed fails the next statement with unavailable instead of running it on a new connection. On postgres.js, a wait with no call deadline is rejected when every slot in that pool has closed, or when the reserved connection it uses closes. A queued reserve is started again after a close. A wait that already has a deadline ends at that deadline. A kill during connect can still surface an unhandled rejection from the driver's own type-catalog query.
postgres.js kill storms (B-02)
postgres.js 3.4.9 can raise an uncaught TypeError from nextWrite (socket.write on a null socket) and unhandled CONNECTION_CLOSED / 57P01 rejections when backends are terminated while queries run. That happens with postgres.js alone; OKModel cannot stop the driver's own scheduled write. Prefer okmodel/pg/pg or okmodel/pg/bun where failover or kill storms are expected. The limit and a minimal reproduction are in the repository at docs/postgresjs-kill-storm.md.