In this blog, we'll dive into setting up email verification in a modern web application. By leveraging a robust stack of tools and frameworks, you'll create a seamless and secure email verification system integrated into your full-stack application.
By the end of this guide, you'll have a functional system that validates users via email before granting access to their accounts.
Tech Stack
Here’s what we’ll use:
- Better_Auth v1: A lightweight, extensible TypeScript authentication library.
- Next.js: A React framework for building server-rendered applications.
- Prisma: A modern ORM for efficient database interaction.
- Shadcn: A utility-first component library for rapid UI development.
- TailwindCSS: A popular utility-first CSS framework.
Prerequisites
Before proceeding, ensure you have the following ready:
- Node.js (LTS version) installed.
- A package manager like npm, yarn, or pnpm (we'll use
pnpm
in this guide). - A PostgreSQL database instance (local or hosted, such as Supabase or PlanetScale).
- If you're working locally, Docker is a great way to set this up.
- Familiarity with TypeScript, Next.js, and Prisma.
Note:
This blog is the continuation of Email and Password Auth If you want to follow along please first visit: Email and Password Auth using Better_Auth or you could clone the branch of the repository from the command below to follow along.
Clone Repo:
git clone -b feat-email-password-auth https://github.com/Daanish2003/better_auth_nextjs.git
Install Dependencies:
pnpm install
Run the docker if you have docker locally:
docker compose up -d
Step 1: Setting up Resend
service
Resend is a service for sending transactional emails like password resets and email verifications.
Install the Resend SDK
Add the Resend package to your project:
pnpm add resend
Get Your Resend API Key
- Visit Resend.
- Log in or create an account.
- Navigate to API Keys and create a new key.
- Add the API key to your
.env
file:
RESEND_API_KEY=your-api-key-here
Create a Helper for Resend
Organize your code by creating a reusable helper for Resend.
- Create a folder
src/helpers/email/
. - Add a file named
resend.ts
:
import { Resend } from "resend";
export const resend = new Resend(process.env.RESEND_API_KEY);
Step 2: Integrate Email verification with Better_Auth
Better_Auth provides native support for email verification.
Update the auth.ts
File
Open src/lib/auth.ts
and configure email verification:
// auth.ts
import prisma from "@/db";
import { resend } from "@/helpers/email/resend";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
export const auth = betterAuth({
appName: "better_auth_nextjs",
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
emailAndPassword: {
enabled: true,
autoSignIn: true,
minPasswordLength: 8,
maxPasswordLength: 20,
requireEmailVerification: true, //It does not allow user to login without email verification [!code highlight]
},
emailVerification: {
sendOnSignUp: true, // Automatically sends a verification email at signup
autoSignInAfterVerification: true, // Automatically signIn the user after verification
sendVerificationEmail: async ({ user, url }) => {
await resend.emails.send({
from: "Acme <onboarding@resend.dev>", // You could add your custom domain
to: user.email, // email of the user to want to end
subject: "Email Verification", // Main subject of the email
html: `Click the link to verify your email: ${url}`, // Content of the email
// you could also use "React:" option for sending the email template and there content to user
});
},
},
// ignore this if your not adding OAuth
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectURI: process.env.BETTER_AUTH_URL + "/api/auth/callback/google",
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
redirectURI: process.env.BETTER_AUTH_URL + "/api/auth/callback/github",
},
},
});
Explanation:
-
sendVerificationEmail
: This function is triggered when email verification starts. It accepts a data object with the following properties:-
user
: The user object containing the email address. -
url
: The verification URL the user must click to verify their email.
-
Step 3: Update UI Components
To handle the email verification workflow, we’ll update the sign-in and sign-up components.
Update the SignIn
Component
Navigate to src/components/auth/sign-in.tsx
and use this code:
// components/auth/sign-in.tsx
"use client"
import React from 'react'
import CardWrapper from '../card-wrapper'
import FormError from '../form-error'
import { FormSuccess } from '../form-success'
import { FcGoogle } from 'react-icons/fc'
import SocialButton from './social-button'
import { FaGithub } from 'react-icons/fa'
import { useAuthState } from '@/hooks/useAuthState'
import LoginSchema from '@/helpers/zod/login-schema'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'
import { Input } from '../ui/input'
import { Button } from '../ui/button'
import { signIn } from '@/lib/auth-client'
import { useRouter } from 'next/navigation'
const SignIn = () => {
const router = useRouter()
const { error, success, loading, setSuccess, setError, setLoading, resetState } = useAuthState();
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: zodResolver(LoginSchema),
defaultValues: {
email: '',
password: '',
}
})
const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
try {
await signIn.email({
email: values.email,
password: values.password
}, {
onResponse: () => {
setLoading(false)
},
onRequest: () => {
resetState()
setLoading(true)
},
onSuccess: () => {
setSuccess("LoggedIn successfully")
router.replace('/')
},
// changes were made on onError option
onError: (ctx) => {
/* Whenever user tried to signin but email is not verified it catches the error and display the error */
if(ctx.error.status === 403) {
setError("Please verify your email address")
}
/* handles other error */
setError(ctx.error.message);
},
});
} catch (error) {
console.log(error)
setError("Something went wrong")
}
}
return (
<CardWrapper
cardTitle='Sign In'
cardDescription='Enter your email below to login to your account'
cardFooterDescription="Don't have an account?"
cardFooterLink='/signup'
cardFooterLinkTitle='Sign up'
>
<Form {...form}>
<form className='space-y-4' onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
disabled={loading}
type="email"
placeholder='example@gmail.com'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
disabled={loading}
type="password"
placeholder='********'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormError message={error} />
<FormSuccess message={success} />
// ignore this if your not adding OAuth into your app
<Button disabled={loading} type="submit" className='w-full'>Login</Button>
<div className='flex gap-x-2'>
<SocialButton provider="google" icon={<FcGoogle />} label="Sign in with Google" />
<SocialButton provider="github" icon={<FaGithub />} label="Sign in with GitHub" />
</div>
</form>
</Form>
</CardWrapper>
)
}
export default SignIn
Update the SignUp
Component
Similarly, update src/components/auth/sign-up.tsx
:
// components/auth/sign-up.tsx
"use client"
import React from 'react'
import CardWrapper from '../card-wrapper'
import FormError from '../form-error'
import { FormSuccess } from '../form-success'
import { FcGoogle } from 'react-icons/fc'
import SocialButton from './social-button'
import { FaGithub } from 'react-icons/fa'
import { useAuthState } from '@/hooks/useAuthState'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'
import { Input } from '../ui/input'
import { Button } from '../ui/button'
import { SignupSchema } from '@/helpers/zod/signup-schema'
import { signUp } from '@/lib/auth-client'
const SignUp = () => {
const { error, success, loading, setLoading, setError, setSuccess, resetState } = useAuthState();
const form = useForm<z.infer<typeof SignupSchema>>({
resolver: zodResolver(SignupSchema),
defaultValues: {
name: '',
email: '',
password: '',
}
})
const onSubmit = async (values: z.infer<typeof SignupSchema>) => {
try {
await signUp.email({
name: values.name,
email: values.email,
password: values.password,
callbackURL:'/' // redirect the user after email is verified
}, {
onResponse: () => {
setLoading(false)
},
onRequest: () => {
resetState()
setLoading(true)
},
onSuccess: () => {
// setSuccess("User has been created")
// router.replace('/')
setSuccess("Verification link has been sent to your mail")
},
onError: (ctx) => {
setError(ctx.error.message);
},
});
} catch (error) {
console.error(error)
setError("Something went wrong")
}
}
return (
<CardWrapper
cardTitle='SignUp'
cardDescription='Create an new account'
cardFooterLink='/login'
cardFooterDescription='Already have an account?'
cardFooterLinkTitle='Login'
>
<Form {...form}>
<form className='space-y-4' onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
disabled={loading}
type="text"
placeholder='john'
{...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
disabled={loading}
type="email"
placeholder='example@gmail.com'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
disabled={loading}
type="password"
placeholder='********'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormError message={error} />
<FormSuccess message={success} />
//ignore this if your not adding Oauth
<Button disabled={loading} type="submit" className='w-full'>Submit</Button>
<div className='flex gap-x-2'>
<SocialButton provider="google" icon={<FcGoogle />} label="Sign in with Google" />
<SocialButton provider="github" icon={<FaGithub />} label="Sign in with GitHub" />
</div>
</form>
</Form>
</CardWrapper>
)
}
export default SignUp
Step 4: Run Your Application
Start your development server:
pnpm dev
- Access your application at
http://localhost:3000/signup
to test sign-up. - Check your inbox for the verification email.
- Log in at
http://localhost:3000/signin
once verified.
Final Thoughts
Congratulations! You have successfully integrated Email verification into your application, providing users with a seamless and secure authentication experience. 🎉
If you enjoyed this blog, please consider following and supporting me! Your encouragement motivates me to create more helpful content for you. 😊
Reference Links:
Email And Password with Better_Auth: https://dev.to/daanish2003/email-and-password-auth-using-betterauth-nextjs-prisma-shadcn-and-tailwindcss-hgc
OAuth Blog: https://dev.to/daanish2003/oauth-using-betterauth-nextjs-prisma-shadcn-and-tailwindcss-45bp
Better_Auth Docs: https://www.better-auth.com/
pnpm Docs: https://pnpm.io/
Docker Docs: https://docs.docker.com/
Prisma Docs: https://www.prisma.io/docs/getting-started
Shadcn Docs: https://ui.shadcn.com/
Next.js Docs: https://nextjs.org/
Tailwindcss Docs: https://tailwindcss.com/
Github repository: https://github.com/Daanish2003/better_auth_nextjs
Top comments (0)