Convex App
Database Schema
Convex database tables, fields, and indexes.
The database schema is defined in apps/convex/schema.ts using Convex's schema API. It serves as the single source of truth for your data model — Convex generates TypeScript types from it automatically.
Tables
users
Stores application users synced from Clerk.
| Field | Type | Description |
|---|---|---|
externalId | string | Clerk user ID (e.g., user_2abc123) |
Indexes:
by_external_idon["externalId"]— Fast lookup by Clerk user ID
webhooks
Stores Clerk webhook events for audit and processing.
| Field | Type | Description |
|---|---|---|
externalId | string | Clerk user ID from the event |
type | "user.created" | "user.deleted" | Event type (discriminated union) |
payload | any | Full Clerk webhook payload |
Indexes:
by_external_idon["externalId"]— Fast lookup by Clerk user ID
Schema Definition
// apps/convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
externalId: v.string(),
}).index("by_external_id", ["externalId"]),
webhooks: defineTable({
externalId: v.string(),
type: v.union(v.literal('user.created'), v.literal('user.deleted')),
payload: v.any(),
}).index("by_external_id", ["externalId"]),
});Design Notes
- The
userstable is intentionally minimal — Clerk is the source of truth for user profile data (name, email, avatar). We only store theexternalIdto create a Convex-side reference for relationships and access control. - The
webhookstable usesv.any()forpayloadbecause Clerk webhook payloads vary by event type. Validation happens at the application layer via Zod schemas inclerk/webhookSchemas.ts. - Both tables index on
externalIdfor efficient lookups during webhook processing.