Record Ownership When Creating Projects and Issues
Now we are going to change the data model so that we know who owns what. Here are the backend rules we will enforce in this section:
- Creating a project requires being signed in. The creator is recorded as the project’s owner.
- Creating an issue requires being signed in. The creator is recorded as the issue’s creator.
We use two different words on purpose, because the rules are different. A project owner keeps authority for as long as the project exists. An issue creator only keeps it until the status changes. Two different words make that difference easy to see in the code.
Add ownerId and creatorId to the Schema
Update convex/schema.ts:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { authTables } from "@convex-dev/auth/server";
export default defineSchema({
...authTables,
projects: defineTable({
name: v.string(),
description: v.optional(v.string()),
ownerId: v.id("users"),
deletedAt: v.optional(v.number()),
})
.index("by_deletedAt", ["deletedAt"])
.index("by_owner", ["ownerId"])
.searchIndex("search_name", {
searchField: "name",
filterFields: ["deletedAt"],
}),
issues: defineTable({
projectId: v.id("projects"),
creatorId: v.id("users"),
title: v.string(),
description: v.string(),
status: v.union(
v.literal("todo"),
v.literal("in-progress"),
v.literal("done"),
),
}).index("by_project", ["projectId"]),
});
Notice we also added .index("by_owner", ["ownerId"]) on projects. We will use that index later to query “projects I own” efficiently.
Update projects.create to Stamp the Owner
Update convex/projects.ts — specifically the create mutation:
// ... existing imports ...
import { getCurrentUser } from "./users";
// ... list, get stay unchanged ...
export const create = mutation({
args: {
name: v.string(),
description: v.optional(v.string()),
},
handler: async (ctx, args) => {
const user = await getCurrentUser(ctx); // 👀
const projectId = await ctx.db.insert("projects", {
name: args.name,
description: args.description,
ownerId: user._id, // 👀
});
return projectId;
},
});
// ... remove, deleteProjectCascade stay unchanged ...
Three changes:
- We import
getCurrentUserfrom./users. - The handler’s first line is
const user = await getCurrentUser(ctx);. This is the server-side auth gate. If the client is not signed in, the helper throws"Not authenticated"and the mutation never runs. Convex wraps that error and sends it back to the client. - We pass
ownerId: user._idwhen inserting the new row.
Update issues.create the Same Way
Update convex/issues.ts — the create mutation:
// ... existing imports ...
import { getCurrentUser } from "./users";
// ... list stays unchanged ...
export const create = mutation({
args: {
projectId: v.id("projects"),
title: v.string(),
description: v.string(),
},
handler: async (ctx, args) => {
const user = await getCurrentUser(ctx); // 👀
await ctx.db.insert("issues", {
projectId: args.projectId,
creatorId: user._id, // 👀
title: args.title,
description: args.description,
status: "todo",
});
},
});
// ... updateStatus, remove stay unchanged ...
Same pattern, same three changes. We will come back to updateStatus and remove later, when we enforce the harder rules. For example, the issue creator can edit or remove their own issue only as long as it is still in the “todo” status.
Checkpoint: Commit your progress.
git add .
git commit -m "tracker-06: Record ownership when creating projects and issues"
git push