Skip to content
dbSDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

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 CAPABILITY before 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 with enforceSessionRestrictions: false if 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 wraps BEGIN/COMMIT around the statements. Neither form accepts control flow between statements; if you need if, use transaction().

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.

Was this page helpful?