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 getCurrentUser from ./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._id when 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