# Database Design Instructions > **Parent Document**: `main-instructions.md` - Read that first! ## 🎯 Database Design Principles ### Rule #1: Mirror the UI Structure Every table and column must correspond to what users see and interact with in the UI. **Example**: If the Work Order form has fields for: - Title - Description - Status - Assigned To - Due Date Then the `work_orders` table must have exactly these columns (plus system fields like id, createdAt, updatedAt). ### Schema Design Process 1. **Start with the UI**: - List all forms and their fields - Identify all data displayed in lists/tables - Note all filters and search criteria - Document all relationships shown 2. **Design the Tables**: - One table per entity (User, WorkOrder, Project, etc.) - Include only fields that appear in UI or are needed for system function - Add system fields: `id`, `createdAt`, `updatedAt`, (optionally `deletedAt`) - Define relationships based on UI connections 3. **Verify Completeness**: - Can every UI element be populated from the database? - Can every user action be persisted to the database? - Are all filters and sorts supported by the schema? ## ⚡ Performance & Scalability ### Indexes for Frequently Queried Columns **ALWAYS add indexes for frequently queried columns:** ```typescript import { pgTable, uuid, text, timestamp, index, pgEnum, } from "drizzle-orm/pg-core"; export const workOrderStatusEnum = pgEnum("work_order_status", [ "draft", "pending", "approved", "completed", ]); export const workOrders = pgTable( "work_orders", { id: uuid("id").primaryKey().defaultRandom(), title: text("title").notNull(), status: workOrderStatusEnum("status").default("draft").notNull(), clientId: uuid("client_id").references(() => clients.id), assignedTo: uuid("assigned_to").references(() => users.id), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), }, (table) => ({ // Index for status filtering (common query) statusIdx: index("work_orders_status_idx").on(table.status), // Index for foreign keys clientIdIdx: index("work_orders_client_id_idx").on(table.clientId), assignedToIdx: index("work_orders_assigned_to_idx").on(table.assignedTo), // Index for sorting by date createdAtIdx: index("work_orders_created_at_idx").on(table.createdAt), }), ); ``` **When to add indexes:** - Fields used in WHERE clauses - Fields used in ORDER BY clauses - Foreign keys - Status/enum fields frequently filtered - Date fields used for sorting ### Query Optimization **1. Always Use Pagination:** ```typescript // ❌ BAD - loads all records const projects = await db.select().from(projects); // ✅ GOOD - paginated results const pageSize = 50; const projects = await db .select() .from(projects) .limit(pageSize) .offset(page * pageSize) .orderBy(desc(projects.createdAt)); ``` **2. Select Only Needed Fields:** ```typescript // ❌ BAD - loads all columns const projects = await db.select().from(projects); // ✅ GOOD - select only needed fields const projects = await db .select({ id: projects.id, title: projects.title, status: projects.status, createdAt: projects.createdAt, }) .from(projects); ``` **3. Avoid N+1 Query Problems:** ```typescript // ❌ BAD - N+1 query problem const projects = await db.select().from(projects); for (const project of projects) { // This runs a query for EACH project! const client = await db .select() .from(clients) .where(eq(clients.id, project.clientId)); } // ✅ GOOD - single query with join const projectsWithClients = await db .select({ projectId: projects.id, projectTitle: projects.title, clientName: clients.name, }) .from(projects) .leftJoin(clients, eq(projects.clientId, clients.id)); // ✅ ALSO GOOD - batch query const clientIds = projects.map((p) => p.clientId).filter(Boolean); const clientList = await db .select() .from(clients) .where(inArray(clients.id, clientIds)); ``` **4. Use Database Aggregations:** ```typescript // ❌ BAD - loading all data to count in memory const projects = await db.select().from(projects); const activeCount = projects.filter((p) => p.status === "active").length; // ✅ GOOD - database-level aggregation const [result] = await db .select({ count: sql`count(*)` }) .from(projects) .where(eq(projects.status, "active")); ``` ### Connection Pooling **Use basic connection pooling (good enough for now, easy to optimize later):** ```typescript import { drizzle } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; // Simple pool configuration - good for single instance const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10, // Start small, increase when needed idleTimeoutMillis: 30000, }); export const db = drizzle(pool); ``` **Pool sizing:** - Start with `max: 10` (enough for development and initial production) - Increase when you see connection timeout errors - When scaling horizontally: adjust per instance (e.g., 5-10 per instance) ### Performance Monitoring **Add these to your queries for debugging:** ```typescript // Log slow queries const startTime = Date.now(); const result = await db.select().from(largeTable); const duration = Date.now() - startTime; if (duration > 1000) { console.warn(`Slow query detected: ${duration}ms`); } // Use EXPLAIN ANALYZE in development const explained = await db.execute(sql` EXPLAIN ANALYZE SELECT * FROM projects WHERE data->>'status' = 'active' `); console.log(explained); ``` ## 🗄️ Drizzle ORM Standards ### Table Definition Template ```typescript import { pgTable, text, timestamp, integer, boolean, uuid, jsonb, } from "drizzle-orm/pg-core"; import { relations } from "drizzle-orm"; export const tableName = pgTable("table_name", { // Primary Key - always UUID id: uuid("id").defaultRandom().primaryKey(), // Business Fields (based on UI) // Add fields that appear in the UI here // Foreign Keys userId: uuid("user_id") .notNull() .references(() => users.id), // System Fields - ALWAYS include these createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), // Optional: Soft Delete deletedAt: timestamp("deleted_at"), }); // Define relations export const tableNameRelations = relations(tableName, ({ one, many }) => ({ user: one(users, { fields: [tableName.userId], references: [users.id], }), // Add other relations here })); ``` ### Column Type Guidelines | UI Element | Database Type | Example | | --------------------- | ------------------------------------------------- | ------------------------------------------ | | Short text input | `text('column_name')` | Name, Title | | Long text / Textarea | `text('column_name')` | Description, Notes | | Number input | `integer('column_name')` or `real('column_name')` | Age, Price | | Checkbox | `boolean('column_name')` | isActive, isCompleted | | Date picker | `timestamp('column_name')` | dueDate, startDate | | Dropdown (predefined) | `text('column_name')` + enum type | Status: 'draft' \| 'active' \| 'completed' | | Multi-select | `jsonb('column_name')` or relation table | Tags, Categories | | File upload | `text('column_name')` (store URL/path) | avatarUrl, documentPath | | Rich text editor | `text('column_name')` | Content, Description (HTML/Markdown) | ### Enum Types For fields with predefined values (like status), define TypeScript enums: ```typescript // Define enum export const workOrderStatusEnum = pgEnum("work_order_status", [ "draft", "pending_review", "approved", "in_progress", "completed", "cancelled", ]); // Use in table export const workOrders = pgTable("work_orders", { id: uuid("id").defaultRandom().primaryKey(), status: workOrderStatusEnum("status").default("draft").notNull(), // ... other fields }); // Export type export type WorkOrderStatus = (typeof workOrders.status.enumValues)[number]; ``` ### Relationship Patterns #### One-to-Many ```typescript // One User has many WorkOrders export const users = pgTable('users', { ... }); export const workOrders = pgTable('work_orders', { id: uuid('id').defaultRandom().primaryKey(), userId: uuid('user_id').notNull().references(() => users.id), }); export const usersRelations = relations(users, ({ many }) => ({ workOrders: many(workOrders), })); export const workOrdersRelations = relations(workOrders, ({ one }) => ({ user: one(users, { fields: [workOrders.userId], references: [users.id], }), })); ``` #### Many-to-Many ```typescript // Many Projects have many Talents (through junction table) export const projects = pgTable('projects', { ... }); export const talents = pgTable('talents', { ... }); export const projectTalents = pgTable('project_talents', { projectId: uuid('project_id').notNull().references(() => projects.id), talentId: uuid('talent_id').notNull().references(() => talents.id), // Additional junction data role: text('role'), rate: real('rate'), createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => ({ pk: primaryKey(table.projectId, table.talentId), })); ``` ## 🔐 Row Level Security (RLS) ### RLS Policy Template Every table that stores user-specific data MUST have RLS policies. ```sql -- Enable RLS on table ALTER TABLE work_orders ENABLE ROW LEVEL SECURITY; -- Policy: Users can only see their own work orders or ones assigned to them CREATE POLICY "Users can view own work orders" ON work_orders FOR SELECT USING ( auth.uid() = user_id OR auth.uid() = assigned_to OR EXISTS ( SELECT 1 FROM user_roles WHERE user_id = auth.uid() AND role IN ('admin', 'manager') ) ); -- Policy: Users can insert their own work orders CREATE POLICY "Users can create work orders" ON work_orders FOR INSERT WITH CHECK (auth.uid() = user_id); -- Policy: Users can update their own work orders CREATE POLICY "Users can update own work orders" ON work_orders FOR UPDATE USING (auth.uid() = user_id OR auth.uid() = assigned_to); -- Policy: Only admins can delete CREATE POLICY "Admins can delete work orders" ON work_orders FOR DELETE USING ( EXISTS ( SELECT 1 FROM user_roles WHERE user_id = auth.uid() AND role = 'admin' ) ); ``` ### RLS Best Practices 1. **Always enable RLS** on tables with user data 2. **Test policies** with different user roles 3. **Use auth context** (`auth.uid()`) to filter data 4. **Combine policies** - Postgres ORs them together 5. **Performance**: Index columns used in RLS policies 6. **Debugging**: Use `EXPLAIN` to see if RLS is applied ## 📊 Query Patterns with Drizzle ### Basic CRUD ```typescript import { db } from "@/lib/getDatabase"; import { workOrders, users } from "@/schema/schema"; import { eq, and, or, desc, like, gte } from "drizzle-orm"; // Create const newWorkOrder = await db .insert(workOrders) .values({ title: "New Work Order", description: "Description here", userId: currentUserId, }) .returning(); // Read (single) const workOrder = await db.query.workOrders.findFirst({ where: eq(workOrders.id, workOrderId), with: { user: true, // Include related user }, }); // Read (multiple with filters) const filteredOrders = await db.query.workOrders.findMany({ where: and( eq(workOrders.status, "active"), gte(workOrders.dueDate, new Date()), ), orderBy: [desc(workOrders.createdAt)], limit: 10, offset: 0, }); // Update await db .update(workOrders) .set({ status: "completed", updatedAt: new Date(), }) .where(eq(workOrders.id, workOrderId)); // Delete (soft delete preferred) await db .update(workOrders) .set({ deletedAt: new Date() }) .where(eq(workOrders.id, workOrderId)); // Hard delete (use sparingly) await db.delete(workOrders).where(eq(workOrders.id, workOrderId)); ``` ### Transactions Use transactions for operations that must succeed or fail together: ```typescript await db.transaction(async (tx) => { // Create work order const [workOrder] = await tx .insert(workOrders) .values({ title: "New Order", userId: currentUserId, }) .returning(); // Create related tasks await tx.insert(tasks).values([ { workOrderId: workOrder.id, title: "Task 1" }, { workOrderId: workOrder.id, title: "Task 2" }, ]); // Update project status await tx .update(projects) .set({ status: "in_progress" }) .where(eq(projects.id, projectId)); }); ``` **CRITICAL RULES FOR TRANSACTIONS**: 1. **Always use transactions when modifying multiple tables** ```typescript // ❌ WRONG - No transaction for multi-table operation await db.insert(orders).values({ ... }) await db.insert(orderItems).values({ ... }) // If this fails, order still created! // ✅ CORRECT - Transaction ensures atomicity await db.transaction(async (tx) => { await tx.insert(orders).values({ ... }) await tx.insert(orderItems).values({ ... }) // All or nothing }) ``` 2. **Use transactions for dependent operations** - Creating parent + children records - Updating related data - Moving data between tables - Any operation where partial completion would leave invalid state 3. **Pessimistic Locking when needed** ```typescript // Use FOR UPDATE to lock rows during transaction await db.transaction(async (tx) => { // Lock the row to prevent concurrent modifications const [inventory] = await tx .select() .from(inventoryTable) .where(eq(inventoryTable.productId, productId)) .for("update"); // Pessimistic lock if (inventory.quantity < requestedAmount) { throw new Error("Insufficient inventory"); } // Update with locked row - safe from race conditions await tx .update(inventoryTable) .set({ quantity: inventory.quantity - requestedAmount }) .where(eq(inventoryTable.productId, productId)); }); ``` 4. **When to use pessimistic locking**: - Inventory/stock management (prevent overselling) - Financial transactions (prevent double-spending) - Ticket/seat reservations (prevent double-booking) - Counter increments that must be accurate - Any scenario where concurrent updates could cause inconsistency 5. **Transaction best practices**: ```typescript // ✅ GOOD - Keep transactions short and focused await db.transaction(async (tx) => { // Only database operations here const result = await tx.insert(...) await tx.update(...) return result }) // ❌ BAD - Don't do slow operations in transactions await db.transaction(async (tx) => { await tx.insert(...) await sendEmail(...) // ❌ Slow! Locks database await callExternalAPI(...) // ❌ Slow! Locks database }) // ✅ CORRECT - Do slow operations after transaction const result = await db.transaction(async (tx) => { return await tx.insert(...) }) await sendEmail(...) // Outside transaction await callExternalAPI(...) // Outside transaction ``` 6. **Atomic operations**: - Each transaction is atomic (all or nothing) - If any operation fails, entire transaction rolls back - Database remains consistent 7. **Example: Complete order processing** ```typescript async function processOrder(userId: string, items: OrderItem[]) { // Start transaction const orderId = await db.transaction(async (tx) => { // 1. Create order const [order] = await tx .insert(orders) .values({ data: { userId, status: "pending", total: 0 }, }) .returning(); let total = 0; // 2. Process each item with pessimistic locking for (const item of items) { // Lock inventory row const [inventory] = await tx .select() .from(inventoryTable) .where(eq(inventoryTable.productId, item.productId)) .for("update"); if (!inventory || inventory.quantity < item.quantity) { throw new Error(`Insufficient stock for ${item.productId}`); } // Deduct inventory await tx .update(inventoryTable) .set({ quantity: inventory.quantity - item.quantity }) .where(eq(inventoryTable.productId, item.productId)); // Create order item await tx.insert(orderItems).values({ data: { orderId: order.id, productId: item.productId, quantity: item.quantity, price: inventory.price, }, }); total += inventory.price * item.quantity; } // 3. Update order total await tx .update(orders) .set({ data: { ...order.data, total, status: "confirmed" } }) .where(eq(orders.id, order.id)); return order.id; }); // 4. Send confirmation email (OUTSIDE transaction) await sendOrderConfirmationEmail(orderId); return orderId; } ``` ## 🎯 Migration Strategy ### Creating Migrations ```bash # Generate migration from schema changes npx drizzle-kit generate:pg # Apply migrations npx drizzle-kit push:pg ``` ### Migration Best Practices 1. **Never edit generated migrations** - change schema and regenerate 2. **Test migrations** on local database first 3. **Backup before migration** in production 4. **Make migrations reversible** when possible 5. **Add indexes separately** for large tables (avoid locking) --- ## 💡 Custom Database Guidelines ### ⚠️ CRITICAL: Proper Schema Design (NO JSONB) **MANDATORY: Design proper database schemas with typed columns** Every table MUST have properly defined columns that match the UI fields. **DO NOT use JSONB for business data**. ```typescript // ❌ BAD - DO NOT USE JSONB for business data export const workOrders = pgTable("work_orders", { id: uuid("id").defaultRandom().primaryKey(), data: jsonb("data").notNull(), // ❌ FORBIDDEN createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); // ✅ GOOD - Proper schema with typed columns export const workOrders = pgTable("work_orders", { id: uuid("id").defaultRandom().primaryKey(), title: text("title").notNull(), description: text("description"), status: workOrderStatusEnum("status").default("draft").notNull(), assignedTo: uuid("assigned_to").references(() => users.id), dueDate: timestamp("due_date"), budget: real("budget"), priority: integer("priority").default(0), createdBy: uuid("created_by") .notNull() .references(() => users.id), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), deletedAt: timestamp("deleted_at"), }); ``` **Why NO JSONB?** - ✅ Type safety at database level - ✅ Better query performance with indexes - ✅ Data integrity with constraints - ✅ Proper foreign keys - ✅ Easier migrations and schema evolution - ✅ Clear documentation of data structure ### 📋 Complete Migration Files with Revert Functions **MANDATORY: Every schema change MUST include migration files with both UP and DOWN functions** When creating or modifying database schemas, you MUST provide complete migration files: ```typescript // drizzle/migrations/0001_create_work_orders.ts import { sql } from "drizzle-orm"; import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; /** * Migration: Create work_orders table * Created: 2026-02-13 */ // UP: Apply migration export async function up(db: PostgresJsDatabase) { await db.execute(sql` -- Create enum for status CREATE TYPE work_order_status AS ENUM ( 'draft', 'pending_review', 'approved', 'in_progress', 'completed', 'cancelled' ); -- Create work_orders table CREATE TABLE work_orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, description TEXT, status work_order_status NOT NULL DEFAULT 'draft', assigned_to UUID REFERENCES users(id) ON DELETE SET NULL, due_date TIMESTAMP, budget REAL, priority INTEGER DEFAULT 0, created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW(), deleted_at TIMESTAMP ); -- Create indexes CREATE INDEX idx_work_orders_status ON work_orders(status); CREATE INDEX idx_work_orders_assigned_to ON work_orders(assigned_to); CREATE INDEX idx_work_orders_created_by ON work_orders(created_by); CREATE INDEX idx_work_orders_due_date ON work_orders(due_date); CREATE INDEX idx_work_orders_created_at ON work_orders(created_at); -- Create updated_at trigger CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ language 'plpgsql'; CREATE TRIGGER update_work_orders_updated_at BEFORE UPDATE ON work_orders FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); `); } // DOWN: Revert migration export async function down(db: PostgresJsDatabase) { await db.execute(sql` -- Drop trigger first DROP TRIGGER IF EXISTS update_work_orders_updated_at ON work_orders; -- Drop function (only if not used by other tables) -- DROP FUNCTION IF EXISTS update_updated_at_column(); -- Drop indexes DROP INDEX IF EXISTS idx_work_orders_status; DROP INDEX IF EXISTS idx_work_orders_assigned_to; DROP INDEX IF EXISTS idx_work_orders_created_by; DROP INDEX IF EXISTS idx_work_orders_due_date; DROP INDEX IF EXISTS idx_work_orders_created_at; -- Drop table DROP TABLE IF EXISTS work_orders; -- Drop enum type DROP TYPE IF EXISTS work_order_status; `); } ``` **Migration File Requirements:** 1. **UP function** - Creates tables, columns, indexes, triggers 2. **DOWN function** - Reverts ALL changes made in UP (in reverse order) 3. **Clean SQL** - Use raw SQL for complex operations 4. **Order matters** - Drop in reverse order of creation 5. **Comments** - Document what each section does ### 📁 Migration File Structure ``` drizzle/ migrations/ 0001_create_users.ts # First migration 0002_create_work_orders.ts # Second migration 0003_add_work_order_tags.ts # Third migration meta/ _journal.json # Migration history ``` ### Complete Schema Design Process **Step 1: Analyze UI Requirements** ``` UI shows Work Order form with: - Title (text input, required) - Description (textarea, optional) - Status (dropdown: draft, pending, approved, in progress, completed) - Assigned To (user select) - Due Date (date picker) - Budget (number input) - Priority (1-5 selector) ``` **Step 2: Design Drizzle Schema** ```typescript // src/schema/work-orders.ts import { pgTable, uuid, text, timestamp, real, integer, pgEnum, } from "drizzle-orm/pg-core"; import { relations } from "drizzle-orm"; import { users } from "./users"; // Define enum for status values export const workOrderStatusEnum = pgEnum("work_order_status", [ "draft", "pending_review", "approved", "in_progress", "completed", "cancelled", ]); // Define table with typed columns export const workOrders = pgTable( "work_orders", { id: uuid("id").defaultRandom().primaryKey(), title: text("title").notNull(), description: text("description"), status: workOrderStatusEnum("status").default("draft").notNull(), assignedTo: uuid("assigned_to").references(() => users.id, { onDelete: "set null", }), dueDate: timestamp("due_date"), budget: real("budget"), priority: integer("priority").default(0), createdBy: uuid("created_by") .notNull() .references(() => users.id, { onDelete: "cascade" }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), deletedAt: timestamp("deleted_at"), }, (table) => ({ // Indexes for common queries statusIdx: index("idx_work_orders_status").on(table.status), assignedToIdx: index("idx_work_orders_assigned_to").on(table.assignedTo), createdByIdx: index("idx_work_orders_created_by").on(table.createdBy), dueDateIdx: index("idx_work_orders_due_date").on(table.dueDate), }), ); // Define relations export const workOrdersRelations = relations(workOrders, ({ one }) => ({ assignee: one(users, { fields: [workOrders.assignedTo], references: [users.id], }), creator: one(users, { fields: [workOrders.createdBy], references: [users.id], }), })); // Export inferred types export type WorkOrder = typeof workOrders.$inferSelect; export type NewWorkOrder = typeof workOrders.$inferInsert; export type WorkOrderStatus = (typeof workOrders.status.enumValues)[number]; ``` **Step 3: Create Migration File** ```typescript // drizzle/migrations/0002_create_work_orders.ts // Include BOTH up() and down() functions as shown above ``` **Step 4: Create Zod Schemas for Validation** ```typescript // src/lib/schemas/work-order-schemas.ts import { z } from "zod"; export const workOrderStatusValues = [ "draft", "pending_review", "approved", "in_progress", "completed", "cancelled", ] as const; export const createWorkOrderSchema = z.object({ title: z.string().min(1, "Title is required").max(200), description: z.string().max(2000).optional(), status: z.enum(workOrderStatusValues).default("draft"), assignedTo: z.string().uuid().nullable().optional(), dueDate: z.coerce.date().nullable().optional(), budget: z.number().positive().nullable().optional(), priority: z.number().int().min(0).max(5).default(0), }); export const updateWorkOrderSchema = createWorkOrderSchema.partial(); export type CreateWorkOrderInput = z.infer; export type UpdateWorkOrderInput = z.infer; ``` ### Altering Existing Tables **Always provide migration with revert:** ```typescript // drizzle/migrations/0005_add_work_order_tags.ts import { sql } from "drizzle-orm"; import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; // UP: Add tags column export async function up(db: PostgresJsDatabase) { await db.execute(sql` -- Add tags column (array of strings) ALTER TABLE work_orders ADD COLUMN tags TEXT[] DEFAULT '{}'; -- Create GIN index for array searching CREATE INDEX idx_work_orders_tags ON work_orders USING GIN (tags); `); } // DOWN: Remove tags column export async function down(db: PostgresJsDatabase) { await db.execute(sql` DROP INDEX IF EXISTS idx_work_orders_tags; ALTER TABLE work_orders DROP COLUMN IF EXISTS tags; `); } ``` ### Indexing Strategy ### Data Archival Rules ### Backup & Recovery ### ⚠️ CRITICAL: Database Connection Security **NEVER hardcode database credentials**: ```typescript // ❌ FORBIDDEN - NEVER DO THIS import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; const client = postgres("postgresql://user:password123@localhost:5432/mydb"); const db = drizzle(client); // ✅ CORRECT - Use environment variables import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; const connectionString = process.env.DATABASE_URL!; if (!connectionString) { throw new Error("DATABASE_URL not set in environment variables"); } const client = postgres(connectionString); const db = drizzle(client); ``` **drizzle.config.ts**: ```typescript import type { Config } from "drizzle-kit"; export default { schema: "./src/schema/schema.ts", out: "./drizzle", driver: "pg", dbCredentials: { // ✅ CORRECT - Use environment variable connectionString: process.env.DATABASE_URL!, }, } satisfies Config; ``` --- **Last Updated**: 2026-02-12 **Version**: 1.0.3