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:
- Add a mutation that reads user
information from
MutationCtx
'sauth
and stores it in a table. - Call this mutation from your client.
- 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
:
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:
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:
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:
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.
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`...
}
});