DEV Community

Cover image for Building OTP based authentication system with Clerk and Autosend
haimantika mitra
haimantika mitra

Posted on

Building OTP based authentication system with Clerk and Autosend

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:

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
  }
};
Enter fullscreen mode Exit fullscreen mode

What's happening under the hood

  1. Clerk's signUp.create() creates a user account in their system
  2. The account is created but not yet verified
  3. A POST request is sent to the /api/send-verification endpoint
  4. The UI switches to show the verification input screen

Important configuration

Disable Clerk’s built-in email verification:

  1. Go to Clerk Dashboard
  2. User & Authentication → Email, Phone, Username
  3. Turn OFF “Verify at sign-up”
  4. 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();
}
Enter fullscreen mode Exit fullscreen mode

Storage with expiration

const code = generateCode();

verificationCodes.set(email, {
  code,
  expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes
});
Enter fullscreen mode Exit fullscreen mode

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",
});
Enter fullscreen mode Exit fullscreen mode

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,
  });
}
Enter fullscreen mode Exit fullscreen mode

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,
  }),
});
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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",
  });
}
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

Creating the session

if (signUp.status === "complete" && signUp.createdSessionId) {
  await setActive({ session: signUp.createdSessionId });

  setIsRedirecting(true);
  setError("");
  setLoading(false);

  setTimeout(() => {
    window.location.href = "/";
  }, 1000);
}
Enter fullscreen mode Exit fullscreen mode

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)