I’m a junior backend developer working with Node.js.
While working on my app, I made a typo in process.env and noticed that a user was still being created even though the token was not generated successfully.
I understand why this happened technically, but from a business-logic perspective, this flow doesn’t feel correct. A user should not be created if their token generation fails.
After some research, I found that one possible solution is to use a database transaction to ensure that both operations creating the user and generating the token either succeed together or fail together.
This way, the signup process remains consistent and reliable.
My question is: Is using a transaction the best approach for this case, or are there other recommended patterns or solutions that I might be missing?
The core issue is that your application logic is tightly coupled with the identity provider. In a standard Auth0 flow, the “Source of Truth” for an identity should ideally be Auth0 itself.
If you create a user in your local Mongoose DB first and then call Auth0, you risk “Orphaned Users” (local user exists, but can’t log in). If you call Auth0 first and then your DB fails, you get “Ghost Identities” (user can log in, but your app has no record of them).
Instead of creating the user in your DB during the signup request, allow Auth0 to handle the signup entirely.
If you have any further questions, don’t hesitate to reach out.
Hi @vlad.murarasu, thanks for your reply, I really appreciate it.
Just to clarify, I’m not using Auth0 or any external identity provider.
My setup is purely Express + Mongoose + JWT, so the application itself is responsible for user creation and token generation.
In my case, the issue happens because a failure during JWT generation (for example, due to a misconfigured environment variable) can occur after the user document has already been created in MongoDB, which leads to an inconsistent state.
exports.signup = catchAsync(async (req, res, next) => {
// Start the transaction session
const session = await mongoose.startSession();
session.startTransaction();
try {
// Create the user
const newUser = await User.create(
[
{
name: req.body.name,
email: req.body.email,
phone: req.body.phone,
password: req.body.password,
passwordConfirm: req.body.passwordConfirm,
},
],
{ session },
);
// If JWT fails here, transaction rolls back the user creation (user not created)
const token = siginToken(newUser[0]._id);
await session.commitTransaction();
session.endSession();
// Remove password from the res
newUser[0].password = undefined;
// Res
res.status(201).json({
status: 'success',
token,
data: { user: newUser[0] },
});
} catch (err) {
await session.abortTransaction();
session.endSession();
throw err;
} finally {
session.endSession();
}
});
If you have any recommendations specific to this setup (without an external identity provider), I’d love to hear your thoughts.
In your current code, the “failure” (the typo in process.env) is a configuration error, not a transient database error. Using a database transaction to catch a code-level configuration error adds significant overhead (locking resources in MongoDB) for a scenario that should be caught during development or app startup.
Furthermore, JWT generation is a synchronous, CPU-bound task that doesn’t actually interact with the database. If the JWT generation fails, the “damage” is simply that the user exists in the DB but didn’t get their first token—they can still log in immediately afterward because their account was successfully created.
The best approach for your specific setup is to prioritize pre-validation and reordering logic rather than relying on heavy database transactions.
1. Fail Fast (Configuration Validation)
Check that your environment variables exist when the server starts, not when a user signs up. If process.env.JWT_SECRET is missing, the app should crash immediately on startup so you never hit this bug in production.
2. Reorder Operations
You don’t need a transaction if you validate everything before the write operation.
exports.signup = catchAsync(async (req, res, next) => {
// 1. Validate the secret exists (Defensive programming)
if (!process.env.JWT_SECRET) {
return next(new AppError('JWT Secret is not configured', 500));
}
// 2. Create the user (Standard Mongoose call, no transaction needed)
const newUser = await User.create({
name: req.body.name,
email: req.body.email,
password: req.body.password,
passwordConfirm: req.body.passwordConfirm,
});
// 3. Generate token
// Since we checked the secret above, this is virtually guaranteed to succeed
// unless there is a system-level failure.
const token = signToken(newUser._id);
// 4. Send response
newUser.password = undefined;
res.status(201).json({
status: 'success',
token,
data: { user: newUser },
});
});