Provides expert guidance on Drizzle ORM schema design, migration management, and type-safe query building.

Install

mkdir -p .claude/skills/drizzle-orm && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/1104" && unzip -o skill.zip -d .claude/skills/drizzle-orm && rm skill.zip

Installs to .claude/skills/drizzle-orm

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Drizzle ORM patterns: schema definitions, Drizzle Kit migrations, query builders, type branding, custom types, SQLite, Postgres, D1, and Turso/libSQL boundaries. Use when mentioning Drizzle, drizzle-orm, DB schemas, migrations, branded column types, or typed SQL queries.
271 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Intermediate

Key capabilities

  • Define SQL schemas with explicit table relations
  • Configure Drizzle Kit migrations using snapshot diffs
  • Implement branded TypeScript types using $type<T>()
  • Build type-safe SQL queries with the query builder
  • Perform custom type conversions for non-identity data

How it works

It uses a typed query builder for application queries and provides schema-level utilities to define database structures. It enforces type safety by mapping database columns to TypeScript types, including branded types for compile-time validation.

Inputs & outputs

You give it
Database schema definition or raw SQL query
You get back
Type-safe query builder instance or generated migration SQL

When to use drizzle-orm

  • Define a new database schema
  • Configure Drizzle Kit migrations
  • Build type-safe SQL queries
  • Implement custom column type conversions

About this skill

Drizzle ORM Guidelines

Reference Repositories

  • Drizzle ORM : TypeScript ORM with SQL-like query builder
  • Turso : Edge-hosted LibSQL database (Epicenter's database)

Upstream Grounding

When Drizzle schema definitions, migration snapshots, query builder APIs, column typing, custom types, or driver integration affect correctness, use source-backed grounding before relying on memory. If DeepWiki MCP is available, ask a narrow question against drizzle-team/drizzle-orm; for libSQL, Turso sync, embedded replicas, D1 compatibility, or remote SQLite behavior, ask against tursodatabase/turso. If DeepWiki is unavailable or the repo is not indexed, use upstream source or official docs directly. Treat DeepWiki as orientation, then verify decisive details against local installed types, generated migrations, source, driver versions, or official docs before changing code.

Skip DeepWiki for repo-local schema naming and storage-boundary conventions already documented below.

Schema And Migration Rules

  • Export a single schema object from the app's database module and pass that same object to drizzle(...) and Drizzle Kit config.
  • Keep table definitions, relations, and schema exports explicit. Avoid dynamic schema construction that Drizzle Kit cannot statically inspect.
  • Treat Drizzle Kit snapshots as the diff source of truth. Review generated SQL and snapshot changes together.
  • Pick a casing strategy once per database. Do not mix app-level camelCase with ad hoc SQL aliases unless the boundary owns that mapping.
  • Use drizzle-zod, drizzle-valibot, or a local schema parser at IO boundaries when external input becomes a row. Do not treat inferred insert types as runtime validation.

Query Builder Rules

  • Prefer the typed query builder for application queries. Use raw SQL only for expressions the query builder cannot express cleanly.
  • Keep joins and selected shapes near the caller that owns the response contract.
  • Add indexes in schema beside the query pattern that needs them.

Driver Boundaries

  • bun:sqlite and better-sqlite3 are local synchronous SQLite drivers. Do not use them in Cloudflare Workers.
  • D1 is a Cloudflare binding with Worker-specific behavior. Keep it behind Worker code and generated bindings.
  • libSQL and Turso are SQLite-compatible but have network, sync, and compatibility details that are not generic SQLite. Use the turso skill for those decisions.

Use $type<T>() for Branded Strings, Not customType

When you need a column with a branded TypeScript type but no actual data transformation, use $type<T>() instead of customType.

The Rule

If toDriver and fromDriver would be identity functions (x) => x, use $type<T>() instead.

Why

Even with identity functions, customType still invokes mapFromDriverValue on every row:

// drizzle-orm/src/utils.ts - runs for EVERY column of EVERY row
const rawValue = row[columnIndex]!;
const value = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);

Query 1000 rows with 3 date columns = 3000 function calls doing nothing.

Bad Pattern

// Runtime overhead for identity functions
customType<{ data: DateTimeString; driverParam: DateTimeString }>({
	dataType: () => 'text',
	toDriver: (value) => value, // called on every write
	fromDriver: (value) => value, // called on every read
});

Good Pattern

// Zero runtime overhead - pure type assertion
text().$type<DateTimeString>();

$type<T>() is a compile-time-only type override:

// drizzle-orm/src/column-builder.ts
$type<TType>(): $Type<this, TType> {
  return this as $Type<this, TType>;
}

When to Use customType

Only when data genuinely transforms between app and database:

// JSON: object ↔ string - actual transformation
customType<{ data: UserPrefs; driverParam: string }>({
	toDriver: (value) => JSON.stringify(value),
	fromDriver: (value) => JSON.parse(value),
});

Keep Data in Intermediate Representation

Prefer keeping data serialized (strings) through the system, parsing only at the edges (UI components).

The principle: If data enters serialized and leaves serialized, keep it serialized in the middle. Parse at the edges where you actually need the rich representation.

Example: DateTimeString

Instead of parsing DateTimeString into Temporal.ZonedDateTime at the database layer:

// Bad: parse on every read, re-serialize at API boundaries
customType<{ data: Temporal.ZonedDateTime; driverParam: string }>({
	fromDriver: (value) => fromDateTimeString(value),
});

Keep it as a string until the UI actually needs it:

// Good: string stays string, parse only in date-picker component
text().$type<DateTimeString>();

// In UI component:
const temporal = fromDateTimeString(row.createdAt);
// After edit:
const updated = toDateTimeString(temporal);

When not to use it

  • Using customType for identity functions where $type<T>() suffices
  • Dynamic schema construction that Drizzle Kit cannot inspect
  • Mixing app-level casing with ad hoc SQL aliases

Prerequisites

TypeScript environmentDrizzle ORM installedDrizzle Kit configured

Limitations

  • Requires explicit schema definitions for static inspection
  • Performance overhead if customType is used for identity functions

How it compares

Unlike manual SQL string concatenation, this approach provides compile-time type checking and automated migration management.

Compared to similar skills

drizzle-orm side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
drizzle-orm (this skill)322moNo flagsIntermediate
backend-dev16moNo flagsIntermediate
prisma-database06moReviewIntermediate
ck:databases03moReviewIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

Search skills

Search the agent skills registry