Schema Design — Two Related Tables
Our kanban board manages one flat list of tasks. A planner needs projects too, and each project has its own tasks. So we need two tables that are related to each other.
The Data Model
Here is what we need:
- A projects table: each project has a name and a description
- A tasks table: each task belongs to a specific project
The relationship between them: every task has a projectId field that points to a document in the projects table. This is how you model relationships in Convex: with ID references.
projects tasks
┌──────────────────────┐ ┌──────────────────────────────┐
│ _id: Id<"projects"> │◀────────│ projectId: Id<"projects"> │
│ name: string │ │ _id: Id<"tasks"> │
│ description: string │ │ title: string │
└──────────────────────┘ │ description: string │
│ status: "todo" | ... │
└──────────────────────────────┘
This is a common pattern in database design, especially in relational databases. Convex is not a relational database, but it uses the same pattern.
Update the Schema
Update convex/schema.ts:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
projects: defineTable({
name: v.string(),
description: v.optional(v.string()),
}),
tasks: defineTable({
projectId: v.id("projects"),
title: v.string(),
description: v.string(),
status: v.union(
v.literal("todo"),
v.literal("in-progress"),
v.literal("done"),
),
}),
});
What changed:
-
New
projectstable: withnameand (optional)descriptionfields. Like all Convex documents, each project automatically gets_idand_creationTime. -
projectId: v.id("projects"): this is the relationship. Every task now belongs to a project. Thev.id("projects")validator ensures the value is a valid document ID.
This is called a one-to-many relationship: one project has many tasks, and each task belongs to one project.
Update the create mutation
Next, we need to update the create mutation to include the projectId in the arguments. Update convex/tasks.ts:
export const create = mutation({
args: {
+ projectId: v.id("projects"),
title: v.string(),
description: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert("tasks", {
+ projectId: args.projectId,
title: args.title,
description: args.description,
status: "todo",
});
},
});
Deploy the Schema
If npx convex dev is running, it will detect these changes and deploy them automatically. You should see:
✔ Schema validation complete.
✔ Convex functions ready!
Checkpoint: Commit your progress.
git add .
git commit -m "planner-02: Add projects table and projectId to tasks"
git push