Skip to content
Whop SaaS Starter
Guides

Database

Schema, queries, and extending the data model

The template uses Prisma 7 with PostgreSQL via the native pg driver adapter. The schema lives at db/schema.prisma and the client singleton at db/index.ts.

Schema

User

Stores authenticated users. Created on first OAuth sign-in, updated by webhooks.

ColumnTypeDescription
idString (cuid)Primary key
whopUserIdString (unique)Whop's user ID from OAuth
emailString?From Whop profile
nameString?From Whop profile
profileImageUrlString?Avatar URL from Whop CDN
planStringCurrent plan key (default: "free")
whopMembershipIdString?Active Whop membership ID
cancelAtPeriodEndBooleanPending cancellation flag
isAdminBooleanFirst user to sign in becomes admin
createdAtDateTimeAccount creation (timestamptz)
updatedAtDateTimeLast update (timestamptz)

Indexes: whopUserId (unique), email, plan

SystemConfig

Key-value store for app configuration (Whop credentials, accent color, integration settings).

ColumnTypeDescription
keyStringConfig key (primary key)
valueStringConfig value

How Plan Updates Work

User subscribes on Whop
  → Whop sends membership_activated webhook
  → Webhook handler calls activateMembership()
  → prisma.user.upsert() sets plan + whopMembershipId
  → Next request: getSession() reads fresh plan from DB
  → User sees upgraded features

The JWT cookie carries identity, but plan is always read from the database in getSession(). This means webhook-driven plan changes are reflected immediately without requiring the user to re-authenticate.

Common Queries

Read user data (select only what you need)

import { prisma } from "@/db";

const user = await prisma.user.findUnique({
  where: { id: userId },
  select: { plan: true, cancelAtPeriodEnd: true },
});

Upsert (create or update)

Used in OAuth callback and webhook handlers:

await prisma.user.upsert({
  where: { whopUserId },
  update: { plan, whopMembershipId: membershipId },
  create: { whopUserId, plan, whopMembershipId: membershipId },
});

Bulk update by Whop user ID

Webhook handlers use updateMany for safety:

await prisma.user.updateMany({
  where: { whopUserId },
  data: { plan: "free", whopMembershipId: null },
});

Read config

import { getConfig, setConfig } from "@/lib/config";

const value = await getConfig("accent_color"); // string | null
await setConfig("accent_color", "#5b4cff");

Adding a New Model

  1. Add the model to db/schema.prisma:
model Project {
  id        String   @id @default(cuid())
  name      String
  userId    String
  user      User     @relation(fields: [userId], references: [id])
  createdAt DateTime @default(now()) @db.Timestamptz(3)
  updatedAt DateTime @updatedAt @db.Timestamptz(3)

  @@index([userId])
}
  1. Add the relation to the User model:
model User {
  // ... existing fields
  projects Project[]
}
  1. Push the schema:
pnpm db:push      # Development (no migration files)
pnpm db:migrate   # Production (creates migration files)

Best Practices

  • Always use select to fetch only the fields you need
  • Use Promise.all for independent queries (avoid waterfalls)
  • Use @db.Timestamptz(3) for all DateTime fields (timezone-safe)
  • Index columns used in WHERE clauses, JOINs, and foreign keys
  • Use upsert for create-or-update patterns (maps to ON CONFLICT)
  • Use updateMany in webhook handlers (handles edge cases)

Push vs Migrate

CommandUse case
pnpm db:pushDevelopment — applies schema directly, no migration files
pnpm db:migrateProduction — creates versioned migration files for reproducibility

The build script (pnpm build) runs db:push automatically if DATABASE_URL is set.

Database Providers

Works with any PostgreSQL provider. SSL and pool sizing are auto-configured.

  • Neon — use the pooled connection string (hostname contains -pooler)
  • Supabase — use Session mode connection string; reduce pool size to 3 on free tier
  • Prisma Postgres — use direct TCP connection string with sslmode=require
  • Nile — zero-config via Vercel Marketplace; NILEDB_POSTGRES_URL is detected automatically
  • Local — no SSL needed; just postgresql://postgres:postgres@localhost:5432/mydb

See Deployment for provider-specific setup.

Prisma Studio

Browse and edit your data visually:

pnpm db:studio

Opens a web UI at http://localhost:5555.

On this page