Capabilities
What each dbSDK adapter and transport supports, with the evidence behind each cell.
Every adapter declares its capabilities, and the client enforces them before
any network traffic. There is no silent fallback: if a transport cannot do
something, the call fails with a DbError of code CAPABILITY and nothing
is sent. This page states the current matrix and the evidence behind it.
The matrix
| Capability | postgres (TCP) | supabase direct | supabase session | supabase transaction | neon http | neon + websocket tx | neon + postgres tx |
|---|---|---|---|---|---|---|---|
| Transport | tcp | tcp | tcp | tcp | http | http (queries) | http (queries) |
Parameterized query() |
yes | yes | yes | yes | yes | yes | yes |
Interactive transaction() |
yes | yes | yes | yes, one leased connection | refused before dispatch | yes, short-lived WebSocket pool per transaction | yes, pg pool against the direct endpoint |
Atomic batch() |
yes | yes | yes | yes | yes, native single HTTP round trip | yes | yes |
| Session state | on a leased session | on a leased session | on a leased session | guarded before dispatch | no | no on pooled hosts, yes on direct | no on pooled hosts, yes on direct |
| Reusable named prepared statements | no | no | no | no | no | no | no |
Status words used in this table and in code:
- supported: the operation runs.
- refused before dispatch: the client throws
CAPABILITYbefore any network traffic. Example:transaction()on Neon HTTP. - guarded before dispatch: session-state statements (
SET,RESET,LISTEN/NOTIFY,PREPARE/DEALLOCATE,CREATE TEMP,DECLARE ... WITH HOLD) are rejected before dispatch on the Supabase transaction pooler, because that pooler cannot carry them. Disable withenforceSessionRestrictions: falseif you know your deployment allows it. - not supported: the capability does not exist for that transport.
What “session state” means here
capabilities.sessionState: true means session-level state (SET, temporary
objects, LISTEN/NOTIFY, session advisory locks) works on a single
dedicated session: inside transaction(), which leases exactly one
connection, or on a client you lease yourself through raw.
It does not mean state persists between separate top-level queries. Pool
backed adapters may use a different pooled connection for each db.sql
call, so this does not stick:
await db.sql`set statement_timeout = '5s'`; // applies to one pooled session
await db.sql`show statement_timeout`; // may run on another session
Inside transaction(), it does stick, because the callback runs on one
leased connection. If you need session-scoped state across calls, hold a
client from raw or run the work inside a transaction.
Batches and transactions are different tools
db.transaction(fn)runs your callback with multi-round-trip logic on one connection. It needs an interactive-transaction capability.db.batch(statements)is atomic by default: on Neon HTTP it is a single round trip through the driver’s native batch; on TCP transports it leases one connection and wrapsBEGIN/COMMITaround the statements. Neither form accepts control flow between statements; if you needif, usetransaction().
Parameterized binding is available on every row of the table. It does not depend on prepared statements: dbSDK uses unnamed protocol statements and never creates reusable named prepared statements, so binding keeps working even on the Supabase transaction pooler, where named prepared statements cannot.
Evidence levels
Every capability carries an evidence level, also exposed at runtime in
db.capabilities.evidence:
docs: stated by official provider documentation, not executed by this project.tests: verified by this project’s automated tests (fixture or contract tests, and live checks against local servers).live: verified against the real hosted service.
The current runtime levels are conservative and deliberately so: the
postgres adapter declares tests (its behavior is verified by this
project’s automated tests against a real local Postgres 17 container), while
the Supabase and Neon adapters declare docs for every capability - their
transport facts come from provider documentation. The local verification
runs are real and reproducible: a local Postgres 17 container, the Neon HTTP
protocol through a local proxy, and the Supabase adapter against a local
server exercised the transaction-pooler path (commands below and in
CONTRIBUTING).
Hosted Supabase and Neon projects have not been queried by this project, and
no table on this site claims otherwise.
The runtime surface
console.log(db.capabilities);
// {
// interactiveTransactions: false,
// atomicBatch: true,
// sessionState: false,
// transport: "http",
// evidence: { interactiveTransactions: "docs", atomicBatch: "docs", ... }
// }
Check db.capabilities when writing transport-sensitive code instead of
branching on the adapter id. The adapter id can gain new transports; the
capability flags are the contract.
Reproducing the local verification
The test suite runs without any database (fixtures only), and the live local checks run against real servers you start yourself:
docker run -d --name dbsdk-pg-test -e POSTGRES_PASSWORD=dbsdk -e POSTGRES_DB=dbsdk -p 15432:5432 postgres:17-alpine
docker run -d --name dbsdk-neon-proxy \
-e PG_CONNECTION_STRING='postgres://postgres:dbsdk@host.docker.internal:15432/dbsdk' \
-p 14444:4444 ghcr.io/timowilhelm/local-neon-http-proxy:main
DBSDK_TEST_POSTGRES_URL='postgres://postgres:dbsdk@localhost:15432/dbsdk' \
DBSDK_TEST_NEON_CONNECTION_STRING='postgres://postgres:dbsdk@db.localtest.me:5432/dbsdk' \
DBSDK_TEST_NEON_HTTP_ENDPOINT='http://localhost:14444/sql' \
pnpm --filter dbsdk test
The verified state at the time of writing: 182 passed, 0 failed, 0 skipped with the live environment running.