pealdevDOCS
Back to peal.dev

AUTH STARTER · DATABASE (DRIZZLE + NEON)

Database (Drizzle + Neon)

Drizzle ORM over Neon Postgres HTTP. Type-safe queries, real migrations, no surprises in prod.

We use Drizzle because the queries are typed end-to-end and the schema lives next to the code that uses it. Neon HTTP because it's fast, serverless-friendly, and works with edge runtimes without a connection pool.

Where the schema lives

src/db/schema/auth.ts. One file, all auth-related tables. Add your own tables here or in sibling files.

TypeScript
export const user = pgTable("user", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  emailVerified: boolean("email_verified").notNull().default(false),
  image: text("image"),
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
  // ── plugin fields ──
  twoFactorEnabled: boolean("two_factor_enabled"),
  role: text("role"),
  banned: boolean("banned"),
  banReason: text("ban_reason"),
  banExpires: timestamp("ban_expires", { withTimezone: true }),
  phoneNumber: text("phone_number").unique(),
  phoneNumberVerified: boolean("phone_number_verified"),
});

Table names MUST be singular

user, not users. session, not sessions. Better Auth queries by exact table name. Pluralize and nothing works — it'll silently look for tables that don't exist.

Adding your own user fields

Need to track signup source, plan, onboarding step? Just add columns to the user table. Better Auth will pass them through on session.user as long as you also surface them in your SessionUser type.

TypeScript
// src/db/schema/auth.ts — add the column
plan: text("plan").notNull().default("free"),

// src/lib/types.ts — surface it
export interface SessionUser {
  // ...existing fields
  plan?: string;
}

Dev workflow

Terminal
bun run db:push      # dev only — push schema changes directly
bun run db:studio    # visual browser at localhost:4983

Production workflow

Terminal
bun run db:generate  # generate SQL migration in ./drizzle
# review the generated SQL
bun run db:migrate   # apply pending migrations

Never run db:push against production

db:push is destructive — it can drop columns and tables to match your schema. Use generate + migrate so you keep a real, reviewable history of changes. The day db:push deletes a production column is the day you wish you'd read this callout.

Never hand-edit a generated migration

If a migration is wrong, generate a new one. Editing the file after it's been applied to any environment creates drift you'll spend a weekend untangling.