postgres-drizzle

$npx mdskill add ccheney/robust-skills/postgres-drizzle

Build type-safe PostgreSQL apps with Drizzle ORM best practices.

  • Solves database schema, query, and migration complexity in PostgreSQL projects.
  • Depends on PostgreSQL, Drizzle ORM, drizzle-kit, and Node.js/TypeScript.
  • Checks Drizzle version to use correct relations API and query syntax.
  • Generates schema, queries, migrations, and connection code following best practices.

SKILL.md

.github/skills/postgres-drizzleView on GitHub ↗
---
name: postgres-drizzle
description: Proactively apply when creating APIs, backends, or data models. Triggers on PostgreSQL, Postgres, Drizzle, drizzle-orm, drizzle-kit, database, schema, pgTable, tables, columns, indexes, queries, migrations, ORM, relations, relational queries, joins, transactions, SQL, connection pooling, PgBouncer, N+1, JSONB, RLS, full-text search, partitioning. Use when writing database schemas, queries, migrations, connection setup, or any database-related code. PostgreSQL and Drizzle ORM best practices.
---

# PostgreSQL + Drizzle ORM

Type-safe database applications with PostgreSQL 17/18 and Drizzle ORM.

## Version Check (do this first)

Drizzle's API changed significantly between the stable 0.x line and v1.0. Check
`package.json` before writing code, because the two relations APIs are incompatible
and must not be mixed:

| `drizzle-orm` version | Relations API | Query filters |
|-----------------------|---------------|---------------|
| `^0.x` (npm `latest`) | `relations()` per table, `drizzle(client, { schema })` | `where: eq(users.id, id)` |
| `1.0.0-beta.*` / `1.0.0-rc.*` | `defineRelations()` once for all tables, `drizzle(client, { relations })` | `where: { id: userId }` (object style) |

The official docs site (orm.drizzle.team) documents v1.0 syntax on its main pages.
This skill defaults to **stable 0.x** syntax; for v1.0 projects read
[references/RELATIONS.md](references/RELATIONS.md) § "Relational Queries v2".
Signals a project is on v1.0: `defineRelations` imports, object-style `where`,
`r.many.posts()` in relations, `from`/`to` keys instead of `fields`/`references`.

## Essential Commands

```bash
npx drizzle-kit generate   # Generate SQL migration from schema changes
npx drizzle-kit migrate    # Apply pending migrations
npx drizzle-kit push       # Push schema directly (dev/prototyping only)
npx drizzle-kit pull       # Introspect existing DB into a schema file
npx drizzle-kit studio     # Open database browser
npx drizzle-kit check      # Detect migration collisions (race conditions)
```

## Quick Decision Trees

### "How do I model this relationship?"

```
Relationship type?
├─ One-to-many (user has posts)     → FK on "many" side + relations()
├─ Many-to-many (posts have tags)   → Junction table with composite PK + relations()
├─ One-to-one (user has profile)    → FK with unique constraint
└─ Self-referential (comments)      → FK to same table (type the ref as AnyPgColumn)
```

### "Why is my query slow?"

```
Slow query?
├─ Missing index on WHERE/JOIN columns  → Add index (Postgres does NOT auto-index FKs)
├─ Query per row in a loop (N+1)        → Use relational queries (`with:`) or a join
├─ Full table scan                      → EXPLAIN (ANALYZE, BUFFERS), add index
├─ Large OFFSET pagination              → Switch to cursor/keyset pagination
└─ Connection overhead per request      → Pool connections (pg Pool / postgres.js / PgBouncer)
```

### "Which drizzle-kit command?"

```
What do I need?
├─ Schema changed, need versioned SQL   → drizzle-kit generate, review SQL, then migrate
├─ Apply migrations (CI, prod)          → drizzle-kit migrate (or migrate() in code)
├─ Quick local iteration, throwaway DB  → drizzle-kit push
├─ Adopt Drizzle on an existing DB      → drizzle-kit pull
└─ Hand-written SQL (triggers, backfill)→ drizzle-kit generate --custom
```

## Connection Setup

```typescript
// node-postgres — pass a URL and Drizzle creates a Pool for you
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from './schema';

export const db = drizzle(process.env.DATABASE_URL!, { schema });
```

```typescript
// postgres.js — built-in pooling; set prepare: false behind a
// transaction-mode pooler (PgBouncer/Supavisor) unless it supports prepared statements
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';

const client = postgres(process.env.DATABASE_URL!, { max: 20 });
export const db = drizzle(client, { schema });
```

Passing `schema` is what enables `db.query.*` relational queries — forgetting it is
the most common cause of "Property 'users' does not exist on type ...".

Optional: `drizzle(url, { schema, casing: 'snake_case' })` maps camelCase TS keys to
snake_case columns so you can write `pgTable('users', { createdAt: timestamp() })`
without repeating column names. Set the same `casing` in `drizzle.config.ts`.

## Schema Patterns

### Basic Table with Timestamps

```typescript
import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp('updated_at', { withTimezone: true })
    .defaultNow()
    .notNull()
    .$onUpdate(() => new Date()),
});
```

Prefer `timestamp(..., { withTimezone: true })` (timestamptz) — naive timestamps
cause silent timezone bugs. For integer PKs, prefer
`integer().primaryKey().generatedAlwaysAsIdentity()` over `serial()` (the
PostgreSQL-recommended form; serial is legacy).

### Foreign Key with Index

```typescript
import { index } from 'drizzle-orm/pg-core';

export const posts = pgTable('posts', {
  id: uuid('id').primaryKey().defaultRandom(),
  userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  title: varchar('title', { length: 255 }).notNull(),
}, (table) => [
  // Postgres creates NO index for FK columns — add one or JOINs/cascades scan
  index('posts_user_id_idx').on(table.userId),
]);
```

The third `pgTable` argument returns an **array** (the older object form is deprecated).

### Relations (stable 0.x API)

```typescript
import { relations } from 'drizzle-orm';

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, { fields: [posts.userId], references: [users.id] }),
}));
```

`relations()` is application-level metadata for `db.query.*` — it does not create
FK constraints. Define both (`.references()` for the DB, `relations()` for queries).

## Query Patterns

```typescript
import { eq } from 'drizzle-orm';

// Relational query — nested data in one round trip, no N+1
const usersWithPosts = await db.query.users.findMany({
  with: { posts: true },
});

// SQL-like query — filters, joins, aggregations
const activeUsers = await db
  .select()
  .from(users)
  .where(eq(users.status, 'active'));

// Transaction — all statements commit or roll back together
await db.transaction(async (tx) => {
  const [user] = await tx.insert(users).values({ email }).returning();
  await tx.insert(profiles).values({ userId: user.id });
});
```

Inside a transaction, always use `tx`, not `db` — queries on `db` escape the
transaction and won't roll back.

## Performance Checklist

| Priority | Check | Impact |
|----------|-------|--------|
| CRITICAL | Index all foreign keys | Prevents full scans on JOINs and cascaded deletes |
| CRITICAL | Use relational queries or joins for nested data | Avoids N+1 |
| HIGH | Connection pooling in production | Each PG connection costs ~MBs of RAM |
| HIGH | `EXPLAIN (ANALYZE, BUFFERS)` slow queries | Identifies missing indexes |
| MEDIUM | Partial indexes for filtered subsets | Smaller, faster indexes |
| MEDIUM | UUIDv7 (`uuidv7()`, PG18+) or identity for PKs | Better index locality than UUIDv4 |

## Anti-Patterns

| Anti-Pattern | Problem | Fix |
|--------------|---------|-----|
| No FK index | Slow JOINs, slow cascades | Add index on every FK column |
| N+1 in loops | Query per row | `with:` relational queries or a join |
| One connection per request | Connection storms, RAM exhaustion | pg `Pool` / postgres.js `max` / PgBouncer |
| `push` in prod | No history, data-loss prompts | `generate` + `migrate` |
| Mixing 0.x `relations()` with v1.0 `defineRelations` | Type errors, broken `db.query` | Pick one per project (see Version Check) |
| Storing JSON as `text` | No validation, no indexing | `jsonb()` column + GIN index |
| `timestamp` without timezone | Silent TZ bugs | `{ withTimezone: true }` |
| Editing applied migration files | Checksum mismatch, drift | New migration (`generate` / `generate --custom`) |

## Reference Documentation

| Read this | When you are... |
|-----------|-----------------|
| [references/SCHEMA.md](references/SCHEMA.md) | Defining tables: column types, constraints, indexes, enums, generated columns |
| [references/QUERIES.md](references/QUERIES.md) | Writing selects, inserts, upserts, transactions, prepared statements |
| [references/RELATIONS.md](references/RELATIONS.md) | Modeling relations or using `db.query.*` — includes the v1.0 RQB v2 API |
| [references/MIGRATIONS.md](references/MIGRATIONS.md) | Configuring drizzle-kit, generating/applying migrations, custom SQL |
| [references/POSTGRES.md](references/POSTGRES.md) | Using PG17/18 features, RLS, partitioning, JSONB ops, full-text search |
| [references/PERFORMANCE.md](references/PERFORMANCE.md) | Indexing strategy, EXPLAIN, pooling, pagination, bulk operations |
| [references/CHEATSHEET.md](references/CHEATSHEET.md) | Needing a compact syntax reminder for any of the above |

## Resources

- Drizzle ORM docs: https://orm.drizzle.team (documents v1.0 syntax; see Version Check)
- Drizzle GitHub: https://github.com/drizzle-team/drizzle-orm
- PostgreSQL docs: https://www.postgresql.org/docs/current/
- Row-Level Security: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
- Index types: https://www.postgresql.org/docs/current/indexes-types.html

More from ccheney/robust-skills

SkillDescription
clean-ddd-hexagonalProactively apply when designing APIs, microservices, or scalable backend structure. Triggers on DDD, Clean Architecture, Hexagonal, ports and adapters, entities, value objects, domain events, CQRS, event sourcing, repository pattern, use cases, onion architecture, outbox pattern, aggregate root, anti-corruption layer. Use when working with domain models, aggregates, repositories, or bounded contexts. Clean Architecture + DDD + Hexagonal patterns for backend services, language-agnostic (Go, Rust, Python, TypeScript, Java, C#).
feature-slicingProactively apply when creating new features/components/pages or setting up frontend project structure. Triggers on FSD, feature slicing, Feature-Sliced Design, frontend architecture, layer structure, module boundaries, scalable frontend, slice organization, public API, barrel exports, import rules, Steiger. Use when restructuring React/Next.js/Vue/Remix projects, organizing frontend code, fixing import violations, deciding where code belongs (entity vs feature vs widget vs shared), or migrating legacy codebases. Feature-Sliced Design (FSD) architecture for frontend projects.
modern-cssProactively apply when creating design systems, component libraries, or any frontend application. Triggers on CSS Grid, Subgrid, Flexbox, Container Queries, :has(), @layer, @scope, CSS nesting, @property, @function, if(), oklch, color-mix, light-dark, relative color, @starting-style, scroll-driven animations, view transitions, anchor positioning, popover, customizable select, content-visibility, logical properties, text-wrap, interpolate-size, clamp, field-sizing, modern CSS, CSS architecture, responsive design, dark mode, theming, design tokens, cascade layers. Use when writing CSS for any web project, choosing layout approaches, building responsive components, implementing dark mode or theming, creating animations or transitions, styling form elements, or modernizing legacy stylesheets. Modern CSS features and best practices for building interfaces with pure native CSS.
modern-javascriptProactively apply when creating web applications, Node.js services, or any JavaScript project. Triggers on JavaScript, ES6, ES2020, ES2022, ES2024, ES2025, ES2026, modern JS, refactor legacy, array methods, async/await, optional chaining, nullish coalescing, destructuring, spread, rest, template literals, arrow functions, toSorted, toReversed, at, groupBy, iterator helpers, Set union intersection difference, Promise, Promise.try, RegExp.escape, Array.fromAsync, using await using, Temporal, functional programming. Use when writing new JavaScript code, refactoring legacy code, modernizing codebases, implementing functional patterns, checking which ECMAScript edition or runtime supports a feature, or reviewing JS for performance and readability. Modern JavaScript (ES6-ES2026) patterns and best practices.
slack-block-kitProactively apply when generating Slack API payloads with blocks, chat.postMessage calls with structured content, streaming AI responses, or views.open/views.publish calls. Triggers on Block Kit, Slack blocks, section block, actions block, header block, context block, alert block, card block, carousel block, container block, table block, data table block, data visualization block, Slack chart, markdown block, rich text block, image block, input block, video block, context_actions block, plan block, task_card block, chat.startStream, chat.appendStream, chat.stopStream, Slack modal, Slack App Home, Slack surfaces, Slack interactive elements, Slack button, select menu, overflow, datepicker, checkboxes, radio buttons, Work Objects, Slack link unfurl, views.open, views.update, views.push, views.publish, Slack composition objects. Use when building Block Kit payloads, blocks arrays, modals, App Home views, interactive elements, link unfurling, streaming agent output, or rich message layouts.
slack-mrkdwnProactively apply when generating any Slack text content, chat.postMessage text fields, or text objects with type "mrkdwn". Triggers on mrkdwn, Slack formatting, Slack markdown, Slack bold, Slack italic, Slack link syntax, Slack mentions, Slack date formatting, Slack escaping, Slack text object, verbatim, plain_text, Slack mrkdwn vs markdown, markdown_text, Slack blockquote, Slack code block, Slack strikethrough, Slack user mention, Slack channel mention, Slack emoji, link_names, auto-parsing, unfurl_links, mrkdwn_in. Use when formatting Slack message text, writing mrkdwn strings, constructing text objects, escaping user content for Slack, adding mentions or date formatting to messages, streaming markdown to Slack, or debugging text rendering issues. Slack mrkdwn text formatting syntax for messages, text objects, and attachments.
teams-adaptive-cardsProactively apply when generating Microsoft Teams Adaptive Card JSON, Bot Framework card attachments, Teams SDK card responses, message extension cards, Microsoft Graph chatMessage Adaptive Card attachments, Incoming Webhook or Workflows webhook Adaptive Cards, notification cards, approval cards, form cards, Action.Submit, Action.Execute, Universal Actions, refresh, user-specific views, card mentions, msteams.entities, msteams.width, Teams responsive card layout, CodeBlock, Table, FactSet, ColumnSet, Input.ChoiceSet, People Picker, or debugging Teams card rendering. Use when constructing, validating, wrapping, or migrating Teams card payloads, including MessageCard-to-Adaptive-Card migrations.
teams-message-formattingProactively apply when generating Microsoft Teams message text, Bot Framework activities, Teams SDK bot responses, Microsoft Graph chatMessage bodies, Teams Adaptive Card TextBlock or FactSet text, Workflows webhook messages, legacy connector MessageCard text, mentions, emoji, code blocks, links, date/time text, or escaping user content for Teams. Triggers on Teams markdown, Teams Markdown, Teams HTML, Teams bot textFormat, Teams message formatting, Teams mentions, at-mention tags, msteams.entities, Graph chatMessage, contentType html, Adaptive Card Markdown, TextBlock markdown, FactSet markdown, Workflows webhook, Incoming Webhook, MessageCard, connector card, tag mention, channel mention, Teams code block, Teams emoji, and Teams escaping. Use when writing or debugging any Microsoft Teams text rendering issue.