Model Layer
Data access patterns with the models directory.
YeetCode organizes database operations into a models/ directory inside the Convex backend. Each model file is a namespace of pure functions that take a Convex context (ctx) as their first argument — they're not classes, just grouped functions.
Structure
apps/convex/models/
├── users.ts # User CRUD and auth helpers
└── webhooks.ts # Webhook event storageUsers Model
models/users.ts handles user operations and authentication checks.
ensureUserAuthenticated(ctx)
Verifies the current request is authenticated. Throws if not.
await Users.ensureUserAuthenticated(ctx);getCurrentUserOrThrow(ctx)
Returns the authenticated user's document. Throws if not authenticated or user doesn't exist.
const user = await Users.getCurrentUserOrThrow(ctx);Uses getAuthUserId() from @convex-dev/auth/server to extract the user ID from the validated JWT, then fetches the user document.
getById(ctx, userId)
Fetches a user by their Convex document ID.
const user = await Users.getById(ctx, userId);getByExternalId(ctx, externalId)
Looks up a user by their Clerk external ID using the by_external_id index.
const user = await Users.getByExternalId(ctx, clerkUserId);
// Returns null if not foundcreate(ctx, externalId)
Creates a new user. Returns the existing user's ID if one already exists with that external ID (idempotent).
const userId = await Users.create(ctx, clerkUserId);deleteByExternalId(ctx, externalId)
Deletes a user by their Clerk external ID. Throws if the user doesn't exist.
await Users.deleteByExternalId(ctx, clerkUserId);Webhooks Model
models/webhooks.ts handles webhook event persistence.
getByExternalId(ctx, externalId)
Looks up a webhook event by external ID.
const webhook = await Webhooks.getByExternalId(ctx, clerkUserId);create(ctx, data)
Creates a webhook event record. Validates the payload against Zod schemas before inserting. Idempotent — returns the existing record's ID if one exists.
const webhookId = await Webhooks.create(ctx, {
externalId: clerkUserId,
type: 'user.created',
payload: webhookData,
});Using Models
Models are imported as namespaces in Convex functions:
import * as Users from "./models/users";
import * as Webhooks from "./models/webhooks";
export const myMutation = mutation({
handler: async (ctx) => {
await Users.ensureUserAuthenticated(ctx);
const user = await Users.getCurrentUserOrThrow(ctx);
// ...
},
});Conventions
- Every function that accesses the database takes
ctx: MutationCtx | QueryCtxas its first argument - Input validation happens at the start of each function — fail fast with descriptive errors
- Lookups by external ID use database indexes for performance
- Create operations are idempotent where possible (check for existing records first)