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
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!
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:
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.
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. 🙏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.