Add a Database Table
1. Define the table in schema
Section titled “1. Define the table in schema”Edit shared/src/db/schema.ts. Follow conventions:
export const widgets = pgTable( 'widgets', { id: uuid('id').primaryKey().defaultRandom(), user_id: uuid('user_id') .notNull() .references(() => users.id, { onDelete: 'cascade' }), name: text('name').notNull(), status: text('status').notNull().default('active'), created_at: timestamp('created_at', { mode: 'string', withTimezone: true }) .defaultNow() .notNull(), updated_at: timestamp('updated_at', { mode: 'string', withTimezone: true }) .defaultNow() .notNull() .$onUpdate(() => sql`now()`), }, (table) => [ index('idx_widgets_user_id').on(table.user_id), ]);
export type Widget = typeof widgets.$inferSelect;export type NewWidget = typeof widgets.$inferInsert;Conventions:
snake_casecolumn namesuuidprimary keys withdefaultRandom()mode: 'string'on all timestamps (returns ISO strings, not Date objects)created_at+updated_aton every table- Foreign keys with explicit
onDeletebehavior - Indexes on columns used in WHERE clauses
2. Export from shared
Section titled “2. Export from shared”Add the table and types to shared/src/index.ts:
export { widgets, type Widget, type NewWidget } from './db/schema';3. Generate the migration
Section titled “3. Generate the migration”bun run db:generate4. Review the SQL
Section titled “4. Review the SQL”Open the generated file in server/drizzle/. Check:
- Table and column names are correct
- Foreign key references point to the right table
- No unexpected DROP statements
- NOT NULL columns have DEFAULT values (or the table is new)
5. Apply
Section titled “5. Apply”bun run db:migrate6. Verify types
Section titled “6. Verify types”bun run typecheckCustom migrations
Section titled “Custom migrations”For things Drizzle can’t generate — RLS policies, data backfills, enum types:
cd server && bunx drizzle-kit generate --customWrite the SQL yourself in the generated empty file.