Database schema
Drizzle ORM with snake_case columns in SQL, camelCase in TypeScript.
Files
| File | Source | Notes |
|---|---|---|
src/db/schema/auth.ts | CLI-generated | Regenerate via pnpm db:auth-schema after a better-auth or plugin upgrade. Do not hand-edit. |
src/db/schema/business.ts | Hand-written | Every marketplace table that isn't an auth table |
src/db/schema/index.ts | Re-exports both | Used by drizzle(pool, { schema }) and drizzleAdapter(db, { schema }) |
Auth tables (better-auth)
Generated by @better-auth/cli generate. The set includes:
user,session,account,verificationorganization,organizationMember,organizationInvitationoauthClient,oauthAccessToken,oauthAuthorizationCode,oauthConsentjwks(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
| Table | Key columns | Constraints |
|---|---|---|
tags | id, name, slug (unique) | |
agents | id, slug (unique), status, oauthClientId (unique), redirectUris[], createdBy | |
agent_tags | (agentId, tagId) PK | |
agent_capabilities | id, agentId, icon, title, description, sortOrder | |
agent_steps | id, agentId, title, description, sortOrder | |
agent_changelog | id, agentId, version, summary, publishedAt |
Pricing + quota tables
| Table | Key columns | Constraints |
|---|---|---|
agent_pricing_tiers | id, agentId, tierName, isFree, price, currency, billingInterval, stripeProductId, stripePriceId, trialDays, recommended | |
agent_metric_definitions | id, agentId, slug (UQ with agentId), displayName, unit, kind, sortOrder | slug and kind immutable in application logic |
tier_metric_quotas | id, tierId, metricId, quotaLimit | UQ (tierId, metricId). NULL=unlimited, 0=deny, positive=cap |
Subscription + usage tables
| Table | Key columns | Constraints |
|---|---|---|
user_agent_subscriptions | id, userId, agentId, tierId, status, stripeCustomerId, stripeSubscriptionId (unique nullable), currentPeriodEnd, cancelAtPeriodEnd, canceledAt | Partial UQ (userId, agentId) WHERE status IN ('active','trialing') |
subscription_usage | id, subscriptionId, metricId, periodStart, periodEnd, used, updatedAt | |
subscription_addons | id, subscriptionId, metricId, amount, scope (one_cycle|permanent), source (admin_grant|purchased), revokedAt | |
usage_ledger | id, subscriptionId, metricId, eventType, delta, clientReqId (unique), metadata, createdAt | Append-only; UNIQUE on clientReqId for idempotency |
stripe_events | id, stripeEventId (unique), type, livemode, payload (JSONB), processedAt, errorMessage, receivedAt | Idempotency for inbound webhooks |
Social / engagement tables
| Table | Key columns | Constraints |
|---|---|---|
reviews | id, agentId, userId, rating, title, body | UQ (agentId, userId), CHECK (rating BETWEEN 1 AND 5) |
agent_installs | id, agentId, userId, installedAt | UQ (agentId, userId) |
agent_waitlist | id, agentId, email | UQ (agentId, email) |
agent_metrics_daily | (agentId, day) PK, activeUsers, messages, apiCalls, avgLatencyMs, uptimeBp, csatBp |
Observability tables
| Table | Key columns |
|---|---|
audit_logs | id, userId, orgId, action, resource, resourceId, result, errorCode, metadata, timestamp |
interactions | id, type, userId, userEmail, agentId, agentName, metadata, notifiedAt, notifyError, createdAt |
interaction_recipients | id, email (unique), createdAt, updatedAt |
Cross-schema rules
- All FKs into auth tables (
auth.user.id) are declaredON DELETE CASCADEfor owned data (reviews, installs, subscriptions) andON DELETE SET NULLfor tracking data (createdBy). - All
agentIdFKs areON 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
timestamptzwithdefault now()andnotNull()unless documented otherwise. - IDs are UUID v4 generated client-side via
crypto.randomUUID()(orgen_random_uuid()in Postgres). - Booleans are
boolean notNull default falseunless they carry semantic meaning when null.
Where to inspect
pnpm db:studio # Drizzle Studio
psql $DATABASE_URL # raw