#transactionnal sign in ?

3 messages · Page 1 of 1 (latest)

signal lodgeBOT
#

Hi @toxic rose. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

Great question about transactional behavior in Convex!

Yes, throwing the error will cancel everything — including the scheduled function.

Here's what happens in your beforeSessionCreation callback (which runs as a mutation):

  1. ctx.db.patch(...) — patches accountStatus to "SUSPENDED"
  2. ctx.scheduler.runAfter(0, ...) — schedules deleteAccount
  3. throw new Error(...) — throws an error

Because mutations in Convex are fully transactional, throwing an error rolls back all writes and all scheduled functions registered during that mutation. [Scheduling from Mutations]

Scheduling functions from mutations is atomic with the rest of the mutation. If the mutation fails, no function will be scheduled, even if the function fails after the scheduling call.

So in your code:

  • The patch to accountStatus: "SUSPENDED" will be rolled back
  • The deleteAccount scheduled function will never run
  • The sign-in will be rejected with the error message

This means your current code has a logic issue: you're patching and scheduling before throwing, but since the throw rolls everything back, neither the patch nor the scheduled deletion will take effect. The only thing that actually happens is the sign-in is blocked.

If you want the patch and deletion to happen, you should throw conditionally and only after deciding not to throw:

async beforeSessionCreation(ctx, { userId }) {
  const user = await ctx.db.get(userId);
  const existing = await ctx.db.query('usersExtended')
    .withIndex('by_user_id', (q) => q.eq('userId', userId)).first();
#

if (
user?.email &&
(user.email.includes('+') || /\d{8,}/.test(user.email))
) {
// These will be rolled back if you throw after — so only do this if you DON'T throw
if (existing) await ctx.db.patch(existing._id, { accountStatus: "SUSPENDED" });
await ctx.scheduler.runAfter(0, internal.router.users.deleteAccount._internal_delay, {
userId,
forceDelete: true,
});
// ⚠️ Throwing here rolls back the patch AND cancels the scheduled function!
throw new Error("This email address is not permitted.");
}
}


If you want both the suspension/deletion **and** the sign-in rejection, you'd need to handle the suspension/deletion in a separate mutation that commits independently before the throw — but that's a more complex flow. [[Mutation Transactions](<https://docs.convex.dev/functions/mutation-functions#transactions>)]
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------