Skip to main content

Convex Backend Architecture

CodeJam is built on Convex, a real-time backend-as-a-service that provides:
  • Type-safe API: Full TypeScript support from backend to frontend
  • Real-time reactivity: Automatic UI updates when data changes
  • Serverless functions: Queries, mutations, and actions run on-demand
  • Built-in auth: Integrated authentication with multiple providers

Function Types

Convex provides three types of backend functions:

Queries

Queries are read-only operations that fetch data. They:
  • Cannot modify the database
  • Are automatically cached and reactive
  • Trigger React component re-renders when data changes
  • Run transactionally with consistent snapshot reads
import { query } from "./_generated/server";
import { getAuthUserId } from "@convex-dev/auth/server";

export const viewer = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (userId === null) return null;
    const user = await ctx.db.get(userId);
    return user;
  },
});

Mutations

Mutations are write operations that modify data. They:
  • Can read and write to the database
  • Run transactionally (all-or-nothing)
  • Are not cached
  • Trigger query invalidation to update subscribed components
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";

export const updateName = mutation({
  args: { name: v.string() },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");
    await ctx.db.patch(userId, { name: args.name });
  },
});

Actions

Actions are non-transactional operations for external API calls, long-running tasks, or operations requiring third-party services. They:
  • Can call external APIs
  • Can call queries and mutations
  • Do not run transactionally
  • Have longer timeout limits

Using the API from React

Queries with useQuery

Queries are reactive - your component automatically re-renders when data changes:
import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";

function Profile() {
  const user = useQuery(api.users.viewer);
  
  if (user === undefined) return <div>Loading...</div>;
  if (user === null) return <div>Not logged in</div>;
  
  return <div>Welcome {user.name}!</div>;
}

Mutations with useMutation

Mutations are called imperatively:
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";

function UpdateProfile() {
  const updateName = useMutation(api.users.updateName);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    await updateName({ name: "New Name" });
  };
  
  return <form onSubmit={handleSubmit}>...</form>;
}

Real-time Reactivity

One of Convex’s most powerful features is automatic real-time updates:
  1. Subscribe once: Use useQuery to subscribe to data
  2. Automatic updates: When data changes (via any mutation), all subscribed components re-render
  3. No manual refetching: No need for refetch() or cache invalidation
  4. Optimistic updates: Mutations can return immediately while processing
// Component A subscribes to leaderboard
const topUsers = useQuery(api.users.getTopUsers);

// Component B updates user XP
const updateXp = useMutation(api.users.updateXp);
await updateXp({ amount: 100 });

// Component A automatically re-renders with new data!

Authentication Pattern

All authenticated endpoints use the getAuthUserId pattern:
import { getAuthUserId } from "@convex-dev/auth/server";

export const protectedQuery = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Unauthorized");
    
    // Access user's data
    const user = await ctx.db.get(userId);
    return user;
  },
});

Error Handling

Convex functions can throw errors that are automatically propagated to the client:
export const updateProfile = mutation({
  args: { name: v.string() },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");
    if (args.name.length < 2) throw new Error("Name too short");
    
    await ctx.db.patch(userId, { name: args.name });
  },
});
Catch errors on the client:
try {
  await updateProfile({ name: "X" });
} catch (error) {
  console.error(error.message); // "Name too short"
}

Next Steps

Build docs developers (and LLMs) love