FirstDevJob Docs
API Reference

Profiles API

Convex functions for the signed-in user's profile, role and account deletion

Clerk owns identity (email, password, OAuth). Convex stores one companion row per user in the profiles table holding just two things the app controls: an editable fullName and a role that gates moderation. The functions are split across two modules: convex/users.ts (session bootstrap) and convex/profiles.ts (read, rename, delete). Every function keys off identity.subject, the Clerk user id — a caller can never read or write another user's profile through this API.

There is no public profile: no displayName, bio, links or avatar fields exist, and there is no function to fetch another user's profile. The full field list is in Database Schema.

users.currentUser

Session bootstrap query used by the useAuth hook (src/hooks/useAuth.ts).

Kindquery
AuthAnonymous allowed — returns null when signed out
Argsnone

Returns: null, or { id, email, profile } where id is the Clerk user id, email comes from the Clerk identity token, and profile is the raw profiles document (or null if the row has not been created yet).

const user = useQuery(api.users.currentUser);
// user?.profile?.role === "admin"

users.ensureProfile

Creates the profiles row the first time a user signs in. Idempotent: if a row already exists it is returned untouched.

Kindmutation
AuthAuthenticated
Argsnone

Returns: the profiles document.

New rows are inserted with fullName taken from the Clerk identity name (or omitted) and role: "user". Nothing in the API can escalate a role — promoting someone to moderator or admin is a manual edit in the Convex dashboard.

Throws: Not authenticated.

const ensureProfile = useMutation(api.users.ensureProfile);
await ensureProfile();

useAuth calls this once per session after Convex reports the user as authenticated, so pages generally do not need to call it themselves.

profiles.getUserProfile

The profile page's read (src/components/ProfilePage.tsx).

Kindquery
AuthAuthenticated
Argsnone

Returns one of:

ShapeWhen
{ id, email, fullName, role }Signed in and the profile row exists. fullName is null when unset.
{ error: "Not authenticated" }No Clerk identity on the request
{ error: "Profile not found" }Signed in, but users.ensureProfile has not run yet

This query returns error objects instead of throwing, so callers must check for the error key before reading role. The other profile functions throw.

profiles.updateUserName

Kindmutation
AuthAuthenticated
ArgTypeRequiredNotes
fullNamestringYesTrimmed server-side

Returns: { success: true }, or { error: "Name is required" } when the trimmed value is empty, or { error: "Profile not found" }.

Throws: Not authenticated.

const updateUserName = useMutation(api.profiles.updateUserName);
await updateUserName({ fullName: "Jane Developer" });

There is no length cap on fullName in the mutation.

profiles.deleteAccount

Deletes everything this app stores about the caller. It does not delete the Clerk user; the profile page calls this mutation first and then calls Clerk's user.delete().

Kindmutation
AuthAuthenticated
Argsnone

Deleted, in order: every trackedApplications row for the user, every jobNotificationDeliveries row for the user, the emailSubscriptions row (if any), and the profiles row (if any).

Returns:

FieldTypeMeaning
successbooleanAlways true when the mutation completes
deletedTrackedApplicationsCountnumberTracked applications removed
deletedNotificationDeliveriesCountnumberEmail delivery receipts removed
deletedEmailSubscriptionbooleanWhether a subscription row existed
deletedProfilebooleanWhether a profile row existed

Throws: Not authenticated.

Jobs the user submitted are not deleted. jobs.submitterUserId keeps the Clerk id of a deleted account until the retention cron removes the job (see Jobs API).

Roles

profiles.role is the only authorization input in the backend.

RoleCan do
userBrowse, bookmark, track applications, submit jobs, manage email notifications
moderatorEverything above, plus every function in the Admin API
adminSame permissions as moderator; the two are only distinguished in the UI

The dashboard reads roles through dashboard.getDashboardData, which returns userRole: { isAdmin, isModerator, userEmail, role } in the same round trip as the rest of the page data.

On this page