.claude/skills/drizzle-patterns/SKILL.md
Drizzle ORM best practices including schema design with relationships, database migrations, prepared statements for performance, transactions, indexes, Turso SQLite database operations, type safety patterns, query optimization, and database workflow for ree-board project
npx skillsauth add DW225/ree-board drizzle-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Activate this skill when working on:
Primary Keys: All tables use Nano IDs for primary keys
Schema Pattern:
// db/schema.ts
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
import { relations } from "drizzle-orm";
import { nanoid } from "nanoid";
export const boardTable = sqliteTable(
"board",
{
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
userId: text("user_id").notNull(),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
() => new Date()
),
},
(table) => ({
userIdIndex: index("board_user_id_index").on(table.userId),
})
);
export const postTable = sqliteTable(
"post",
{
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
boardId: text("board_id")
.notNull()
.references(() => boardTable.id, { onDelete: "cascade" }),
userId: text("user_id").notNull(),
content: text("content").notNull(),
type: text("type", {
enum: ["went_well", "to_improve", "action_items"],
}).notNull(),
voteCount: integer("vote_count").default(0),
},
(table) => ({
boardIdIndex: index("post_board_id_index").on(table.boardId),
userIdIndex: index("post_user_id_index").on(table.userId),
})
);
// Define relationships
export const boardRelations = relations(boardTable, ({ many }) => ({
posts: many(postTable),
members: many(memberTable),
}));
export const postRelations = relations(postTable, ({ one, many }) => ({
board: one(boardTable, {
fields: [postTable.boardId],
references: [boardTable.id],
}),
votes: many(voteTable),
}));
Step-by-Step Process:
Modify Schema (db/schema.ts)
Generate Migration:
pnpm generate
Review Generated SQL (in drizzle/ folder)
Apply to Development:
pnpm push:dev
Test Thoroughly
Apply to Production:
pnpm push
Example Migration:
// After adding a new column to schema.ts:
export const boardTable = sqliteTable("board", {
id: text("id").primaryKey(),
name: text("name").notNull(),
description: text("description"), // ✅ New field
// ...
});
Critical: Use prepared statements for repeated queries
Pattern:
// lib/db/post.ts
import { db } from "@/db";
import { postTable } from "@/db/schema";
import { eq, sql } from "drizzle-orm";
// ✅ Prepared statement for repeated queries
export const prepareFetchPostsByBoardID = db
.select()
.from(postTable)
.where(eq(postTable.boardId, sql.placeholder("boardId")))
.prepare();
// Usage
export async function getPostsByBoardId(boardId: string) {
return prepareFetchPostsByBoardID.execute({ boardId });
}
When to Use Prepared Statements:
Use for Multi-Table Operations:
import { db } from "@/db";
export async function createBoardWithMember(name: string, userId: string) {
return db.transaction(async (tx) => {
// Create board
const [board] = await tx
.insert(boardTable)
.values({
id: nanoid(),
name,
userId,
})
.returning();
// Add owner as member
await tx.insert(memberTable).values({
id: nanoid(),
boardId: board.id,
userId,
role: "owner",
});
return board;
});
}
Transaction Best Practices:
Index All Foreign Keys and Frequently Queried Columns:
export const postTable = sqliteTable(
"post",
{
id: text("id").primaryKey(),
boardId: text("board_id").notNull(),
userId: text("user_id").notNull(),
type: text("type"),
},
(table) => ({
// ✅ Index foreign keys
boardIdIndex: index("post_board_id_index").on(table.boardId),
userIdIndex: index("post_user_id_index").on(table.userId),
// ✅ Index frequently filtered columns
typeIndex: index("post_type_index").on(table.type),
})
);
Indexing Guidelines:
Efficient Queries:
import { db } from "@/db";
import { postTable, voteTable } from "@/db/schema";
import { eq, and, count, inArray, sql } from "drizzle-orm";
// ✅ Select only needed columns
export async function getBoardPostSummary(boardId: string) {
return db
.select({
id: postTable.id,
content: postTable.content,
voteCount: postTable.voteCount,
})
.from(postTable)
.where(eq(postTable.boardId, boardId));
}
// ✅ Use aggregations efficiently
export async function getVoteCountForPost(postId: string) {
const [result] = await db
.select({ count: count() })
.from(voteTable)
.where(eq(voteTable.postId, postId));
return result.count;
}
// ✅ Batch operations
export async function updatePostVotes(postIds: string[]) {
return db
.update(postTable)
.set({ voteCount: sql`vote_count + 1` })
.where(inArray(postTable.id, postIds));
}
Bad:
export async function getPostsByBoardId(boardId: string) {
// ❌ Query parsed every time
return db.select().from(postTable).where(eq(postTable.boardId, boardId));
}
Good:
const preparedQuery = db
.select()
.from(postTable)
.where(eq(postTable.boardId, sql.placeholder("boardId")))
.prepare();
export async function getPostsByBoardId(boardId: string) {
// ✅ Prepared statement reused
return preparedQuery.execute({ boardId });
}
Bad:
export const postTable = sqliteTable("post", {
id: text("id").primaryKey(),
boardId: text("board_id").notNull(),
// ❌ No index on foreign key
});
Good:
export const postTable = sqliteTable(
"post",
{
id: text("id").primaryKey(),
boardId: text("board_id").notNull(),
},
(table) => ({
// ✅ Index on foreign key
boardIdIndex: index("post_board_id_index").on(table.boardId),
})
);
Bad:
// ❌ Two separate operations - potential inconsistency
await db.insert(boardTable).values(board);
await db.insert(memberTable).values(member); // Could fail leaving orphaned board
Good:
// ✅ Transaction ensures atomicity
await db.transaction(async (tx) => {
await tx.insert(boardTable).values(board);
await tx.insert(memberTable).values(member);
});
Bad:
// ❌ Fetches all columns
const posts = await db
.select()
.from(postTable)
.where(eq(postTable.boardId, boardId));
Good:
// ✅ Select only needed columns
const posts = await db
.select({
id: postTable.id,
content: postTable.content,
})
.from(postTable)
.where(eq(postTable.boardId, boardId));
Bad:
// ❌ Directly modifying schema and pushing without generating migration
// Changes schema.ts
// Runs: pnpm push
Good:
// ✅ Proper migration workflow
// 1. Modify schema.ts
// 2. Run: pnpm generate
// 3. Review generated SQL
// 4. Run: pnpm push:dev (test)
// 5. Run: pnpm push (production)
db/schema.ts - Complete database schema with all tables and relationshipsdb/index.ts - Database connection configurationlib/db/ - Prepared statements and query utilities by entitydrizzle.config.ts - Drizzle configuration for migrationsdrizzle/ - Generated migration filesDevelopment:
// Uses local SQLite file
DATABASE_URL=file:test.db
Production:
# Uses Turso Cloud
TURSO_DATABASE_URL=<TURSO_DATABASE_URL>
TURSO_AUTH_TOKEN=<TURSO_AUTH_TOKEN>
Fetch Board with Posts:
const board = await db.query.boardTable.findFirst({
where: eq(boardTable.id, boardId),
with: {
posts: true,
members: true,
},
});
Update Vote Count:
await db
.update(postTable)
.set({ voteCount: sql`vote_count + 1` })
.where(eq(postTable.id, postId));
Last Updated: 2026-01-10
development
Jest testing strategies, test organization, factory patterns for test data, mocking strategies for authentication and external services, real-time message processor testing, test-driven development workflow, unit vs integration testing, fake timer usage for time-dependent tests, and testing best practices for ree-board project
development
Preact Signals for reactive state management, signal vs computed signal usage, batch updates for performance, action creator patterns, signal integration with React components, state management by domain (boards posts members), reactive patterns, and signal best practices for ree-board project
testing
Role-based access control (RBAC) patterns, authentication wrappers, authorization checks, input validation with Zod schemas, security boundaries, server action security, real-time message validation, preventing common vulnerabilities like XSS and SQL injection, and security best practices for ree-board project
tools
Next.js 16 App Router patterns including server components, client components, server actions, route handlers, layouts, metadata API, dynamic routes, file conventions, data fetching, caching strategies, and Next.js best practices for building modern React applications