DATABASES / LESSON 14
Convex in Your App.
Add a schema, write mutations, and read and write tasks from a Next.js page

Loading the lesson…
Loading activities…
DATABASES / LESSON 14
Add a schema, write mutations, and read and write tasks from a Next.js page

Loading the lesson…
Loading activities…
Last lesson your app could read tasks from Convex. Cool start. Now we'll turn it into a real lil to-do app that adds tasks and checks them off, with every open browser tab staying in sync the whole time.
This lesson picks up right where the Next.js quickstart left off. You should already have these pieces.
tasks table with three sample tasks, each with text and isCompletedconvex/tasks.ts file with a get queryConvexClientProvider wrapping your app in app/layout.tsxnpx convex dev running in one terminal and npm run dev in anotherWe'll touch three files. A new convex/schema.ts, the existing convex/tasks.ts and app/page.tsx.
A schema describes your tables and what each document should look like. Convex runs fine without one. Add a schema though, and Convex checks every document you save while your editor learns the exact shape of your data.
Create a new file called convex/schema.ts.
Here's what's happening.
defineSchema describes your whole databasedefineTable describes one table, here called tasksv is the validator builder. v.string() means "this has to be text" and v.boolean() means "this has to be true or false"You don't list _id or _creationTime. Convex tacks those onto every document automatically.
When you save, npx convex dev pushes the schema to your dev deployment and Convex starts enforcing it. The fields match the sample data you imported, so nothing breaks.
Open convex/tasks.ts. Update the import at the top, add the v import and drop a createTask mutation below your existing get query.
Let's pull it apart.
mutation tells Convex this function is allowed to write to the databaseargs says the function takes one argument called text, and it has to be a string. Convex checks that EVERY time the function gets calledctx.db.insert("tasks", ...) adds a new document to the tasks tableEvery new task starts out with isCompleted: false, since our schema says that field is required.
Add one more mutation at the bottom of the same file.
Two new things in there.
v.id("tasks") checks that the value has the shape of a tasks-table ID. It does not prove that a document with that ID still exists, so the mutation checks with ctx.db.get before updating.ctx.db.patch updates only the fields you pass in. Here it flips isCompleted and leaves text aloneNow swap out app/page.tsx for a page that lists tasks, adds new ones and checks them off.
useQuery(api.tasks.get) subscribes your page to the get query. It returns undefined while the data first loads, which is why you see "Loading..." until the tasks show up. After that, it holds your list of tasks.
useMutation(api.tasks.createTask) doesn't run anything right away. It hands you back a function, and you call that function with your arguments when the user does something, like submitting the form.
Check out api.tasks.createTask and api.tasks.setTaskCompleted. The name comes from the file (tasks.ts) plus the export name. npx convex dev generates this api object for you in the convex/_generated folder, so your editor can autocomplete function names and warn you when you pass the wrong arguments.
Here's the fun part. Look closely... there's no code anywhere in the page that refreshes the list after you add or check off a task.
When createTask saves a new document, Convex knows the get query read from the tasks table. It reruns the query and pushes the new list to every page that's subscribed, and your component re-renders with the new task.
Try it out.
Our to-do app only needed queries and mutations, since it only talks to the Convex database. Later on, if you want to send an email or call a payment service, that's what actions are for. Actions can call outside services and then call a mutation to save the result. The Convex actions docs walk through writing one.
convex/schema.ts uses defineSchema, defineTable and v validators to describe your datactx.db.insert to add documents and ctx.db.patch to update themargs with v validators checks every value sent to your functionsuseQuery reads data and stays subscribed, and useMutation gives you a function to call when you want to writeYou've now seen two VERY different databases. Supabase gives you SQL on Postgres, and Convex gives you documents plus TypeScript functions. Next, start thinking about which one fits the app you actually want to build.