Data with Convex
Mutations & transactions
Lesson 5 of 7
What you'll learn
- Write a Convex mutation that reads then writes
- Run it from a client component with
useMutation - Understand why mutations are transactional
A mutation is a function that changes the database. Like queries, it gets a typed context — but it can insert, patch, and delete. Here's the real "mark complete" toggle from this app:
export const setComplete = mutation({
args: {
lessonId: v.string(),
courseSlug: v.string(),
completed: v.boolean(),
},
handler: async (ctx, { lessonId, courseSlug, completed }) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const existing = await ctx.db
.query("progress")
.withIndex("by_user_and_lesson", (q) =>
q.eq("clerkUserId", identity.subject).eq("lessonId", lessonId)
)
.unique();
if (completed && !existing) {
await ctx.db.insert("progress", {
clerkUserId: identity.subject,
lessonId,
courseSlug,
completedAt: Date.now(),
});
} else if (!completed && existing) {
await ctx.db.delete(existing._id);
}
},
});
Transactional by default
Every mutation runs as a single transaction. The read (existing) and the write (insert/delete) happen atomically — no other mutation can slip in between them. That eliminates an entire class of race conditions you'd normally guard against by hand.
You call it from the client like this:
const setComplete = useMutation(api.progress.setComplete);
await setComplete({ lessonId, courseSlug, completed: true });
The moment it commits, every useQuery reading that row re-renders. The button you clicked, the sidebar checkmark, and the dashboard counter all move together.
Authenticate inside the function
Notice the mutation calls ctx.auth.getUserIdentity() itself. Never trust the client to say who it is — the server reads identity from the verified token. We'll wire that token up next.
The exercise implements the same idempotent toggle logic. Run it and confirm completing twice doesn't create duplicates.
setComplete is called several times. Run it — the final state should have each lesson at most once, exactly like the real mutation.
Data works. Now let's prove who the user is, with Clerk.
Sign in to save your progress across devices.