Skip to Content
Dirly 2.0 is released 🎉
ArchitectureConvex Backend

Convex Backend

Understanding queries, mutations, and real-time data

Convex provides a TypeScript serverless backend with automatic real-time synchronization. All backend logic is defined in the convex/ directory.

Architecture Overview

Convex functions are organized into three types:

Operation Types in the API

TypeUse CaseExample
QueryRead data (GET-like operations)getTools, getToolBySlug
MutationWrite / change data (POST / PUT / DELETE)submitTool, upvoteTool
ActionSide effects: external API calls, emails, AI processing, etc.sendApprovalEmail

Queries

Queries are reactive - they automatically re-run when data changes.

Basic Query Pattern

convex/tools.ts:28-84

export const getTools = query({ args: { category: v.optional(v.string()), pricing: v.optional(v.string()), search: v.optional(v.string()), sort: v.optional(v.string()), // "newest", "upvotes" }, handler: async (ctx: QueryCtx, args) => { let toolsQuery; // Start with an index if (args.category && args.category !== "All") { toolsQuery = ctx.db .query("tools") .withIndex("by_category", (q) => q.eq("category", args.category!)); } else { toolsQuery = ctx.db .query("tools") .withIndex("by_approved", (q) => q.eq("approved", true)); } let tools = await toolsQuery.collect(); // Apply additional filters if (args.category && args.category !== "All") { tools = tools.filter((t) => t.approved); } if (args.pricing && args.pricing !== "All") { tools = tools.filter((t) => t.pricing === args.pricing); } if (args.search) { const searchLower = args.search.toLowerCase(); tools = tools.filter( (t) => t.name.toLowerCase().includes(searchLower) || t.description.toLowerCase().includes(searchLower) || t.tags.some((tag) => tag.toLowerCase().includes(searchLower)) ); } // Sort results if (args.sort === "upvotes") { tools.sort((a, b) => b.upvotes - a.upvotes); } else { tools.sort((a, b) => b.createdAt - a.createdAt); } return tools; }, });

Always start queries with an index using .withIndex(). Convex requires indexed queries for performance.

Query by Unique Field

convex/tools.ts

export const getToolBySlug = query({ args: { slug: v.string() }, handler: async (ctx: QueryCtx, args: { slug: string }) => { return await ctx.db .query("tools") .withIndex("by_slug", (q) => q.eq("slug", args.slug)) .first(); // Returns first match or null }, });

Query with Authentication

convex/tools.ts

export const getSubmittedTools = query({ args: {}, handler: async (ctx: QueryCtx) => { const identity = await getIdentity(ctx); return await ctx.db .query("tools") .withIndex("by_submittedBy", (q) => q.eq("submittedBy", identity.subject)) .collect(); }, });

Using Queries in React

convex/tools.ts

import { useQuery } from "convex/react"; import { api } from "@/convex/_generated/api"; function ToolsPage() { const tools = useQuery(api.tools.getTools, { category: "Image Generation", sort: "upvotes" }); if (tools === undefined) return <div>Loading...</div>; return ( <div> {tools.map(tool => <ToolCard key={tool._id} tool={tool} />)} </div> ); }

Queries return undefined while loading, then update automatically when data changes. No need for manual refetching!

Mutations

Mutations modify database state.

Create (Insert)

convex/tools.ts

export const submitTool = mutation({ args: { name: v.string(), description: v.string(), category: v.string(), tags: v.array(v.string()), websiteUrl: v.string(), pricing: v.string(), // ... more fields }, handler: async (ctx: MutationCtx, args: any) => { const identity = await getIdentity(ctx); // Generate slug const slug = args.name .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)+/g, ""); // Check for conflicts const existing = await ctx.db .query("tools") .withIndex("by_slug", (q) => q.eq("slug", slug)) .first(); const finalSlug = existing ? `${slug}-${Date.now()}` : slug; // Insert new tool const toolId = await ctx.db.insert("tools", { name: args.name, slug: finalSlug, description: args.description, category: args.category, tags: args.tags, websiteUrl: args.websiteUrl, pricing: args.pricing, upvotes: 0, submittedBy: identity.subject, approved: false, createdAt: Date.now(), isNew: true, // ... more fields }); return { toolId, slug: finalSlug }; }, });

Update (Patch)

convex/tools.ts

export const approveTool = mutation({ args: { toolId: v.id("tools"), sendEmail: v.optional(v.boolean()), }, handler: async (ctx: MutationCtx, args: { toolId: Id<"tools"> }) => { await checkAdmin(ctx); await ctx.db.patch(args.toolId, { approved: true }); return { success: true }; }, });

Delete

convex/tools.ts

export const rejectTool = mutation({ args: { toolId: v.id("tools"), reason: v.optional(v.string()), }, handler: async (ctx: MutationCtx, args) => { await checkAdmin(ctx); // Get data before deletion (for emails, etc.) const tool = await ctx.db.get(args.toolId); await ctx.db.delete(args.toolId); return { success: true, tool, reason: args.reason }; }, });

Increment Counter

convex/tools.ts

export const upvoteTool = mutation({ args: { toolId: v.id("tools") }, handler: async (ctx: MutationCtx, args: { toolId: Id<"tools"> }) => { const tool = await ctx.db.get(args.toolId); if (!tool) throw new Error("Tool not found"); await ctx.db.patch(args.toolId, { upvotes: tool.upvotes + 1 }); }, });

Using Mutations in React

import { useMutation } from "convex/react"; import { api } from "@/convex/_generated/api"; function UpvoteButton({ toolId }: { toolId: Id<"tools"> }) { const upvoteTool = useMutation(api.tools.upvoteTool); return ( <button onClick={() => upvoteTool({ toolId })}> Upvote </button> ); }

Authentication & Authorization

Convex integrates with Clerk for user identity.

Get Current User

convex/tools.ts

async function getIdentity(ctx: QueryCtx | MutationCtx) { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Unauthenticated"); return identity; }

Admin Check

convex/tools.ts

export async function checkAdmin(ctx: QueryCtx | MutationCtx) { const { user } = await getCurrentUser(ctx); if (!user || user.role !== "admin") { throw new Error("Unauthorized: Admin access required"); } return user; }

Protected Query

convex/tools.ts

export const getPendingTools = query({ handler: async (ctx: QueryCtx) => { await checkAdmin(ctx); // Throws if not admin return await ctx.db .query("tools") .withIndex("by_approved", (q) => q.eq("approved", false)) .collect(); }, });

Always verify user identity in mutations. Never trust client-side authentication checks.

Real-Time Subscriptions

Convex queries are reactive - components automatically re-render when data changes.

// Component A: Displays tools function ToolsList() { const tools = useQuery(api.tools.getTools, {}); // Automatically updates when tools change return tools?.map(tool => <div>{tool.name}</div>); } // Component B: Upvotes a tool function UpvoteButton({ toolId }) { const upvote = useMutation(api.tools.upvoteTool); return <button onClick={() => upvote({ toolId })}>Upvote</button>; }

When the upvote button is clicked:

  1. upvoteTool mutation runs
  2. Database is updated
  3. All components using getTools automatically re-render with new data

No manual cache invalidation or refetching needed - Convex handles it automatically!

Aggregations & Statistics

convex/tools.ts

export const getStats = query({ handler: async (ctx: QueryCtx) => { const approvedTools = await ctx.db .query("tools") .withIndex("by_approved", (q) => q.eq("approved", true)) .collect(); const categories = new Set(approvedTools.map((t) => t.category)); const totalUpvotes = approvedTools.reduce((sum, t) => sum + t.upvotes, 0); const featuredCount = approvedTools.filter((t) => t.featured).length; return { totalTools: approvedTools.length, totalCategories: categories.size, totalFeatured: featuredCount, totalUpvotes, }; }, });

Error Handling

export const getToolBySlug = query({ args: { slug: v.string() }, handler: async (ctx, args) => { const tool = await ctx.db .query("tools") .withIndex("by_slug", (q) => q.eq("slug", args.slug)) .first(); if (!tool) { throw new Error("Tool not found"); } if (!tool.approved) { throw new Error("Tool pending approval"); } return tool; }, });

Handle errors in React:

function ToolPage({ slug }) { const tool = useQuery(api.tools.getToolBySlug, { slug }); if (tool === undefined) return <div>Loading...</div>; if (tool === null) return <div>Tool not found</div>; return <div>{tool.name}</div>; }
Last updated on