The Current User on the Server

To enforce auth rules on the backend, we need to know who is calling. Convex Auth gives us that through the ctx.auth object, and the getAuthUserId helper reads it and returns a user ID. In this section, we will write a currentUser query that the frontend can subscribe to, and a getCurrentUser helper that mutations can call to enforce “must be signed in” rules.

Create convex/users.ts

Create convex/users.ts:

import { query } from "./_generated/server";
import type { QueryCtx, MutationCtx } from "./_generated/server";
import { getAuthUserId } from "@convex-dev/auth/server";
import type { Doc } from "./_generated/dataModel";

/**
 * Public query used by the frontend to show the signed-in user in the header.
 * Returns `null` when nobody is signed in.
 */
export const currentUser = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (userId === null) return null;
    return await ctx.db.get(userId);
  },
});

currentUser is a Convex query. The client can subscribe to it directly with useQuery(api.users.currentUser). Queries are reactive, so the React component that uses this query will re-render on its own when the user signs in or out.

Every Convex query, mutation, and action receives a ctx object. You have already used ctx.db for database access. That same object has another field, ctx.auth, which holds the identity attached to the incoming request.

Convex Auth provides a helper called getAuthUserId that reads ctx.auth, validates the session token, and returns a Doc<"users"> ID (the primary key into the users table the library manages). It returns null if the caller is not signed in.

When you save the file, npx convex dev re-deploys and api.users.currentUser becomes available on the client.

Add getCurrentUser Helper

Update convex/users.ts to add the getCurrentUser helper:

/**
 * Server-side helper used by mutations (and queries that require auth).
 * Throws if the caller is not signed in, or if their user row no longer exists.
 * Returns the full user document so callers can read fields like `_id` or `name`.
 */
export async function getCurrentUser(
  ctx: QueryCtx | MutationCtx,
): Promise<Doc<"users">> {
  const userId = await getAuthUserId(ctx);
  if (userId === null) {
    throw new Error("Not authenticated");
  }
  const user = await ctx.db.get(userId);
  if (user === null) {
    throw new Error("Signed-in user no longer exists");
  }
  return user;
}

getCurrentUser is a plain async function. It is a helper that mutations call internally. Convex does not register it as an exported function (only query/mutation/action constructors get registered), so the client can never call it directly. It is an internal utility, not part of the API.

The type signature says QueryCtx | MutationCtx. Either context has an auth field and a db field, so getCurrentUser works from both. In practice we will only call it from mutations, but making it usable from queries too adds no extra work and keeps the helper flexible.

This is how we will use it in mutations:

const user = await getCurrentUser(ctx);

That line enforces “you must be signed in” and gives us the user’s ID to compare against ownerId, creatorId, and other authorization fields. Putting the check in a helper means we write it once and every mutation gets it. If we ever need to change how auth works (add role checks, log unauthorized attempts, and so on), there is one function to edit.

Checkpoint: Commit your progress.

git add .
git commit -m "tracker-04: Add currentUser query and getCurrentUser helper"
git push