Back to Skills

convex-patterns

majiayu000
Updated Yesterday
58
9
58
View on GitHub
Metadesign

About

This skill provides Convex backend patterns including query/mutation/action structure, TypeScript workarounds, and auth patterns. It covers resilient generation with 10-minute actions and normalized schema design without nested documents. Use it when working with Convex functions, internal operations, or backend architecture.

Quick Install

Claude Code

Recommended
Plugin CommandRecommended
/plugin add https://github.com/majiayu000/claude-skill-registry
Git CloneAlternative
git clone https://github.com/majiayu000/claude-skill-registry.git ~/.claude/skills/convex-patterns

Copy and paste this command in Claude Code to install this skill

Documentation

Convex Backend Patterns

94+ Convex modules power the backend. Real-time queries, mutations for writes, actions for LLM calls (10min limit). Normalized schema design (no nested documents).

Query/Mutation/Action Structure

Three function types:

// QUERY: Read-only, reactive, client-subscribable
export const getUser = query({
  args: { userId: v.id("users") },
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
});

// MUTATION: Write operations, trigger re-renders
export const updateUser = mutation({
  args: { userId: v.id("users"), name: v.string() },
  handler: async (ctx, args) => {
    await ctx.db.patch(args.userId, { name: args.name });
  },
});

// ACTION: External APIs, LLM calls, long-running (up to 10min)
export const generateResponse = action({
  args: { conversationId: v.id("conversations"), modelId: v.string() },
  handler: async (ctx, args) => {
    // Call external API (Vercel AI Gateway)
    const result = await streamText({ model, messages });
    // Update DB via mutation
    await ctx.runMutation(internal.messages.create, { ... });
  },
});

When to use:

  • Query: Real-time data fetch (conversations, messages, users)
  • Mutation: DB writes (create message, update status)
  • Action: LLM streaming, embeddings, external HTTP calls

TypeScript Recursion Workaround

With 94+ modules, TypeScript hits depth limits on internal.* and api.* types. Use @ts-ignore + cast pattern:

// From convex/generation.ts line 100-110
const costBias = await (ctx.runQuery as any)(
  // @ts-ignore - TypeScript recursion limit with 94+ Convex modules
  api.users.getUserPreferenceByUserId,
  { userId: args.userId, key: "autoRouterCostBias" },
) as Promise<number | null>;

// From convex/chat.ts line 166-174
const conversationId = await ctx.runMutation(
  // @ts-ignore - TypeScript recursion limit with 85+ Convex modules
  internal.conversations.createInternal,
  {
    userId: user._id,
    model: modelsToUse[0],
    title: "New Chat",
  },
);

Pattern: (ctx.runX as any) + @ts-ignore on reference + as ReturnType. Bypasses parameter inference, keeps return type safety.

Auth Pattern (Defense-in-Depth)

Every query/mutation verifies user via internal helper:

// From convex/lib/userSync.ts (pattern)
export async function getCurrentUserOrCreate(ctx: MutationCtx) {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) throw new Error("Not authenticated");

  const user = await ctx.db
    .query("users")
    .withIndex("by_clerk_id", (q) => q.eq("clerkId", identity.subject))
    .first();

  if (!user) throw new Error("User not found");
  return user;
}

// Usage in mutations (from convex/chat.ts line 91)
const user = await getCurrentUserOrCreate(ctx);

Actions use internal queries:

// From convex/lib/helpers.ts line 30-41
export const getCurrentUser = internalQuery({
  args: {},
  handler: async (ctx): Promise<Doc<"users"> | null> => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) return null;

    return await ctx.db
      .query("users")
      .withIndex("by_clerk_id", (q) => q.eq("clerkId", identity.subject))
      .first();
  },
});

// Called from actions
const user = await ctx.runQuery(internal.lib.helpers.getCurrentUser, {});

Normalized Schema Design

NO nested documents. Use junction tables for M:N relationships.

Bad (nested):

// ❌ DON'T DO THIS
defineTable("messages", {
  attachments: v.optional(v.array(v.object({
    name: v.string(),
    storageId: v.string(),
    size: v.number(),
  }))),
})

Good (normalized):

// ✅ From convex/schema.ts line 304-337
attachments: defineTable({
  messageId: v.id("messages"),
  conversationId: v.id("conversations"), // Denormalized for filtering
  userId: v.id("users"),
  type: v.union(v.literal("image"), v.literal("file"), v.literal("audio")),
  name: v.string(),
  storageId: v.id("_storage"),
  mimeType: v.string(),
  size: v.number(),
  createdAt: v.number(),
})
  .index("by_message", ["messageId"])
  .index("by_conversation", ["conversationId"])
  .index("by_user", ["userId"])

Junction tables for M:N:

// From convex/schema.ts line 552-560
projectConversations: defineTable({
  projectId: v.id("projects"),
  conversationId: v.id("conversations"),
  addedAt: v.number(),
  addedBy: v.id("users"),
})
  .index("by_project", ["projectId"])
  .index("by_conversation", ["conversationId"])
  .index("by_project_conversation", ["projectId", "conversationId"])

Benefits: 40% smaller docs, 10x faster cascade deletes, queryable relationships, no data drift.

Resilient Generation Pattern

LLM calls MUST survive page refresh. Use 10min actions + DB persistence.

Flow (from convex/chat.ts + generation.ts):

// 1. Pre-create message with status: "pending"
const assistantMessageId = await ctx.runMutation(internal.messages.create, {
  conversationId,
  userId: user._id,
  role: "assistant",
  content: "",
  status: "pending", // Not "generating" yet
  model,
});

// 2. Schedule action (non-blocking)
await ctx.scheduler.runAfter(0, internal.generation.generateResponse, {
  conversationId,
  existingMessageId: assistantMessageId,
  modelId,
  userId: user._id,
});

// 3. Action updates status → "generating" → streams to partialContent → "complete"
await ctx.runMutation(internal.messages.updateStatus, {
  messageId: assistantMessageId,
  status: "generating",
  generationStartedAt: Date.now(),
});

// 4. Stream updates DB periodically (every 500ms)
for await (const chunk of stream) {
  accumulatedContent += chunk;
  await ctx.runMutation(internal.messages.updatePartialContent, {
    messageId: assistantMessageId,
    partialContent: accumulatedContent,
  });
}

// 5. Finalize on complete
await ctx.runMutation(internal.messages.updateStatus, {
  messageId: assistantMessageId,
  status: "complete",
  content: finalContent,
  partialContent: undefined,
  generationCompletedAt: Date.now(),
});

Message states: pending | generating | complete | stopped | error

Client subscribes:

const message = useQuery(api.messages.get, { id: messageId });
// Auto-updates as partialContent changes

On refresh: Client sees partialContent from DB, streaming continues server-side.

Index Requirements

Every foreign key needs index. Composite indexes for common queries.

// From convex/schema.ts line 282-301
messages: defineTable({ ... })
  .index("by_conversation", ["conversationId"])
  .index("by_user", ["userId"])
  .index("by_conversation_status", ["conversationId", "status"]) // Find generating messages
  .index("by_conversation_created", ["conversationId", "createdAt"]) // Ordered messages
  .vectorIndex("by_embedding", {
    vectorField: "embedding",
    dimensions: 1536,
    filterFields: ["conversationId", "userId"],
  })
  .searchIndex("search_content", {
    searchField: "content",
    filterFields: ["conversationId", "userId", "role"],
  })

Index types:

  • Simple: ["userId"] - Foreign keys
  • Composite: ["userId", "status"] - Filtered queries
  • Vector: Semantic search (1536d embeddings)
  • Search: Full-text search (Convex native)

Internal Helpers Pattern

Avoid type recursion by extracting helpers:

// From convex/lib/helpers.ts line 58-69
export const getConversationMessages = internalQuery({
  args: { conversationId: v.id("conversations") },
  handler: async (ctx, args): Promise<Doc<"messages">[]> => {
    return await ctx.db
      .query("messages")
      .withIndex("by_conversation", (q) =>
        q.eq("conversationId", args.conversationId))
      .order("asc")
      .collect();
  },
});

Usage in actions:

const messages = await ctx.runQuery(
  internal.lib.helpers.getConversationMessages,
  { conversationId }
);

All helpers: getCurrentUser, getConversation, getConversationMessages, getMemoriesByIds, getMessage, getMessageAttachments, etc.

Testing Patterns

Use convex-test with factories:

// From convex/__tests__/users.test.ts
import { convexTest } from "../../__tests__/testSetup";
import { createMockIdentity, createTestUserData } from "@/lib/test/factories";
import schema from "../schema";

it("returns user for authenticated identity", async () => {
  const t = convexTest(schema);
  const identity = createMockIdentity();

  await t.run(async (ctx) => {
    await ctx.db.insert("users", createTestUserData({
      clerkId: identity.subject,
      email: "test@example.com",
    }));
  });

  const asUser = t.withIdentity(identity);
  // @ts-ignore - Type instantiation too deep with 94+ Convex modules
  const result = await asUser.query(api.users.getCurrentUser, {});

  expect(result?.email).toBe("test@example.com");
});

Factories (src/lib/test/factories.ts):

  • createMockIdentity() - Auth identity
  • createTestUserData() - User record
  • createTestConversationData() - Conversation
  • createTestMessageData() - Message

Pattern: t.run() for DB setup, t.withIdentity() for auth context, @ts-ignore on type-deep queries.

Key Files

  • convex/schema.ts - Normalized schema (1546 lines)
  • convex/chat.ts - Message sending, regeneration
  • convex/generation.ts - LLM streaming action (resilient)
  • convex/lib/helpers.ts - Internal query helpers (avoid recursion)
  • convex/__tests__/users.test.ts - Testing patterns
  • convex/lib/userSync.ts - Auth helpers (getCurrentUserOrCreate)
  • convex/messages.ts - Message mutations (status, partialContent)

Avoid

  • Nested documents in schema (use junction tables)
  • Client-only streaming (loses data on refresh)
  • Direct ctx.runQuery(api.*) in actions without cast (type errors)
  • Missing indexes on foreign keys (slow queries)
  • Queries in actions without internal helper (type depth errors)
  • Mutations for LLM calls (10s timeout, use actions)

GitHub Repository

majiayu000/claude-skill-registry
Path: skills/convex-patterns

Related Skills

content-collections

Meta

This skill provides a production-tested setup for Content Collections, a TypeScript-first tool that transforms Markdown/MDX files into type-safe data collections with Zod validation. Use it when building blogs, documentation sites, or content-heavy Vite + React applications to ensure type safety and automatic content validation. It covers everything from Vite plugin configuration and MDX compilation to deployment optimization and schema validation.

View skill

creating-opencode-plugins

Meta

This skill provides the structure and API specifications for creating OpenCode plugins that hook into 25+ event types like commands, files, and LSP operations. It offers implementation patterns for JavaScript/TypeScript modules that intercept and extend the AI assistant's lifecycle. Use it when you need to build event-driven plugins for monitoring, custom handling, or extending OpenCode's capabilities.

View skill

langchain

Meta

LangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.

View skill

Algorithmic Art Generation

Meta

This skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.

View skill