Clerk + Convex Custom Sign-up Flow Race Conditions

Hey everyone, Here is a summary of what had been discussed on the clerk-channel here in the convex community, and I really need some help figuring out an issue I'm facing with Convex + Clerk integration during a custom sign-up flow. The Context: I'm implementing a custom multi-step onboarding flow (6 steps). At step 3, the user signs up/signs in using Clerk, but I'm using a custom sign-in flow as described here: Clerk Docs – Custom Sign-In Flow. This means I manually handle user creation, email verification, and setting the Clerk session as active using setActive. Once the email is verified and the session is successfully set as active with setActive, I need to immediately call a Convex mutation (initializeUser) to create a user record in my users table. This record stores essential data and their current onboarding step (e.g., profileState: "pending_step4"). This is crucial so if the user drops off, they can resume later from where they left off. I avoid creating this DB record before step 3 because the user isn't verified yet, and there's no point saving incomplete/unauthenticated data if they might just leave.
Custom Flows: Build a custom email/password authentication flow
Learn how to build a custom email/password sign-up and sign-in flow using the Clerk API.
3 Replies
Convex Bot
Convex Bot2mo ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
Critically Talented
The Problem: The issue arises right after await setActive({ session: signUpAttempt.createdSessionId }). The very next step is to call my initializeUser mutation. Inside this mutation, it seems that ctx.auth.getUserIdentity() returns null or doesn't reflect the user who just became active. I know clerk + convex are linked properly because in another itteration where i had the next step component call this mutation it works, but calling it even after an awaited setActive call leads to the initializeUser mutation failing because it requires an authenticated user identity. Here's a redacted snippet of the relevant part of my React component code:
// Called when submitting a valid verification code per schema configuration
const handleVerificationSubmit = async (
values: z.infer<typeof VerificationSchema>
) => {
// ... (Loading / error handling)
setIsInLoadingState(true);

try {
// Use the code the user provided to attempt verification
const signUpAttempt = await signUp.attemptEmailAddressVerification({
code: values.verificationCode,
});

// If verification was completed, set the session to active
// initiate the DB then go to next step
if (signUpAttempt.status === "complete") {
await setActive({ session: signUpAttempt.createdSessionId });

// PROBLEM AREA: The call below sometimes fails because ctx.auth.getUserIdentity() isn't ready
// Really tempted to just put an awaited timeout promise right here...
await initializeUser({
// ... user data
}); // TODO: This HAS to succeed reliably!

toast.info("Verification Success");
goToNextStep(); // Proceed to next onboarding step

} else {
// ... (Handle non-complete status)
}
} catch (error) {
// Show error here (Often fails here because initializeUser throws)
} finally{
setIsInLoadingState(false);
}
};
// Called when submitting a valid verification code per schema configuration
const handleVerificationSubmit = async (
values: z.infer<typeof VerificationSchema>
) => {
// ... (Loading / error handling)
setIsInLoadingState(true);

try {
// Use the code the user provided to attempt verification
const signUpAttempt = await signUp.attemptEmailAddressVerification({
code: values.verificationCode,
});

// If verification was completed, set the session to active
// initiate the DB then go to next step
if (signUpAttempt.status === "complete") {
await setActive({ session: signUpAttempt.createdSessionId });

// PROBLEM AREA: The call below sometimes fails because ctx.auth.getUserIdentity() isn't ready
// Really tempted to just put an awaited timeout promise right here...
await initializeUser({
// ... user data
}); // TODO: This HAS to succeed reliably!

toast.info("Verification Success");
goToNextStep(); // Proceed to next onboarding step

} else {
// ... (Handle non-complete status)
}
} catch (error) {
// Show error here (Often fails here because initializeUser throws)
} finally{
setIsInLoadingState(false);
}
};
Thoughts & What I've Considered: Analogy to React State: This reminds me of using useState in React. If you update state and immediately try to use the new value on the next line within the same function scope, you get the old value because state updates are asynchronous. useRef, however, updates immediately.
// useState (async update)
const [value, setValue] = useState(0);
const handleClick = () => {
setValue(42);
console.log(value); // Still logs 0
};

// useRef (sync update)
const valueRef = useRef(0);
const handleClick = () => {
valueRef.current = 42;
console.log(valueRef.current); // Logs 42 immediately
};
// useState (async update)
const [value, setValue] = useState(0);
const handleClick = () => {
setValue(42);
console.log(value); // Still logs 0
};

// useRef (sync update)
const valueRef = useRef(0);
const handleClick = () => {
valueRef.current = 42;
console.log(valueRef.current); // Logs 42 immediately
};
I wonder if something similar is happening under the hood? Maybe ctx.auth.getUserIdentity() needs some internal state sync or confirmation from Clerk from webhooks or an api call that hasn't completed by the time my mutation runs right after setActive? Clerk Webhooks: I looked into webhooks, but the Clerk docs explicitly state they aren't guaranteed to be immediate and shouldn't be relied upon for synchronous flows like user onboarding. As you can see here:
For example, if you are onboarding a new user, you can't rely on the webhook delivery as part of that flow. Typically the delivery will happen quickly, but it's not guaranteed to be delivered immediately or at all. Webhooks are best used for things like sending a notification or updating a database, but not for synchronous flows where you need to know the webhook was delivered before moving on to the next step.
So, that's not a viable option for ensuring the user record is created before proceeding. Timeout Workaround: I considered awaiting a setTimeout promise (e.g., 500ms) between setActive and initializeUser as a hacky workaround, but I'm sure there must be a better, more reliable way. The Question : How can I reliably ensure that after a successful await setActive(), the subsequent Convex mutation call can immediately access the user's identity via ctx.auth.getUserIdentity()? I understand this might be a relatively complicated specific issue related to the custom flow integration. Relying on timing feels risky. Really appreciate any insights, workarounds, or thoughts the community or the Convex/Clerk devs might have. Maybe there needs to be a tighter alignment between Clerk's setActive success and Convex's ctx.auth readiness, although I understand ctx.auth.getUserIdentity likely needs async data from Clerk, relies on state or an api call. Just spewing out ideas here, kinda stuck. 🙏
erquhart
erquhart2mo ago
I would create the user before step 3. This will result in incomplete user objects. As long as you can identify them as incomplete, you can exclude them from analytics and delete them via cron job as often as you like. Keep in mind there isn't a perfect solution here, so it's a question of which compromise is most acceptable. Having incomplete user records that are cleared up automatically feels a bit yuck, but is generally non-intrusive. It's a different class of compromise than risk of a user not being created or systems getting out of sync.

Did you find this page helpful?