Skip to main content

Database schema

Drizzle ORM with snake_case columns in SQL, camelCase in TypeScript.

Files

FileSourceNotes
src/db/schema/auth.tsCLI-generatedRegenerate via pnpm db:auth-schema after a better-auth or plugin upgrade. Do not hand-edit.
src/db/schema/business.tsHand-writtenEvery marketplace table that isn't an auth table
src/db/schema/index.tsRe-exports bothUsed by drizzle(pool, { schema }) and drizzleAdapter(db, { schema })

Auth tables (better-auth)

Generated by @better-auth/cli generate. The set includes:

  • user, session, account, verification
  • organization, organizationMember, organizationInvitation
  • oauthClient, oauthAccessToken, oauthAuthorizationCode, oauthConsent
  • jwks (keys)

Schema is owned by better-auth — if you need to add a column, override via the auth config rather than mutating the generated file.

Business tables

Catalog tables

TableKey columnsConstraints
tagsid, name, slug (unique)
agentsid, slug (unique), status, oauthClientId (unique), redirectUris[], createdBy
agent_tags(agentId, tagId) PK
agent_capabilitiesid, agentId, icon, title, description, sortOrder
agent_stepsid, agentId, title, description, sortOrder
agent_changelogid, agentId, version, summary, publishedAt

Pricing + quota tables

TableKey columnsConstraints
agent_pricing_tiersid, agentId, tierName, isFree, price, currency, billingInterval, stripeProductId, stripePriceId, trialDays, recommended
agent_metric_definitionsid, agentId, slug (UQ with agentId), displayName, unit, kind, sortOrderslug and kind immutable in application logic
tier_metric_quotasid, tierId, metricId, quotaLimitUQ (tierId, metricId). NULL=unlimited, 0=deny, positive=cap

Subscription + usage tables

TableKey columnsConstraints
user_agent_subscriptionsid, userId, agentId, tierId, status, stripeCustomerId, stripeSubscriptionId (unique nullable), currentPeriodEnd, cancelAtPeriodEnd, canceledAtPartial UQ (userId, agentId) WHERE status IN ('active','trialing')
subscription_usageid, subscriptionId, metricId, periodStart, periodEnd, used, updatedAt
subscription_addonsid, subscriptionId, metricId, amount, scope (one_cycle|permanent), source (admin_grant|purchased), revokedAt
usage_ledgerid, subscriptionId, metricId, eventType, delta, clientReqId (unique), metadata, createdAtAppend-only; UNIQUE on clientReqId for idempotency
stripe_eventsid, stripeEventId (unique), type, livemode, payload (JSONB), processedAt, errorMessage, receivedAtIdempotency for inbound webhooks

Social / engagement tables

TableKey columnsConstraints
reviewsid, agentId, userId, rating, title, bodyUQ (agentId, userId), CHECK (rating BETWEEN 1 AND 5)
agent_installsid, agentId, userId, installedAtUQ (agentId, userId)
agent_waitlistid, agentId, emailUQ (agentId, email)
agent_metrics_daily(agentId, day) PK, activeUsers, messages, apiCalls, avgLatencyMs, uptimeBp, csatBp

Observability tables

TableKey columns
audit_logsid, userId, orgId, action, resource, resourceId, result, errorCode, metadata, timestamp
interactionsid, type, userId, userEmail, agentId, agentName, metadata, notifiedAt, notifyError, createdAt
interaction_recipientsid, email (unique), createdAt, updatedAt

Cross-schema rules

  • All FKs into auth tables (auth.user.id) are declared ON DELETE CASCADE for owned data (reviews, installs, subscriptions) and ON DELETE SET NULL for tracking data (createdBy).
  • All agentId FKs are ON DELETE CASCADE — deleting an agent (rare; usually you flip status) cleans up its children.

Naming conventions

  • TS field names are camelCase, SQL column names snake_case. Drizzle's column constructor handles the mapping: userId: varchar('user_id', ...).
  • Timestamps are timestamptz with default now() and notNull() unless documented otherwise.
  • IDs are UUID v4 generated client-side via crypto.randomUUID() (or gen_random_uuid() in Postgres).
  • Booleans are boolean notNull default false unless they carry semantic meaning when null.

Where to inspect

pnpm db:studio          # Drizzle Studio
psql $DATABASE_URL # raw