Skip to main content

Overview

The Hono app uses PostgreSQL as its database with Drizzle ORM for type-safe database operations. The schema is defined in src/db/schema.ts.

Common Fields

File: src/db/schema.ts:14-18 All tables with timestamps include these common fields:
const timestamps = {
  createdAt: timestamp().defaultNow().notNull(),
  updatedAt: timestamp().$onUpdate(() => new Date()),
  deletedAt: timestamp(),
};
Fields:
  • createdAt - Automatically set to current timestamp on creation
  • updatedAt - Automatically updated to current timestamp on modification
  • deletedAt - Nullable timestamp for soft deletes

Authentication Tables

These tables are used by Better Auth for authentication and session management.

user

File: src/db/schema.ts:22-29 Stores user account information.
export const userTable = pgTable("user", {
  id: text().primaryKey(),
  name: text().notNull(),
  email: text().notNull().unique(),
  emailVerified: boolean().default(false).notNull(),
  image: text(),
  ...timestamps,
});
Columns:
ColumnTypeConstraintsDescription
idtextPRIMARY KEYUnique user identifier
nametextNOT NULLUser’s display name
emailtextNOT NULL, UNIQUEUser’s email address
emailVerifiedbooleanNOT NULL, DEFAULT falseEmail verification status
imagetextnullableURL to user’s profile image
createdAttimestampNOT NULLAccount creation timestamp
updatedAttimestampnullableLast update timestamp
deletedAttimestampnullableSoft delete timestamp
Type Export:
export const selectUserTableSchema = createSelectSchema(userTable);
export type UserTable = z.infer<typeof selectUserTableSchema>;
Relationships:
  • One-to-many with session table
  • One-to-many with account table

session

File: src/db/schema.ts:33-43 Stores active user sessions.
export const sessionTable = pgTable("session", {
  id: text().primaryKey(),
  expiresAt: timestamp().notNull(),
  token: text().notNull().unique(),
  ipAddress: text(),
  userAgent: text(),
  userId: text()
    .notNull()
    .references(() => userTable.id, { onDelete: "cascade" }),
  ...timestamps,
});
Columns:
ColumnTypeConstraintsDescription
idtextPRIMARY KEYUnique session identifier
expiresAttimestampNOT NULLSession expiration time
tokentextNOT NULL, UNIQUESession token for authentication
ipAddresstextnullableIP address of the session
userAgenttextnullableBrowser/client user agent
userIdtextNOT NULL, FOREIGN KEYReference to user
createdAttimestampNOT NULLSession creation timestamp
updatedAttimestampnullableLast update timestamp
deletedAttimestampnullableSoft delete timestamp
Type Export:
export const selectSessionTableSchema = createSelectSchema(sessionTable);
export type SessionTable = z.infer<typeof selectSessionTableSchema>;
Foreign Keys:
  • userId references user.id with CASCADE delete

account

File: src/db/schema.ts:47-62 Stores OAuth provider accounts and credentials linked to users.
export const accountTable = pgTable("account", {
  id: text().primaryKey(),
  accountId: text().notNull(),
  providerId: text().notNull(),
  userId: text()
    .notNull()
    .references(() => userTable.id, { onDelete: "cascade" }),
  accessToken: text(),
  refreshToken: text(),
  idToken: text(),
  accessTokenExpiresAt: timestamp(),
  refreshTokenExpiresAt: timestamp(),
  scope: text(),
  password: text(),
  ...timestamps,
});
Columns:
ColumnTypeConstraintsDescription
idtextPRIMARY KEYUnique account identifier
accountIdtextNOT NULLProvider-specific account ID
providerIdtextNOT NULLOAuth provider identifier
userIdtextNOT NULL, FOREIGN KEYReference to user
accessTokentextnullableOAuth access token
refreshTokentextnullableOAuth refresh token
idTokentextnullableOAuth ID token
accessTokenExpiresAttimestampnullableAccess token expiration
refreshTokenExpiresAttimestampnullableRefresh token expiration
scopetextnullableOAuth scope
passwordtextnullableHashed password for email/password auth
createdAttimestampNOT NULLAccount creation timestamp
updatedAttimestampnullableLast update timestamp
deletedAttimestampnullableSoft delete timestamp
Type Export:
export const selectAccountTableSchema = createSelectSchema(accountTable);
export type AccountTable = z.infer<typeof selectAccountTableSchema>;
Foreign Keys:
  • userId references user.id with CASCADE delete
Use Cases:
  • OAuth provider accounts (Google, GitHub, etc.)
  • Email/password authentication (password field)
  • Token management for API access

verification

File: src/db/schema.ts:66-72 Stores verification tokens for email verification, password resets, etc.
export const verificationTable = pgTable("verification", {
  id: text().primaryKey(),
  identifier: text().notNull(),
  value: text().notNull(),
  expiresAt: timestamp().notNull(),
  ...timestamps,
});
Columns:
ColumnTypeConstraintsDescription
idtextPRIMARY KEYUnique verification identifier
identifiertextNOT NULLEmail or identifier to verify
valuetextNOT NULLVerification token/code
expiresAttimestampNOT NULLToken expiration time
createdAttimestampNOT NULLToken creation timestamp
updatedAttimestampnullableLast update timestamp
deletedAttimestampnullableSoft delete timestamp
Type Export:
export const selectVerificationTableSchema = createSelectSchema(verificationTable);
export type VerificationTable = z.infer<typeof selectVerificationTableSchema>;
Use Cases:
  • Email verification tokens
  • Password reset tokens
  • Magic link authentication
  • Two-factor authentication codes

Rate Limiting Table

rate_limit

File: src/db/schema.ts:79-84 Stores rate limiting data for request throttling.
export const rateLimitTable = pgTable("rate_limit", {
  id: uuid("id").defaultRandom().primaryKey(),
  key: text("key").notNull().unique(),
  count: integer("count").default(0).notNull(),
  lastRequest: bigint("last_request", { mode: "number" }).notNull(),
});
Columns:
ColumnTypeConstraintsDescription
iduuidPRIMARY KEYAuto-generated UUID
keytextNOT NULL, UNIQUERate limit key (session ID or IP)
countintegerNOT NULL, DEFAULT 0Request count in current window
lastRequestbigintNOT NULLTimestamp of last request (milliseconds)
Key Format:
  • Authenticated users: session:<session_id>
  • Anonymous users: ip:<ip_address>
Usage:
  • Tracks request counts per client within time windows
  • Used by DbStore in rate limiting middleware
  • Automatically managed by rate limiter (increment/decrement/reset)
  • Expired records are automatically deleted on access
No Timestamps: This table intentionally omits timestamp fields as it manages its own time-based data via lastRequest.

Schema Exports

File: src/db/schema.ts:30-31,44-45,63-64,73-75 Each table exports both a Zod schema and TypeScript type:
// User
export const selectUserTableSchema = createSelectSchema(userTable);
export type UserTable = z.infer<typeof selectUserTableSchema>;

// Session
export const selectSessionTableSchema = createSelectSchema(sessionTable);
export type SessionTable = z.infer<typeof selectSessionTableSchema>;

// Account
export const selectAccountTableSchema = createSelectSchema(accountTable);
export type AccountTable = z.infer<typeof selectAccountTableSchema>;

// Verification
export const selectVerificationTableSchema = createSelectSchema(verificationTable);
export type VerificationTable = z.infer<typeof selectVerificationTableSchema>;
Benefits:
  • Runtime validation with Zod schemas
  • Type safety with TypeScript types
  • Auto-generated from Drizzle table definitions
  • Consistent with database schema

Database Connection

File: src/db/index.ts The database connection is exported from the db module:
import { db } from '@/db/index.js';
Usage in Better Auth: File: src/auth/libs/index.ts:16-24
database: drizzleAdapter(db, {
  provider: "pg",
  schema: {
    user: schema.userTable,
    session: schema.sessionTable,
    account: schema.accountTable,
    verification: schema.verificationTable,
    rate_limit: schema.rateLimitTable,
  },
})

Database Operations

Common Drizzle ORM operations used throughout the codebase:

Select

import { db } from '@/db/index.js';
import { userTable } from '@/db/schema.js';
import { eq } from 'drizzle-orm';

const user = await db
  .select()
  .from(userTable)
  .where(eq(userTable.id, userId))
  .limit(1);

Insert

const newUser = await db
  .insert(userTable)
  .values({
    id: 'user_123',
    name: 'John Doe',
    email: '[email protected]',
    emailVerified: false,
  })
  .returning();

Update

const updated = await db
  .update(userTable)
  .set({ emailVerified: true })
  .where(eq(userTable.id, userId))
  .returning();

Delete

await db
  .delete(userTable)
  .where(eq(userTable.id, userId));

Soft Delete

await db
  .update(userTable)
  .set({ deletedAt: new Date() })
  .where(eq(userTable.id, userId));

Build docs developers (and LLMs) love