Drizzle
Optional Drizzle ORM interoperability over dbSDK-owned connections - typed schema queries on the same pool, with dbSDK keeping the lifetime, TLS posture, and guarded-pooler rules.
dbsdk/drizzle is an optional interoperability entry: it hands Drizzle ORM’s
stable node-postgres / neon-http drivers a validated, dbSDK-owned driver
handle, so you can use Drizzle’s typed schema, query builder, and relational
queries on the same pool dbSDK manages — without giving up the lifetime
ownership, TLS posture, and guarded-pooler rules that dbSDK enforces.
Its companion entry dbsdk/orm is the SDK-owned schema authoring surface
for the same stable drizzle-orm copy: one import for tables, columns,
relations, operators, and sql (see Schema authoring with dbsdk/orm).
dbSDK does not fork or wrap Drizzle. Schemas and builders are imported from
dbsdk/orm (a pure re-export) or from drizzle-orm directly — both resolve
to the same stable 0.45.3 API; the bridge only supplies the connection and
refuses anything that would bypass it. There is no claim of full Drizzle
parity here — Studio, Kit, seed, and validators are separate tools with their
own licenses.
Install
drizzle-orm is an optional peer dependency (^0.45.3, the stable line —
not the 1.0 RC). The SDK itself stays dependency-free; the Drizzle entry is
lazy, so installing nothing extra keeps dbsdk imports untouched.
npm i drizzle-orm@^0.45.3 # or: pnpm add drizzle-orm@^0.45.3
Calling a factory without the peer installed fails before any dispatch with an actionable install hint.
A note for skipLibCheck: false users: drizzle-orm 0.45.3’s own published
type declarations do not fully typecheck under strict NodeNext even in a bare
drizzle consumer (~72 upstream errors, with or without dbsdk). With all
optional peers you use installed, dbsdk’s packed declarations contribute
zero errors. If you omit @neondatabase/serverless (legitimate when you only
use dbsdk/postgres + dbsdk/drizzle), skipLibCheck: false reports 2
unresolved-optional-peer-type errors in dbsdk’s own .d.ts — the same pattern
as upstream drizzle entries for their optional peers. skipLibCheck: true (the
setting used for every other dbSDK entry) avoids all of this.
The shape
import { createDatabase } from "dbsdk";
import { postgres } from "dbsdk/postgres";
import { drizzlePostgres } from "dbsdk/drizzle";
import { pgTable, serial, text } from "drizzle-orm/pg-core";
const users = pgTable("users", {
id: serial("id").primaryKey(),
email: text("email").notNull(),
});
const db = createDatabase({
adapter: postgres({ connectionString: process.env.DATABASE_URL! }),
});
const drizzleDb = await drizzlePostgres(db, { schema: { users } });
const rows = await drizzleDb.select().from(users); // fully typed
await db.close(); // YOU still own the lifetime — Drizzle never ends the pool
Both factories are async (they lazy-import the driver) and take the same
config subset: { schema, logger, casing } — a subset of Drizzle’s stable
DrizzleConfig<TSchema>, so schema/relations/logger/casing generic inference
is preserved exactly.
drizzlePostgres(db, config?)→ Drizzle’sNodePgDatabase<TSchema>with$clientset to the dbSDK-owned pool.drizzleNeonHttp(db, config?)→ Drizzle’sNeonHttpDatabase<TSchema>with$clientset to the dbSDK-owned neon query function.
Schema authoring with dbsdk/orm
dbsdk/orm is a pure re-export of the stable drizzle-orm 0.45.3 root and
pg-core namespaces as a single import — no renames, no wrappers, no
upstream code copied, zero runtime logic (no connection, no pool, no
dispatch). It is PostgreSQL-focused authoring only: no other dialect
surfaces, and no Studio, Kit, seed, or migration tooling is shipped here.
import { createDatabase } from "dbsdk";
import { postgres } from "dbsdk/postgres";
import { drizzlePostgres } from "dbsdk/drizzle";
// Authoring comes from ONE import; execution still goes through dbsdk/drizzle.
import { pgSchema, pgTable, serial, text, integer, relations, eq, asc } from "dbsdk/orm";
const app = pgSchema("myapp");
export const users = app.table("users", {
id: serial("id").primaryKey(),
email: text("email").notNull(),
});
export const posts = app.table("posts", {
id: serial("id").primaryKey(),
authorId: integer("author_id").notNull(),
title: text("title"),
});
export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts) }));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));
const db = createDatabase({
adapter: postgres({ connectionString: process.env.DATABASE_URL! }),
});
const drizzleDb = await drizzlePostgres(db, {
schema: { users, posts, usersRelations, postsRelations },
});
// Typed join select + relational query on the dbSDK-owned pool.
const joined = await drizzleDb
.select({ email: users.email, title: posts.title })
.from(users)
.innerJoin(posts, eq(posts.authorId, users.id));
const feed = await drizzleDb.query.users.findMany({ with: { posts: true } });
await db.close(); // YOU still own the lifetime — Drizzle never ends the pool
Behavior details:
- Same optional peer as the bridge.
dbsdk/ormre-exports the exact objectsdbsdk/drizzleresolves, so a normal install has one physical drizzle-orm copy and schemas authored through the facade just work through the bridge. This shared-peer resolution is the mechanism that keeps a second incompatible copy out of a normal install — it is not an absolute guarantee against unusual dependency layouts that force one. - Without the peer, only this subpath fails. The
dbsdkroot entry never importsdbsdk/orm. Ifdrizzle-ormis not installed, importingdbsdk/ormfails with Node’s standard module-resolution error naming the missing dependency (root imports are unaffected);dbsdk/drizzlekeeps its actionable factory-time refusal. - Full upstream surface, familiar API. The
export *re-exports keep 100% upstream API fidelity (the five type names exported by both root and pg-core resolve explicitly to the pg-core specializations). Two stable quirks to know:one/manyare callback-injected insiderelations(table, ({ one, many }) => …), not module-level exports, and pg-core’s client-freeQueryBuilderis SELECT-only — INSERT/UPDATE/DELETE builders come from an instance created viadbsdk/drizzle. - Strict TypeScript. The packed declarations compile under NodeNext +
exactOptionalPropertyTypes+noUncheckedIndexedAccesswith zero dbSDK-attributed errors (skipLibCheck: true; withskipLibCheck: falsethe ~72 reported errors are all inside drizzle-orm’s own published d.ts — the same upstream boundary as the bridge, unchanged by the facade).
Supported modes
The bridge is deliberately narrow. It accepts what it can verify:
| Database | Accepted by | Why |
|---|---|---|
dbsdk/postgres (any PostgreSQL endpoint) |
drizzlePostgres |
TCP, sessionState: true |
dbsdk/supabase, connectionMode: "direct" |
drizzlePostgres |
TCP, sessionState: true |
dbsdk/supabase, connectionMode: "session" |
drizzlePostgres |
TCP, sessionState: true |
dbsdk/planetscale, connectionMode: "direct" |
drizzlePostgres |
TCP, sessionState: true; pooled mode is refused like any session-stateless pooler |
dbsdk/neon with transport: "http" |
drizzleNeonHttp |
separate explicit factory, no pool |
Refused before the raw handle is touched or any pool is created, with a
DbError code CONFIGURATION:
- Transaction-mode poolers (
sessionState: false), e.g. SupabaseconnectionMode: "transaction". dbSDK’s per-statement session-state guards live in its query functions, not in the pool — Drizzle executes through the pool directly and would bypass them. Use direct or session mode instead. - HTTP databases passed to
drizzlePostgres(usedrizzleNeonHttp) and TCP databases passed todrizzleNeonHttp(usedrizzlePostgres). - Neon’s websocket transport. Its
raw.poolis structurally pg-compatible, but dbSDK has not verified that mode for this bridge, so it is refused rather than claimed.
No DSN, no connection, no client
Drizzle’s own drizzle() also accepts a connection string or a
{ connection: ... } config and would construct its own pg.Pool or
neon() client. That would silently bypass dbSDK’s pool configuration —
including TLS with certificate verification on by default, CA pinning, and
lifetime ownership. The bridge therefore accepts only a validated dbSDK-owned
handle:
await drizzlePostgres(db, { connection: "postgres://..." }); // TypeScript error
await drizzlePostgres(db, { client: somePool }); // TypeScript error
await drizzlePostgres("postgres://..." as never); // TypeScript error
JavaScript callers hit the same refusals at runtime, before any dispatch. No error message ever contains your connection string.
What the bridge does not change (honest boundaries)
- Errors. Only the bridge’s pre-execution validation uses dbSDK error
conventions (
DbError, codeCONFIGURATION). Queries, transactions, and batches executed through the returned Drizzle instance surface native Drizzle/driver errors — they are not normalized intoDbError, and dbSDK’s indeterminate-write semantics do not extend to Drizzle’s executor. - Lifetime.
db.close()ends the pool. A previously returned Drizzle instance then fails on use (the pool was ended) and is never recreated. For Neon HTTP there is no pool to end, so a previously returned instance keeps working after close — there is simply nothing to release. - Prepared statements. Over TCP, Drizzle may use its own prepared statements; dbSDK’s “no named prepared statements” invariant applies to dbSDK’s own executor, not to Drizzle’s. Over Neon HTTP there are no prepared statements at all.
- Neon HTTP limits are the driver’s, unchanged: no interactive
transactions (
db.transactionon the Drizzle instance throws), no session state, no automatic WebSocket fallback, no retry. Use dbSDK’sdb.batchfor atomic multi-statement writes, or a transaction transport on the adapter for dbSDK-level transactions.
What is next
The next workflow unit is the migration lifecycle: provision via
management, get the connection string, generate and apply
schema with drizzle-kit (generate/push, dev-time CLI) and
drizzle-orm/migrator’s migrate() over this same bridge, then seed. This
page will show that end-to-end when it lands.
