Database Schema Overview

Understand the database schema structure, tables, relationships, and multi-tenancy model.

The SaaS Kit uses a multi-tenant database schema with organization-based isolation. All tables are defined using Drizzle ORM.

Core Schema

The core schema is defined in the packages/database/src/schema/core.ts file, where the kit defines the core tables and relationships.

Extending the Schema

You can extend the schema by adding your own Drizzle ORM definitions to the packages/database/src/schema/schema.ts file:

packages/database/src/schema/schema.ts

export * from './core';
// Add your own tables here

Authentication Tables

The authentication tables contain the tables for the authentication system as defined in Better Auth.

Users

The table user stores user accounts and profile data.

ColumnTypeDescription
idtextPrimary key
nametextDisplay name
emailtextUnique email address
createdAttimestampUser creation date
updatedAttimestampUser last update date
banReasontextReason for ban
banExpirestimestampBan expiration date
emailVerifiedbooleanEmail verification status
imagetextProfile picture URL
twoFactorEnabledboolean2FA enabled
roletextAdmin role (for admin features)
bannedbooleanBan status
stripeCustomerIdtextStripe customer ID

Sessions

The table session stores authentication sessions.

ColumnTypeDescription
idtextPrimary key
tokentextUnique session token
userIdtextFK to users (cascade delete)
expiresAttimestampSession expiration
createdAttimestampSession creation date
updatedAttimestampSession last update date
ipAddresstextClient IP address
userAgenttextBrowser user agent
impersonatedBytextAdmin user ID if impersonating
activeOrganizationIdtextCurrent organization context

Accounts

The table account stores OAuth provider accounts and password credentials.

ColumnTypeDescription
idtextPrimary key
providerIdtextProvider name (google, github, credential)
accountIdtextProvider's account ID
userIdtextFK to users (cascade delete)
accessTokentextOAuth access token
refreshTokentextOAuth refresh token
passwordtextHashed password (for credential auth)
idTokentextOAuth ID token
accessTokenExpiresAttimestampOAuth access token expiration
refreshTokenExpiresAttimestampOAuth refresh token expiration
scopetextOAuth scope
createdAttimestampAccount creation date
updatedAttimestampAccount last update date

Verifications

The table verification stores email verification and password reset tokens.

ColumnTypeDescription
idtextPrimary key
identifiertextEmail or phone number
valuetextVerification code/token
expiresAttimestampToken expiration
createdAttimestampVerification creation date
updatedAttimestampVerification last update date

Two Factors

The table two_factor stores TOTP 2FA secrets and backup codes.

ColumnTypeDescription
idtextPrimary key
secrettextTOTP secret
backupCodestextBackup codes
userIdtextFK to users (cascade delete)

Rate Limit

The table rate_limit stores rate limiting state for the sliding window algorithm. Used by Better Auth and the custom rate limit service.

ColumnTypeDescription
idtextPrimary key (equals the rate limit key)
keytextRate limit key (e.g., api:upload:user123)
countintegerRequest count in current window
lastRequestbigintTimestamp of last request (ms)

See: Rate Limit Service

Organization Tables

Organizations

The table organization stores team/workspace containers.

ColumnTypeDescription
idtextPrimary key
nametextOrganization name
slugtextUnique URL-friendly slug
logotextLogo URL
metadatatextJSON metadata
createdAttimestampOrganization creation date
stripeCustomerIdtextStripe customer ID

Members

The table member links users to organizations with roles.

ColumnTypeDescription
idtextPrimary key
organizationIdtextFK to organizations (cascade delete)
userIdtextFK to users (cascade delete)
roletextRole (owner, admin, member)
createdAttimestampMember creation date

Invitations

The table invitation tracks pending organization invitations.

ColumnTypeDescription
idtextPrimary key
organizationIdtextFK to organizations (cascade delete)
emailtextInvitee's email
roletextRole to assign when accepted
statustextpending, accepted, or declined
expiresAttimestampInvitation expiration
inviterIdtextFK to users (cascade delete)
createdAttimestampInvitation creation date

Organization Roles

The table organization_role stores custom RBAC permission definitions per organization.

ColumnTypeDescription
idtextPrimary key
organizationIdtextFK to organizations (cascade delete)
roletextRole name
permissiontextPermission name
createdAttimestampOrganization role creation date
updatedAttimestampOrganization role last update date

Billing Tables

Subscriptions

The table subscription stores billing and subscription data.

ColumnTypeDescription
idtextPrimary key
plantextPlan identifier
referenceIdtextOrganization/user reference
stripeCustomerIdtextStripe customer ID
stripeSubscriptionIdtextStripe subscription ID
statustextactive, trialing, canceled, etc.
periodStarttimestampCurrent billing period start
periodEndtimestampCurrent billing period end
seatsintegerNumber of seats
trialStarttimestampTrial start date
trialEndtimestampTrial end date
cancelAttimestampDate when sub will be cancelled
canceledAttimestampCanceled at date
endedAttimestampEnded at date
cancelAtPeriodEndbooleanBoolean to indicate if the subscription should be canceled at the end of the current period

Multi-Tenancy Model

All tenant data is isolated by organization. When adding custom tables, always include an organizationId foreign key.

For example, the table project could be defined as follows, where organizationId is a foreign key to the organization table:

export const project = pgTable('project', {
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull()
.references(() => organization.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
});

Note: Always make sure to filter queries by organizationId to ensure data isolation:

// Correct - respects multi-tenancy
const projects = await db.from(project)
.where(eq(project.organizationId, currentOrgId));

Index Suggestions

Core tables already include PK/unique/FK indexes. If query patterns match, consider adding:

TableSuggested indexWhy
accountunique(provider_id, account_id)Fast provider lookup, prevent dup accounts
verificationindex(identifier, value)Fast token lookup by identifier + code
organizationindex(stripe_customer_id)Resolve org from billing webhooks
organization_roleindex(organization_id, role) or unique(organization_id, role, permission)Role permission fetch + dedup
memberunique(organization_id, user_id)Fast membership lookup, prevent dup
invitationindex(organization_id, status) (opt: email, organization_id, status)Pending invites list + accept flow
subscriptionindex(reference_id), index(stripe_customer_id), index(stripe_subscription_id)App + webhook lookups
rate_limitunique(key)Upsert by key, avoid full scan

Next: Migrations →