Clerk + Convex: Can't use fetch() in queries and mutations. Please consider using an action
Inside my convex folder, i have a mutation that is creating a user, and if this fails, I want to delete the user from Clerk.
I cant use
useAction
inside this context, so I tried importing it directly which is seemingly okay.
I'm using an action as logs suggested, but obviously i'm misunderstanding something.
// somewhere inside convex, inside a mutation
// convex/clerk.js
Error message:
32 Replies
mutations cannot call actions directly, because they are deterministic and cannot have side effects. You can schedule the action to be run later https://docs.convex.dev/scheduling/scheduled-functions
Scheduled Functions | Convex Developer Hub
Convex allows you to schedule functions to run in the future. This allows you to
is this the only way?
I'm concerned that this approach is more of a hack rather than best practice.
does my use-case make sense? I may have made it confusing.
i suppose i could call fetch api directly but would love to hear your thoughts on the matter
You can choose how to separate the code that is transactional and writes to the database, from the code that could have transient errors and has side-effects. You can schedule the action to run as a scheduled function, but then it might not succeed. You could wrap it in the actions-retrier to improve reliability. Or you could have a cron job that periodically cleans up user state.
Oh actually there is a bigger problem. If you end up throwing an error, the entire mutation will rollback. Therefore you can't schedule anything
Can you describe more about what you're trying to accomplish with the clerk cleanup?
My auth flow is:
1. create user with clerk
2. once clerk user is confirmed, create user in db
3. If db transaction fails, rely on convex transaction guarantee to rollback. Additionally, at this point, we delete the user from clerk
To delete the clerk user, i need to invoke (at least im assuming from clerk docs)
await clerkClient.users.deleteUser(clerkUserId);
This method comes from import { clerkClient } from "@clerk/nextjs/server";
This seems like a difficult problem. What if instead of deleting the clerk user, you write the convex function so that it won't fail?
you mean like, re-invoking the db operations until success?
Mutations don't fail due to transient errors. The only kinds of failures are deterministic, like if the document doesn't match the schema, and you can prevent this in code
Alternatively, from the client you could do
Which is also slightly weird because if the mutation fails it's likely the action will also fail
ah i see what you mean. in that sense, my mutations should be safe.
i just need to handle the deletion of the clerk user, but leaning into the fact that my mutations should be safe at a fundamental level, it should be safe to not implement a fail-safe measure like deleting the clerk user directly.
hmm
Sg. For handling deletion of clerk user, i would do if with clerk webhooks
im using nextjs. what about calling an api route, and handling the deletion from there?
also, will convex be roling out their own auth service
Sorry i'm not super familiar with auth flows. What do you mean by "handle the deletion of a clerk user"?
probably. we're discussing internally
@zid in a sense doing scheduling a Convex action to do the deletion (scheduling can run in less than 1s after it is scheduled, think milliseconds) is safer than a Next API endpoint because you can retry it if it fails
oh, i just mean (and my apologies if im making this unnecessarily confusing), i need to delete the user that gets created in clerks database because a user's account has been created with an assocated email for example. I need to clean that up
hmm i see, let me look at docs
The Next.js API-style way to do this with Convex is
- invoke a Convex action from the client, which
1. runs a mutation which deletes the user from Convex
2. does a fetch call to delete it from Clerk
and that works totally fine! We suggest clients running mutations and scheduling the action because the mutation will not fail like an action can, if e.g. Clerk's service happens to be down when the user clicks the button.
But to get "normal" levels of reliability, you can just do both things in one Convex action.
To get really "Convexy" levels of reliability, you run a mutation does the Convex user deletion which schedules an action to take care of the Clerk side. If that action does fail, you have Convex retry logic or you can record that it failed and deal with it once an hour with a cron job or you can just see that failure in the logs and do it manually
btw this may help https://stack.convex.dev/retry-actions
Automatically Retry Actions
Learn how to automatically retry actions in Convex while also learning a little about scheduling, system tables, and function references.
okay so it seems like calling scheduled action inside mutation is supported natively and the api looks something like
await ctx.scheduler.runAfter
touching on the conversation with Lee, it seems like there shouldnt be a concern regarding a mutation failing (assuming the code is solid and tested, of course).
in my setup, if db opreations do fail, ill simply be relying on the transactional guarantee where no operations will succeed, hence all i need to take care of is deleting the clerk user in a catch block. From here, it seems like scheduled action could be the best route forwardyep the scheduling of the function is part of this transaction, it will also be "rolled back" (not scheduled) if something fails
"scheduled" might be a confusing term here, since you frequently "schedule" something to happen immediately — here scheduled might just means the transaction will complete before this runs. So if the fetch() call does fail, that type of failure has to be dealt with differently than tha automatic Convex mutation transaction stuff.
Im getting a little confused (sorry lol)
I dont get how
deleteUserFromClerkWithAScheduledFn
would roll back as its outside the scope of any db operations during the creation of the user
in other words, separate transactions, separate queues?Where is this code?
I think you're understanding everything right, you have two options
- Run everything from a Convex Action. Do these in either order. I'd write this as
* Run things from a mutation
If you do the second and you're worried the Clerk delete might fail, then you should retry that action, like the https://stack.convex.dev/retry-actions link Lee posted.
Or you could put the retry in the action like
If you don't want to delete the user in Convex until after the Clerk thing is successful, then do them in that order
I think our starting points are a bit different.
The starting point for me is that the transaction (batch of writes) when attempting to create the user in convex has failed, and thus nothing has been created in convex, so nothing to delete. The only thing that happened was an error.
It's this error, and its respective catch block, where I attempt to delete the user in Clerk's database, as this was created first.
Ah so this isn't the user clicking the "delete me" button, it's the user clicking a "sign up" button but something about the Clerk claims makes the Convex user creation flow fail?
sorry I should have read more of this thread
nono, no worries at all, much appreciated
so
1. user gets created successfully in Clerk
2. my app takes the newly created clerk's userId and attempts to create a user in convex
3. Its step 2 that i want to create a fail-safe measure
Similarly you can run this as an action that calls a mutation that creates the user (if this fails, go delete the user in clerk) or a mutation that schedules an action if some condition is true
from lees conversation, one of the main conclusions was that a mutation failing is virtually impossible, provided its not a deterministic error
so this might be a bit overkill, but yea
Are there many ways you're worried about the user creation failing that you can't anticipate? I'd use if statements to decide if the user creation ought to succeed or not, and schedule the function to delete the user if not.
But I think you've correctly identified that you can't both schedule a function AND use the automatic Convex "rollback" (not applying the transaction) when an error is thrown — this could be real shortcoming in some cases where you'd need to use an Action to catch the error and decide what to do
from lees conversation, one of the main conclusions was that a mutation failing is virtually impossible, provided its not a deterministic errorYes So I agree you don't need this, just use if statements to decide whether you want to create this user or not instead of saying "any error that happens, programmer error or validation error or the clerk users belongs to the wrong team, throw an error" But if you do want to delete the clerk user no matter what in any error case, they I'd use the action approach. Currently throwing errors as control flow in Convex is limited in this way, there's only one "level" of this: everything will be rolled back in a mutation (including the scheduling of your clerk deletion fetch action), not just a specific section of code.
i think we're on the same page, lol
i guess to really understand at the level i want to, i would need to understand
everything will be rolled back in a mutation, not a specific section of code.
more deeply. Just more into the mechanics of how that works at a lower level. To be clear, im not asking this of you haha. i understand enough to move forward.
Thanks a bunch.it's not too bad, just every time you run a mutation in convex we don't actually apply persist the changes to the database until the end (but it looks to the function running like they're applied).
If an error is thrown (and not caught, I mean if an error propagates all the way up), we don't apply these changes to the database.
oh okay, right
i think i forgot that these operations are happening all underneath a single mutation/fn
I'd say that what you want is an explicit transaction boundary, or a subtransaction. Today we do these by running an action: an action can call as many mutations as it wants, but each one is its own transaction. Once one of these mutations finishes you can't roll that one back, so you lose some of the magic of putting related code all together.
that makes sense.
although, earlier i tried to create an action for the fail-safe delete clerk fn but ran into some issue regarding where and how actions can be called. I'll have to read the docs.
perhaps you tried to call it directly, you can only schedule actions from mutations in Convex
yea,that was it lol
ah, alrighty, thanks again @ballingt @lee .
Super helpful.