
Stop Paying for Resend: BetterAuth + Nodemailer Integration Guide
How to replace Resend with Nodemailer in your Next.js application for free, unlimited email verification and password resets.
# Why I Ditched Resend for Nodemailer in BetterNotes
Let's be real: Resend is amazing until you hit that paywall. When you're building a project like BetterNotes, you want a seamless auth flow without the "onboarding@resend.dev" limitation or the monthly subscription just to send verification emails to your users.
If you are using BetterAuth with Next.js 15, switching to Nodemailer is a no-brainer. It gives you full control and lets you use your own SMTP server (like Gmail) for free.
## The Problem with Resend Free Tier
-
Limited Delivery: You are restricted to sending emails primarily to your own registered address.
-
Forced Branding: You have to deal with generic "onboarding" addresses instead of your own brand identity.
-
Scalability: For indie hackers, the jump from free to paid tiers can be a barrier to entry when managing a growing product from scratch.
## The Solution: A Custom SMTP Transporter
By creating a simple utility using Nodemailer, we can intercept BetterAuth's email events and route them through our own mailer. This setup is perfect for modern frontend apps that prioritize performance and cost-efficiency.
### 1. The Environment Setup (Gmail)
Before coding, you need to configure your credentials. You cannot use your regular Gmail password for security reasons; you must use an App Password.
- EMAIL_USER: This is simply your full Gmail address (e.g.,
yourname@gmail.com). - EMAIL_PASS: This is the 16-character App Password generated by Google.
#### How to get your App Password:
- Go to your Google Account settings.
- Navigate to Security.
- Enable 2-Step Verification (this is mandatory).
- Search for "App Passwords" in the search bar.
- Give it a name (like "BetterNotes") and copy the code provided.
### 2. Setup the Email Library
Create a utility file to handle the SMTP transport. This keeps your code clean and reusable across your application.
// lib/email.ts
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS, // The 16-char App Password
},
});
export async function sendEmail({
to,
subject,
html,
}: {
to: string;
subject: string;
html: string;
}) {
await transporter.sendMail({
from: `"BetterNotes" <${process.env.EMAIL_USER}>`,
to,
subject,
html,
});
}### 3. Connect Nodemailer to BetterAuth
Now, we need to tell BetterAuth to stop looking for Resend and start using our new sendEmail function. We use @react-email/components to render our templates into raw HTML strings.
// lib/auth.ts
import { betterAuth } from 'better-auth';
import { render } from '@react-email/components';
import { sendEmail } from '@/lib/email';
import VerificationEmail from '@/components/emails/VerificationEmail';
import ResetPasswordEmail from '@/components/emails/ResetPasswordEmail';
export const auth = betterAuth({
emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
const emailHTML = await render(
VerificationEmail({
name: user.name,
verificationUrl: url,
}),
);
await sendEmail({
to: user.email,
subject: 'Verify your email address',
html: emailHTML,
});
},
sendOnSignUp: true,
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
sendResetPassword: async ({ user, url }) => {
const emailHTML = await render(
ResetPasswordEmail({
name: user.name,
resetPasswordUrl: url,
}),
);
await sendEmail({
to: user.email,
subject: 'Reset your password',
html: emailHTML,
});
},
},
// ... rest of your config (drizzleAdapter, etc.)
});## Key Benefits of this Stack
-
Next.js 15 + TypeScript: Fully type-safe implementation ensures your user objects and URLs are never undefined during the build process.
-
BetterAuth Hooks: By using the
sendVerificationEmailandsendResetPasswordhooks, you bypass default providers entirely. -
Developer Experience (DX): You get the benefit of autocompletion and refactoring confidence that comes with a robust TypeScript setup.
## Real Use Cases
-
Verification: Ensure every user in your database is real before they access their data.
-
Security: Centralizing authorization logic where data is fetched to prevent accidental leaks.
-
Personal Branding: Sending from your own domain or professional Gmail without third-party tags.
## Conclusion
Switching from Resend to Nodemailer took me about 10 minutes, and it saved me from future subscription headaches. If you're building a production app but want to keep overhead low, this is the way to go.
Check out the live project at better-notes-og.vercel.app or dive into the code on GitHub.
Next Steps: Want to see how I used TypeScript with Drizzle or managed a side project from zero to deployment? Let me know on LinkedIn!

