Data Model
The database schema is defined in packages/backend/convex/schema.ts. Convex uses a document-oriented model — each table stores JSON documents with validated shapes.
The database schema is defined in packages/backend/convex/schema.ts. Convex
uses a document-oriented model — each table stores JSON documents with
validated shapes.
Tables
users
The application-level user record. Created automatically when a Better Auth
user signs up (via the onCreate trigger in auth.ts).
| Field | Type | Description |
|---|---|---|
authUserId | string | Foreign key to the Better Auth user |
name | string? | Display name |
email | string? | Email address |
username | string? | Chosen username (set during onboarding) |
image | string? | Avatar URL |
imageId | Id<"_storage">? | Convex storage ID for uploaded avatar |
personalOrganizationId | string? | The user's default personal workspace |
activeOrganizationId | string? | Currently selected workspace |
Indexes: email, authUserId
organization_profiles
App-owned metadata for each Better Auth organization. Tracks lifecycle status independently of the auth layer.
| Field | Type | Description |
|---|---|---|
organizationId | string | Better Auth organization ID |
ownerUserId | Id<"users"> | Creating user |
isPersonal | boolean | true for the auto-created personal workspace |
status | "active" | "suspended" | "deleted" | Lifecycle status |
createdAt | number | Timestamp |
updatedAt | number | Timestamp |
Indexes: by_organizationId, by_ownerUserId_and_isPersonal, by_status
billing_customers
Maps an organization to a billing provider customer.
| Field | Type | Description |
|---|---|---|
organizationId | string | Workspace identifier |
provider | "polar" | "stripe" | "lemon_squeezy" | Billing provider |
providerCustomerId | string | Provider's customer ID |
email | string? | Billing email |
Indexes: by_organizationId_and_provider, by_provider_and_providerCustomerId
billing_subscriptions
Tracks active and historical subscriptions per organization.
| Field | Type | Description |
|---|---|---|
organizationId | string | Workspace |
provider | string | Billing provider |
providerSubscriptionId | string | Provider's subscription ID |
planKey | string | Maps to a billingPlanCatalog entry |
status | string | active, trialing, past_due, canceled, etc. |
currentPeriodStart/End | number | null | Billing cycle timestamps |
cancelAtPeriodEnd | boolean | Will cancel at cycle end |
Indexes: by_organizationId_and_provider, by_provider_and_providerSubscriptionId,
by_provider_and_providerCustomerId
billing_grants
Individual capability grants tied to billing events. This is the core of the entitlement system — the rest of the app checks grants, not subscriptions.
| Field | Type | Description |
|---|---|---|
organizationId | string | Workspace |
capabilityKey | CapabilityKey | e.g. "feature.pro", "billing.portal" |
source | string | Event source identifier (e.g. "stripe:subscription:sub_123") |
sourceType | "subscription" | "one_time" | "manual" | How the grant was created |
provider | string | "polar", "stripe", "lemon_squeezy", or "manual" |
planKey | string | Associated billing plan |
expiresAt | number | null | null for lifetime grants |
revokedAt | number | null | Set when revoked |
Indexes: by_organizationId, by_organizationId_and_capabilityKey,
by_source_and_capabilityKey
billing_event_sources
Idempotency tracking for billing webhook events.
| Field | Type | Description |
|---|---|---|
source | string | Event source identifier |
lastEventId | string | Last processed webhook event ID |
lastEventTimestamp | number | Timestamp of last processed event |
workspace_records
Removable demo table for organization-scoped data. See Removing the Workspace Demo.
| Field | Type | Description |
|---|---|---|
organizationId | string | Scoping workspace |
title | string | Record title |
status | "open" | "in_progress" | "done" | Record status |
createdByUserId | Id<"users"> | Creator |
security_verification_challenges
Short-lived step-up verification challenges for sensitive actions. Stores only a salted, peppered hash of the verification code — never the raw code.
| Field | Type | Description |
|---|---|---|
userId | Id<"users"> | User being verified |
action | SensitiveActionId | Action being protected |
method | VerificationMethod | "emailCode", "password", "totp", etc. |
codeHash | string | Salted+peppered hash |
expiresAt | number | TTL timestamp |
attemptCount | number | Failed attempts counter |
security_verification_grants
Records that a user recently passed step-up verification for a specific action.
| Field | Type | Description |
|---|---|---|
userId | Id<"users"> | Verified user |
action | SensitiveActionId | Verified action |
expiresAt | number | Grant validity window |
usedAt | number? | Set when consumed (single-use grants) |
Better Auth Tables
Better Auth manages its own tables for user, session, account,
organization, member, invitation, verification, and twoFactor.
These are accessed through the Better Auth API, not queried directly from
app code.
Relationships
users ──┬── organization_profiles (ownerUserId)
├── workspace_records (createdByUserId)
├── security_verification_challenges (userId)
└── security_verification_grants (userId)
organization_profiles ──── billing_customers (organizationId)
├── billing_subscriptions (organizationId)
├── billing_grants (organizationId)
└── workspace_records (organizationId)Next Reads
- Security Model — how permissions and step-up verification work.
- Billing Overview — the billing system.
- Multi-Tenancy Overview — workspaces and organizations.
Convex Backend
The Convex backend lives at packages/backend/convex. It contains all server- side logic: database schema, queries, mutations, actions, authentication, billing, permissions, email, webhooks, and cron jobs.
Server Actions and Functions
All backend logic runs as Convex functions. This page explains the function types, patterns, and conventions used throughout the starter.