KitRocket

Database Schema

Complete database schema reference for all tables in the KitRocket boilerplate.

KitRocket uses Drizzle ORM with Supabase Postgres. All schema files live in src/db/schema/.

Tables overview

TableFilePurpose
userssrc/db/schema/users.tsUser accounts
sessionssrc/db/schema/sessions.tsActive sessions
accountssrc/db/schema/accounts.tsOAuth provider links
subscriptionssrc/db/schema/subscriptions.tsPayment subscriptions
usagesrc/db/schema/usage.tsAI token tracking (Pro)

users

Stores user accounts. Created on registration (email/password or OAuth).

import { pgTable, text, timestamp } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: text("id").primaryKey(),
  email: text("email").notNull().unique(),
  name: text("name").notNull(),
  emailVerified: timestamp("email_verified"),
  avatar: text("avatar"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
ColumnTypeNullableDescription
idtextNoPrimary key (nanoid or uuid)
emailtextNoUnique email address
nametextNoDisplay name
email_verifiedtimestampYesWhen email was verified
avatartextYesAvatar image URL
created_attimestampNoAccount creation time
updated_attimestampNoLast profile update

sessions

Stores active user sessions. Managed automatically by Better Auth.

import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { users } from "./users";

export const sessions = pgTable("sessions", {
  id: text("id").primaryKey(),
  userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
  token: text("token").notNull().unique(),
  expiresAt: timestamp("expires_at").notNull(),
  ipAddress: text("ip_address"),
  userAgent: text("user_agent"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});
ColumnTypeNullableDescription
idtextNoPrimary key
user_idtextNoForeign key to users
tokentextNoUnique session token
expires_attimestampNoSession expiration time
ip_addresstextYesClient IP address
user_agenttextYesClient user agent string
created_attimestampNoSession creation time

Sessions are cleaned up by Better Auth when they expire.

accounts

Links OAuth providers to user accounts. A single user can have multiple accounts (e.g., Google + GitHub + email/password).

import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { users } from "./users";

export const accounts = pgTable("accounts", {
  id: text("id").primaryKey(),
  userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
  provider: text("provider").notNull(),
  providerAccountId: text("provider_account_id").notNull(),
  accessToken: text("access_token"),
  refreshToken: text("refresh_token"),
  expiresAt: timestamp("expires_at"),
  password: text("password"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});
ColumnTypeNullableDescription
idtextNoPrimary key
user_idtextNoForeign key to users
providertextNoProvider name: "google", "github", "credential"
provider_account_idtextNoID from the OAuth provider
access_tokentextYesOAuth access token
refresh_tokentextYesOAuth refresh token
expires_attimestampYesToken expiration time
passwordtextYesHashed password (email/password accounts only)
created_attimestampNoAccount link creation time

subscriptions

Tracks payment subscriptions. Updated via DodoPayments webhooks.

import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { users } from "./users";

export const subscriptions = pgTable("subscriptions", {
  id: text("id").primaryKey(),
  userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
  plan: text("plan").notNull(),
  status: text("status").notNull().default("active"),
  dodoCustomerId: text("dodo_customer_id"),
  dodoSubscriptionId: text("dodo_subscription_id"),
  currentPeriodEnd: timestamp("current_period_end"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
ColumnTypeNullableDescription
idtextNoPrimary key
user_idtextNoForeign key to users
plantextNoPlan name: "starter" or "pro"
statustextNoStatus: "active", "cancelled", "past_due", "trialing"
dodo_customer_idtextYesDodoPayments customer ID
dodo_subscription_idtextYesDodoPayments subscription ID
current_period_endtimestampYesEnd of current billing period
created_attimestampNoSubscription creation time
updated_attimestampNoLast status change

usage (Pro)

Tracks AI token usage per user. Used for rate limiting and usage-based billing.

import { pgTable, text, integer, timestamp } from "drizzle-orm/pg-core";
import { users } from "./users";

export const usage = pgTable("usage", {
  id: text("id").primaryKey(),
  userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
  tokensUsed: integer("tokens_used").notNull(),
  model: text("model").notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});
ColumnTypeNullableDescription
idtextNoPrimary key
user_idtextNoForeign key to users
tokens_usedintegerNoTotal tokens (input + output)
modeltextNoAI model used (e.g., "claude-sonnet-4-20250514")
created_attimestampNoWhen the usage occurred

Entity relationships

users
  |-- sessions (one-to-many)      A user can have multiple active sessions
  |-- accounts (one-to-many)      A user can link multiple OAuth providers
  |-- subscriptions (one-to-one)  A user has one active subscription (or none)
  |-- usage (one-to-many)         A user has many AI usage records (Pro)

All foreign keys use ON DELETE CASCADE — deleting a user removes all associated records.

Common queries

Get user with subscription

const result = await db
  .select()
  .from(users)
  .leftJoin(subscriptions, eq(users.id, subscriptions.userId))
  .where(eq(users.id, userId));

Get monthly token usage

const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

const result = await db
  .select({ total: sql<number>`sum(${usage.tokensUsed})` })
  .from(usage)
  .where(
    and(
      eq(usage.userId, userId),
      gte(usage.createdAt, thirtyDaysAgo)
    )
  );

Database commands

# Push schema to database (development)
pnpm db:push

# Generate a migration file
pnpm db:generate

# Apply pending migrations
pnpm db:migrate

# Open Drizzle Studio (visual database browser)
pnpm db:studio

Drizzle Studio opens a web UI at https://local.drizzle.studio where you can browse tables, run queries, and edit records.

On this page