Skip to main content

Storing Users in the Convex Database

You might want to have a centralized place that stores information about the users who have previously signed up or logged in to your app. To do this you can:

  1. Add a mutation that reads user information from MutationCtx's auth and stores it in a table.
  2. Call this mutation from your client.
  3. Optionally define a schema and an index for this table

storeUser mutation

This is an example of a mutation that stores the user's name and tokenIdentifier:

convex/users.ts
import { mutation } from "./_generated/server";

/**
* Insert or update the user in a Convex table then return the document's ID.
*
* The `UserIdentity.tokenIdentifier` string is a stable and unique value we use
* to look up identities.
*
* Keep in mind that `UserIdentity` has a number of optional fields, the
* presence of which depends on the identity provider chosen. It's up to the
* application developer to determine which ones are available and to decide
* which of those need to be persisted. For Clerk the fields are determined
* by the JWT token's Claims config.
*/
export const store = mutation({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Called storeUser without authentication present");
}

// Check if we've already stored this identity before.
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier),
)
.unique();
if (user !== null) {
// If we've seen this identity before but the name has changed, patch the value.
if (user.name !== identity.name) {
await ctx.db.patch(user._id, { name: identity.name });
}
return user._id;
}
// If it's a new identity, create a new `User`.
return await ctx.db.insert("users", {
name: identity.name!,
tokenIdentifier: identity.tokenIdentifier,
});
},
});

Calling storeUser from React

You can call this mutation when the user logs in from a useEffect hook. Once the mutation succeeds we store the Convex document ID representing the user in the component state.

Here we created a helper hook that does this job:

src/useStoreUserEffect.ts
import { useUser } from "@clerk/clerk-react";
import { useConvexAuth } from "convex/react";
import { useEffect, useState } from "react";
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { Id } from "../convex/_generated/dataModel";

export default function useStoreUserEffect() {
const { isAuthenticated } = useConvexAuth();
const { user } = useUser();
// When this state is set we know the server
// has stored the user.
const [userId, setUserId] = useState<Id<"users"> | null>(null);
const storeUser = useMutation(api.users.store);
// Call the `storeUser` mutation function to store
// the current user in the `users` table and return the `Id` value.
useEffect(() => {
// If the user is not logged in don't do anything
if (!isAuthenticated) {
return;
}
// Store the user in the database.
// Recall that `storeUser` gets the user information via the `auth`
// object on the server. You don't need to pass anything manually here.
async function createUser() {
const id = await storeUser();
setUserId(id);
}
createUser();
return () => setUserId(null);
// Make sure the effect reruns if the user logs in with
// a different identity
}, [isAuthenticated, storeUser, user?.id]);
return userId;
}

You can then use this hook in the top level component which needs the user ID:

src/App.tsx
import useStoreUserEffect from "./useStoreUserEffect.js";

export function App() {
const userId = useStoreUserEffect();
if (userId === null) {
return <div>Storing user...</div>;
}
return <div>Stored user ID: {userId}</div>;
}

Note that you can simplify the hook based on your needs. You can remove the isAuthenticated check if you only call the hook from a component wrapped in Authenticated from convex/react. You can remove the useState if your client doesn't need to know the user ID, but you need to make sure that a mutation which requires that the user is stored doesn't get called before the useEffect finishes.

users table schema

We can define a users table, optionally with an index for efficient document queries:

convex/schema.ts
users: defineTable({
name: v.string(),
tokenIdentifier: v.string(),
}).index("by_token", ["tokenIdentifier"]),

Reading from users table

You can now load user documents from queries and mutations. This can be useful for referencing the user's document ID in other documents. Note that using the "by_token" index requires you to define that index.

convex/myMutation.ts
import { mutation } from "./_generated/server";

export const send = mutation({
args: { body: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Unauthenticated call to mutation");
}
// Note: If you don't want to define an index right away, you can use
// ctx.db.query("users")
// .filter(q => q.eq(q.field("tokenIdentifier"), identity.tokenIdentifier))
// .unique();
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier),
)
.unique();
if (!user) {
throw new Error("Unauthenticated call to mutation");
}
// do something with `user`...
}
});