YeetCode
Convex App

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 storage

Users 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 found

create(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 | QueryCtx as 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)

On this page