---
title: Drizzle
description: 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.
sidebar:
  order: 16
---

`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`](#schema-authoring-with-dbsdkorm)).

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.

```sh
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

```ts
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's `NodePgDatabase<TSchema>` with
  `$client` set to the dbSDK-owned pool.
- `drizzleNeonHttp(db, config?)` → Drizzle's `NeonHttpDatabase<TSchema>` with
  `$client` set 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.

```ts
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/orm` re-exports the exact
  objects `dbsdk/drizzle` resolves, 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 `dbsdk` root entry never
  imports `dbsdk/orm`. If `drizzle-orm` is not installed, importing
  `dbsdk/orm` fails with Node's standard module-resolution error naming the
  missing dependency (root imports are unaffected); `dbsdk/drizzle` keeps 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`/`many` are callback-injected inside
  `relations(table, ({ one, many }) => …)`, not module-level exports, and
  pg-core's client-free `QueryBuilder` is SELECT-only — INSERT/UPDATE/DELETE
  builders come from an instance created via `dbsdk/drizzle`.
- **Strict TypeScript.** The packed declarations compile under NodeNext +
  `exactOptionalPropertyTypes` + `noUncheckedIndexedAccess` with zero
  dbSDK-attributed errors (`skipLibCheck: true`; with `skipLibCheck: false`
  the ~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. Supabase
  `connectionMode: "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` (use `drizzleNeonHttp`) and
  TCP databases passed to `drizzleNeonHttp` (use `drizzlePostgres`).
- **Neon's websocket transport.** Its `raw.pool` is 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:

```ts
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`, code `CONFIGURATION`). Queries, transactions, and
  batches executed *through the returned Drizzle instance* surface native
  Drizzle/driver errors — they are not normalized into `DbError`, 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.transaction` on the Drizzle instance throws), no session
  state, no automatic WebSocket fallback, no retry. Use dbSDK's `db.batch` for
  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](/docs/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.
