Vibe-coding has got us thinking more about security. Earlier this year, we saw Leo’s SaaS under attack, it was experiencing bypassed subscriptions, abused API keys, and database corruption.
Big tech giants are focusing a lot on building applications that are AI+security first. As developers, every solution that we build now needs security to be built into the design phase. The first step to any application (SaaS or not) is authentication, and companies like Clerk, Auth0, etc., have already made it easier for us to build authentication systems that are secure.
While you can also use their pre-built solutions, you might also need to plug and play custom solutions, and this is exactly what we will learn in this blog: how we can use Clerk’s authentication system with Autosend to build a secure authentication system.
To follow this article end-to-end, you will need:
- Working knowledge of Next.js
- A Clerk account
- An Autosend account
What is Autosend?
I recently discovered Autosend on X and thought about playing around with it over the weekend. By definition, Autosend is an email platform for developers and marketers to send and track transactional and marketing emails. If you have used other platforms like Resend and Twilio, it does compare with them. What stood out for me is the developer experience; the product is easy to use with a clear developer journey outlining the steps to follow to send a successful email, and the documentation is very easy to follow as well. Although it does not have a free account, the paid tier for someone like us starts from $1, where you can send up to 3000 emails, which is more than enough for people like us building hobby projects.
Integration flow
To build this entire thing wouldn’t take you more than an hour. To make things easy, we are using Clerk’s Next.js starter guide (you can also copy the prompt and get started faster in your AI IDE of choice/use the Cursor button) and then replace the email verification part with Autosend.
In this implementation, Clerk manages user accounts and sessions, while Autosend handles email delivery for verification codes. Unlike Clerk's built-in email verification, we generate and validate our own codes, giving us complete control over the email content, delivery, and user experience.
Step-by-Step Process: Building the Authentication Flow
The authentication flow consists of six main stages: User Registration, Code Generation, Email Delivery, Code Verification, Validation, and Session Creation. Let's break down each step to understand how Clerk and Autosend work together to create a secure authentication experience.
Step 1: User initiates signup
When a user visits the /signup page and submits their credentials, the process begins.
The signup page (app/signup/page.tsx) presents a beautiful gradient-styled form where users enter their email and password. When they click "Sign Up", the handleSubmit function is triggered:
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Create the sign-up with Clerk
await signUp.create({
emailAddress: email,
password,
});
// Send verification code via Autosend
const response = await fetch("/api/send-verification", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
if (response.ok) {
setPendingVerification(true); // Show code input screen
}
};
What's happening under the hood
- Clerk's
signUp.create()creates a user account in their system - The account is created but not yet verified
- A POST request is sent to the
/api/send-verificationendpoint - The UI switches to show the verification input screen
Important configuration
Disable Clerk’s built-in email verification:
- Go to Clerk Dashboard
- User & Authentication → Email, Phone, Username
- Turn OFF “Verify at sign-up”
- Save changes
This allows verification to be handled by Autosend instead of Clerk.
Step 2: Backend generates verification code
The /api/send-verification endpoint generates and stores a verification code.
Code generation
function generateCode(): string {
return Math.floor(100000 + Math.random() * 900000).toString();
}
Storage with expiration
const code = generateCode();
verificationCodes.set(email, {
code,
expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes
});
Triggering email delivery
const result = await sendVerificationEmail(email, code);
if (!result.success) {
verificationCodes.delete(email);
return NextResponse.json({ error: result.error }, { status: 500 });
}
return NextResponse.json({
success: true,
message: "Verification code sent via Autosend",
});
Step 3: Autosend delivers the email
The email is constructed and sent through Autosend’s API.
Email construction
export async function sendVerificationEmail(
email: string,
code: string,
name?: string
): Promise<AutosendResponse> {
return sendEmail({
to: { email, name },
subject: `Your verification code: ${code}`,
html: htmlTemplate,
text: plainTextVersion,
});
}
API call
const response = await fetch("https://api.autosend.com/v1/mails/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AUTOSEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: {
email: process.env.AUTOSEND_FROM_EMAIL,
name: process.env.AUTOSEND_FROM_NAME || "Authentication"
},
to: { email },
subject: `Your verification code: ${code}`,
html: options.html,
text: options.text,
}),
});
Step 4: User enters verification code
The user enters the 6-digit code in the UI.
Verification screen
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800">
📧 We've sent a verification code to <strong>{email}</strong> via Autosend.
</p>
</div>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter 6-digit code"
maxLength={6}
className="text-center text-2xl font-mono tracking-widest"
/>
Step 5: Backend validates the code
The /api/verify-code endpoint performs strict validation.
Validation logic
export async function POST(req: NextRequest) {
const { email, code } = await req.json();
const stored = verificationCodes.get(email);
if (!stored) {
return NextResponse.json(
{ success: false, error: "No verification code found. Please request a new one." },
{ status: 400 }
);
}
if (stored.expiresAt < Date.now()) {
verificationCodes.delete(email);
return NextResponse.json(
{ success: false, error: "Verification code has expired. Please request a new one." },
{ status: 400 }
);
}
if (stored.code !== code) {
return NextResponse.json(
{ success: false, error: "Invalid verification code. Please try again." },
{ status: 400 }
);
}
verificationCodes.delete(email);
return NextResponse.json({
success: true,
message: "Email verified successfully",
});
}
Step 6: Complete signup and create session
After verification, Clerk finalizes signup and creates the user session.
Checking status
console.log(`SignUp status: ${signUp.status}`);
console.log(`Created session ID: ${signUp.createdSessionId}`);
Creating the session
if (signUp.status === "complete" && signUp.createdSessionId) {
await setActive({ session: signUp.createdSessionId });
setIsRedirecting(true);
setError("");
setLoading(false);
setTimeout(() => {
window.location.href = "/";
}, 1000);
}
And finally, users see a success screen and are then redirected to the homepage fully authenticated.
Complete flow visualization
A visual diagram showing how Clerk, the API routes, Autosend, and the frontend connect together to build the entire authentication system.
Ending Notes
And that’s how you can easily build a fully functional and customizable authentication flow for your applications.
A few things to keep in mind as you build on top of this:
- Security enhancements: Consider adding rate limiting to prevent abuse, you don't want someone spamming your verification endpoint. Also, think about implementing attempt limits (maybe 3-5 tries) before requiring a new code.
- User experience: The 10-minute expiration works well, but you might want to add a "Resend code" button with a cooldown timer. Users appreciate this when emails take longer to arrive or they accidentally delete them.
This pattern isn't limited to just signup flows either. You can use the same Clerk + Autosend combination for password resets, or magic link authentication. The beauty is that once you understand how these pieces fit together, you can adapt them to whatever authentication pattern your application needs.
If you are interested to learn more about authentication,email platforms, and 2FA here are some helpful resources:





Top comments (0)