A verification email is a message carrying a single-use, expiring token that proves the person signing up controls the address they typed. How to send verification email from your own app: generate a random token, store only its hash, email a link or a 6-digit code (my example) from your own domain, expire it and accept it once.

What is email verification: what a verification email proves, and what it does not

Email verification proves one thing: the person at the keyboard can read mail sent to the address they typed. The app sends a secret, the person returns it, and the account is marked verified with a timestamp. Checking whether a mailbox exists is a different job, done by address-checking tools.

Of everything that goes into how to improve email deliverability, this message is where a failure costs you a customer first, because the person is sitting at the screen waiting for it.

Two jobs share the same words. Ownership verification is the first: your app sends a message carrying a secret to the address, and the person proves control by sending the secret back through a link or a code. A verified email address is one where that round trip happened, and the app records it on the user row with a timestamp. Address checking is the second: a tool tests the syntax, the domain’s mail records and sometimes the mailbox, with no action from the owner. It suggests the mailbox exists and says nothing about who reads it.

Meaning of “verify an email”Who does itWhat it provesWhere it is covered
Ownership verificationYour app or its auth provider, by sending a secret to the addressThe person can read mail at that address, at that momentThe rest of this page
Address checkingAn address-checking tool, with no action from the ownerThe address is well formed and its mailbox probably existsDeliverability testing, separate from sign-up

On a signup screen, “verify your email” means the first job. Working out how to verify emails already sitting in a list is the second, and the tools for it belong with how to test email deliverability. When a screen says your email needs verification, the account exists and stays limited until the link or code comes back.

Neither job proves identity. Auth0’s documentation says it “does not recommend using an email address as a way to validate that a user is who they say they are.” A verified address is also not a promise for next year: people change jobs and abandon inboxes, so verification is best seen as a fact about one moment.

The password reset email runs on the same mechanism with higher stakes. A verification token sets a flag on the account; a reset token lets whoever holds it take the account over.

Why it matters: the one email every signup waits on

It is the first message a new customer gets from you, and it blocks everything after it. When a verification email does not arrive, the signup is lost and no dashboard turns red: the person closes the tab and the user table keeps a half-made account.

Unverified addresses cost you later too. A mistyped address that nobody verified turns into a hard bounce the next time you send to it, and bounces eat into the sending reputation every other message depends on; an email bounce handling checklist is where those get handled. A signup form with no verification also lets bots and typos fill the user table, and blocking bots submitting our signup form is the form-side control that works next to it.

The reset email carries the sharpest risk. A reset token that can be guessed, reused or stays valid for days turns the reset email into an account takeover path. The reset flow’s own rules, from consistent responses to single-use tokens, sit with authentication best practices; the sending side is what this page covers.

Whether your flow has any of these gaps is what the seven checks further down decide.

How to send verification email: the flow, part by part

Sending a verification email has 6 parts: a trigger, a random token stored as a hash, a message with a link or code, a transactional provider on your own domain, an endpoint that accepts the token once, and a rule for what unverified accounts may do.

The rules below come from OWASP’s Forgot Password Cheat Sheet and from the Firebase, Auth0 and Supabase documentation, read in October 2026. The code is an illustration to review your own flow against, not a library to paste in.

How to set up email verification: the six moving parts

Email verification is set up in 6 parts, and managed auth such as Supabase, Firebase or Auth0 sends the message and handles the token. The parts that stay yours are the sender domain, the template, the redirect URL and the sending limits, and those are where production flows tend to fail.

PartWhat it doesWho provides it on managed authWhat you still own
1. TriggerStarts a send on signup, on an email change, or when the person presses resendAuth0 sends on signup by default; Firebase sends when your code calls sendEmailVerification; Supabase sends its confirmation templateThe resend button, its cooldown, and the email-change path
2. TokenA random value, stored as a hash with the user id, the purpose and the expiryGenerated and stored by the platformNothing, unless you build the flow yourself
3. MessageA template carrying a link or a codeA default template (Auth0’s built-in provider allows no custom templates)The wording, the sender name and the language
4. SenderA transactional provider such as Resend or Amazon SES, on your authenticated domainA default sender; Auth0 and Supabase document theirs as not meant for productionYour domain, your provider account and its limits
5. EndpointAccepts the token once and marks the address verifiedThe platform’s verify link or code checkThe redirect URL the person lands on
6. StateRecords the verification and decides what an unverified account may doSets the flag (email_verified in Auth0 and in Firebase’s rules)What your rules, backend and screens allow before the flag is set

The email verification process has the same six parts whether you build it or rent it; for a builder-made app, the third column is the one to read twice. Every flow starts the same way, with the app having to send an email to verify the address before it trusts it. The trigger is the part people forget when they plan how to send a verification email: signup is obvious, but an email change and a resend button need the same flow.

Part 4 is where I have a firm rule: never send from the app server’s own mail command or from a personal Gmail account. A transactional provider on your own domain keeps a log of every message, and the failure section below leans on that log. Choosing the provider, and tracking what it delivers, falls under transactional email best practices. What the message says is a separate job again, and starting from an email verification email template saves writing it from nothing.

Whichever way you do email verification, decide one more thing before launch: what an unverified account may do.

  • Nothing at all keeps the account at a “check your inbox” screen. It is the safest choice, and a person who mistyped the address is stuck until they fix it.
  • Read-only access lets them look around but not create, invite or pay. Signup keeps moving, and an address the person does not own buys very little.
  • Full access for a grace period gives the smoothest start, and for that window anyone can hold a working account on someone else’s address.

The verification endpoint in code: token, single use, expiry

A verification endpoint issues a random token, stores only its hash with the user, the purpose and the expiry, and accepts it once. In my rules, each new token cancels the older ones and the token never reaches logs. A landing page with a button keeps mail scanners from using the link up.

The token rules come from OWASP’s Forgot Password Cheat Sheet: tokens “Randomly generated using a cryptographically safe algorithm”, “Stored in a secure manner”, “Invalidated after they have been used” and set to “expire after an appropriate period.” Those rules, with the reset flow around them, are part of authentication best practices. What follows is the part they leave to you: what the endpoint does with the token.

Below is an email verification script for review, in plain Node: one function issues a token, the other consumes it.

// Illustration for review, not a library. `db` stands in for your data layer.
import { randomBytes, createHash } from 'node:crypto';

const EXPIRY_MS = 30 * 60 * 1000; // example value: 30 minutes, set your own
const sha256 = (value) => createHash('sha256').update(value).digest('hex');

// Issue: called on signup, email change or resend.
export async function issueToken(db, userId, purpose) {
  const token = randomBytes(32).toString('hex'); // example size: 32 random bytes
  await db.tokens.deleteMany({ userId, purpose }); // a new token cancels older ones
  await db.tokens.insert({ hash: sha256(token), userId, purpose, expiresAt: Date.now() + EXPIRY_MS });
  return token; // goes into the email link only, never into a log
}

// Consume: called by the landing page's POST, never by a bare GET.
export async function consumeToken(db, token, purpose) {
  const row = await db.tokens.findOne({ hash: sha256(token), purpose });
  if (!row || row.expiresAt < Date.now()) return null; // one answer for unknown and expired
  const removed = await db.tokens.deleteOne({ hash: row.hash }); // single use
  if (removed.count !== 1) return null; // a parallel request used it first
  return row.userId; // the caller marks the address verified, or allows the reset
}

Only a hash of the token is stored, so a read of the tokens table yields no working links. Node’s randomBytes “Generates cryptographically strong pseudorandom data”, and createHash turns the token into the digest that gets stored. The token also carries a purpose (verify, reset, change-email), so a verify token can never reset a password. Issuing a new token deletes the older ones for that purpose, which is my rule: only the newest email works.

Keep the token out of logs, analytics URLs and the referrer. After use, the landing page strips it from the address bar, and OWASP asks for a no-referrer Referrer Policy on the reset page “in order to avoid referrer leakage.” A code in place of a link fits the same two functions, with one addition: a limit on wrong attempts, which belongs to the code rules in authentication best practices.

Then there is the link scanner. Microsoft’s Safe Links documentation says that with protection on, “URLs are scanned prior to message delivery”, and that “URLs that don’t have a valid reputation are detonated asynchronously in the background.” Supabase’s email templates guide names that product when it warns that some providers’ security features may “prefetch URL links from incoming emails”, so its confirmation link is “consumed instantly”, which leads to a “Token has expired or is invalid” error. Auth0’s docs say email verification links “can lead to accidental verification by email scanners”.

My fix is a landing page: the link opens a page with a button, and only the button’s POST request verifies the account. A GET that is safe to repeat, changing nothing until the POST arrives, does the same job. Supabase’s docs give two ways out: a code in place of the link, or a link to your own page with a confirm button.

A tested auth library or your platform’s built-in flow beats hand-rolled code. The block is here so you can read what yours does and spot the step it skips.

An email verification link holds one parameter, the token, on your production domain over HTTPS. It carries no email address, user id or open redirect. The bug builder-made apps hit is a link that opens localhost because the auth provider’s site URL was never changed.

Leave the email address out, because the link passes through mail logs and forwards. Leave the user id out too: the token already identifies the user, and a second parameter invites someone to edit it. A redirect parameter that accepts any URL is the worst of the three, since an open redirect on your own domain turns your verification email into a phishing tool. My rule on top of those: the domain in the link matches the domain the email comes from, so the message looks like what it is.

Supabase’s redirect URLs guide says the Site URL “defines the default redirect URL when no redirectTo is specified in the code”, and tells you to “Change this from http://localhost:3000 to your production URL”. A project that skipped that step sends links that open localhost. In Firebase, the matching settings are the template’s “customize action URL” and the continue URL you pass when sending. The same localhost slip outside email is the subject of an app that works locally but not in production, and on Lovable the published address also has to be on the redirect list, one of the causes in why users can’t log in to your app.

A correct link can still break on the way. An email provider with click tracking switched on rewrites the links in your message, and Supabase’s docs say its links then “won’t perform as expected”, recommending that you disable tracking.

A password reset link follows the same rules. The reset flow adds its own, such as the same answer whether or not the account exists and the no-referrer policy on the reset page, and those are part of authentication best practices.

Verification code emails: the code, the expiry, and the retry

A verification code email, in my rules, shows the code, says when it expires and offers a way to ask for a new one after a cooldown. The screen after sending names the address, masked on a reset, so a stranger learns nothing, and lets the person fix a typo.

A code in place of a link has, to my eye, two advantages: a mail scanner cannot use it up, and it works when someone signs up on a laptop and reads mail on a phone. Auth0 offers codes for the first reason; its one-time passwords, in its words, ensure “each user actively verifies an existing email address.”

When you send the email verification code, the rules for the code itself (single use, a short expiry, a constant-time compare, a resend limit per address) come from authentication best practices. This section is about what the person sees.

What the email or screen showsThe ruleWhy
The codeOn its own line, with no link wrapped around itEasy to read and copy on a phone
What the code is forThe app’s name and the action: confirm your email, or reset your passwordA code with no context looks like phishing
When it expiresA plain duration that matches the server’s settingThe person knows whether to hurry or ask again
A line for people who did not ask”If you did not ask for this, ignore this email”A stranger who receives it knows nothing happened
The confirmation screen”An email with a verification code was just sent to” and the addressThe person can spot a typo in what they entered
The address on a reset screenMasked, for example the first letter and the domainThe screen does not confirm to a stranger that an account exists
The resend linkShown only after the cooldownNo pile of sends while the first is still on its way
A way to fix the addressA “wrong address?” link back to the formA typo otherwise ends in a dead end

For a dated example of one platform’s defaults, Supabase’s passwordless email guide describes “a six-digit code” and says: “By default, a user can only request an OTP once every 60 seconds, and they expire after 1 hour.” The expiry is configurable, and anything over 86,400 seconds (one day) is “strongly discouraged”. That one setting also governs the confirmation and password recovery links, so shortening it for codes shortens those as well.

Send email verification in Firebase

Firebase sends the verification email itself when the app calls sendEmailVerification on the signed-in user. Before launch, change 4 settings in the console: the sender name, the custom sending domain, the action URL and the template. The email_verified value only matters where your rules check it.

To send email verification in Firebase, call sendEmailVerification(auth.currentUser) from the client SDK after signup, and set auth.languageCode first if the message should go out in the person’s language. If you would rather use your own template and provider, the Admin SDK’s generateEmailVerificationLink returns a link that, in Firebase’s words, “can be inserted into the custom verification email and then emailed to the corresponding user using a custom SMTP server.” Either way, Firebase’s guide to managing users has the calls.

The console part lives in the Templates tab under Security > Authentication. Each email type lets you set the sender name, the sender address and the reply-to address. “Customize domain” puts your domain in the From field and the action links, and Firebase says verifying it “can take up to 24 hours”, so do it well before launch day. “Customize action URL” points the link at your own handler page. Firebase documents a limit of 1000 address verification emails a day on the Spark plan and 100,000 a day on Blaze; password reset emails get 150 a day on Spark. Firebase says these quotas scale with the number of users.

emailVerified is a property on the user object, and in security rules auth.token.email_verified is, per Firebase’s rules and authentication guide, “true if the user has verified they have access to the email address.” Firebase’s docs do not say that rules block unverified users by default, so your rules or your backend have to check the value.

Picture a founder’s Firebase app that calls sendEmailVerification at signup, with Firestore security rules that let any signed-in user read and write their workspace data. The rules check only that a user is signed in, never email_verified, so an account created with someone else’s address, never verified, gets the same access as any other. The verification email sets a flag, and the flag protects nothing until the rules or the backend read it.

Auth0 and other managed auth providers: where the verification email comes from and how to change it

Managed auth platforms send verification email from a default sender meant for trying things out. Auth0 says its built-in provider does not support production use, Supabase’s built-in service has a small hourly limit, and Lovable caps each project’s authentication emails per hour. Production means your own domain and provider.

The Auth0 email verification flow, as Auth0’s email verification docs describe it: “By default, Auth0 emails verification links to users when they sign up”, and “When the user clicks the link, the user’s email_verified flag is set to true.” What an unverified user may do is your call; Auth0’s support site shows a post-login Action that checks event.user.email_verified and calls api.access.deny when it is false.

Auth0’s page on email providers is plain about the default: the built-in provider “does not support production use”, sends everything from no-reply@auth0user.net, allows no custom templates, and is limited to “10 emails per minute, regardless of email type.” Production needs your own provider, set in the dashboard under Branding > Email Provider with the “Use my own email provider” toggle.

To make Auth0 send verification email again, the Management API has two calls. The email verification job (/api/v2/jobs/verification-email) “triggers Auth0 to send the verification email using the verify email template”; the email verification ticket (/api/v2/tickets/email-verification) gives you a link to send yourself, through your own provider.

PlatformWho sends by defaultThe limit of the defaultWhat to configure for production
Auth0The built-in email provider, from no-reply@auth0user.net10 emails per minute of any type; no custom templatesYour own provider under Branding > Email Provider
Supabase AuthThe built-in email serviceA small hourly limit, and only to addresses on the project’s teamCustom SMTP, then the Site URL and the redirect list
Firebase AuthenticationFirebase’s own senderAddress verification emails: 1000 a day on Spark, 100,000 a day on Blaze, scaling with the number of usersCustom domain, sender name, reply-to and action URL; or Admin SDK links sent through your provider
LovableThe default Lovable Cloud Auth senderA per-project hourly cap on authentication emails (“Rate limit for sending emails”), raised only after you set up managed email or a custom domainEmails on your own domain (paid plans), with workspace limits per hour: Pro 100 app and 500 authentication emails, Business 300 and 3,000, Enterprise 1,000 and 10,000

Supabase’s built-in service exists for “Exploring and getting started with Supabase Auth” and for testing templates with the project’s team; the exact hourly figure, and the custom SMTP setup that lifts it, are in the Supabase email rate limit. On Lovable, app emails and authentication emails have separate hourly limits, set by plan, as the table shows. A Lovable Cloud confirmation email that never arrives has causes of its own, set out in why users can’t log in to your app.

The pattern I see across all four: the default sender is for getting started, and the production step is the same everywhere, your domain and your provider. Once your own provider is in place its limits apply to all your mail, not only the auth messages, and going over one shows up as an email rate limit exceeded error.

When verification fails: the failure modes and what to check first

A missing verification or reset email is traced in 5 checks, from the send outward: was it sent, was it accepted or suppressed, was it delivered to spam, did the link fail because it expired or was already used, and did it arrive late. The provider’s log answers the first two.

Work from the send outward, never from the mailbox inward. “Check your spam folder” is the last answer, not the first. When a customer says they’re not getting the verification email, their address in the provider’s log tells you which of the five cases you are in before anyone looks at an inbox.

What the customer saysWhere it showsLikely causeFirst check
”Why am I not getting the verification email?”No entry for the address in the provider’s logThe app never asked: the trigger did not fire, or a job queue is stuckSearch the provider’s log by recipient
”Why won’t the verification email send?”An error on the signup screen or in the app’s logThe send call failed: provider credentials, or the built-in sender’s limitThe app’s log for the send call
”I never received the password reset email”A bounced or suppressed status in the provider’s logA mistyped address, a full mailbox, or an earlier bounce or complaint on the suppression listThe message’s status and the suppression list
”The verification email was not received”, though the log says deliveredA delivered statusSpam, a company mail gateway’s quarantine, or a tabAsk them to search spam and quarantine for your sending domain
”Email verification failed” when they clickedThe verify endpoint’s logExpired, used first by a link scanner, replaced by a newer resend, or the wrong environmentThe token’s state and the link’s host
”Why am I not receiving verification emails until an hour later?”Send and delivery times far apart in the logA queue at the provider, or a receiving server that greylistsCompare the send and delivery timestamps
  1. Confirm it was sent. Search the email provider’s log for the address. Nothing there means the email verification is not sending at all, because the app never asked: the trigger did not fire, the send call failed with a “failed to send verification email” error, the platform’s built-in sender hit its limit, or a job queue is stuck.
  2. Then confirm the provider accepted it. A bounce means a mistyped address or a full mailbox. A “suppressed” status means an earlier bounce or complaint put the address on the provider’s suppression list, and the provider is no longer sending to it. When a customer never received a password reset email after years of normal mail, this is the likely answer. Suppression lists themselves belong with an email bounce handling checklist.
  3. If the log says delivered, look past the inbox: spam, a quarantine at a company mail gateway, or a tab the person never opens. Getting out of spam is the job of transactional emails landing in spam, and testing placement before launch belongs with deliverability testing.
  4. If it arrived and the click failed, look at the token. “Email verification failed” on click means the token expired, a link scanner used it first, a newer resend replaced it and the person clicked the older email, or the link points at the wrong environment.
  5. If it arrived late, compare the send and delivery times. A queue at the provider or a greylisting mailbox can hold a message back. An expiry shorter than real-world delay is, more often than not, what turns late into failed.

My rules for support are short. Support can resend, confirm the address on file, and remove a suppression after checking why it was added. Support never reads a token out of the database, never sets a password for someone, and never marks an address verified because an email from that address asked.

Two platform cases are pointers rather than new checks: on Supabase’s built-in sender, check 1 can end at its hourly limit, covered in the Supabase email rate limit article; on Lovable Cloud, the missing confirmation email is traced in why users can’t log in to your app. If the verification that keeps failing is for a Google, Microsoft or other account you do not run, their help pages are the place to go, and nothing here applies.

How to check your own app

A verification email is checked from the sending side in 7 tests: arrival from your domain at three mailbox providers, a link that opens production, a provider log entry for each send, a suppressed address that someone would notice, no raw token in logs, a working resend, and a scanner-safe link.

Run them on production, with test accounts you own.

  1. 01 Sign up with a Gmail, an Outlook and a company address. Pass: each message arrives within about a minute, from your own domain
  2. 02 Open the link or enter the code from each message. Pass: it lands on the production site, not localhost or a preview address
  3. 03 Find each message in the provider's log, matched by recipient and send time. Pass: every send has an entry
  4. 04 Press resend. Pass: a second email arrives, and its link or code works
  5. 05 Where the provider lets you add an address to its suppression list, add a test address and request a reset. Pass: the provider shows the message as suppressed, and the app writes a log line support can find
  6. 06 Copy the token from a received test email and search the app's logs and analytics for it, within the host's log retention window. Pass: it is not there
  7. 07 Open the verification link in a private window and stop at the landing page without pressing its button. Pass: the account stays unverified until the button is pressed

The minute in check 1 is a threshold I picked, not a provider figure; slower than that, look at the provider’s queue. Check 3 matches on recipient and send time because you know both for every test send. Check 6 uses the token you actually received, because a search for a value nobody sent proves nothing, and an empty result counts only inside the host’s log retention window.

By their own docs, the default links on Supabase and Auth0 fail check 7, for the scanner reason in the endpoint section; a code, or on Supabase a template link to your own page with a confirm button, passes it. What Firebase’s default handler page does on first load is not stated in Firebase’s docs, so run the check there too.

The token paths themselves (a second click, an expired link, a reused token) are tested in the verify table that goes with authentication best practices. Evidence to keep: a dated table of the seven results, and the provider log entries from check 1.

The Production Hardening Sprint verifies deliverable 11.2 this way: “Trigger each critical message type and confirm provider records and expected recipient delivery.” Deliverable 11.3 is verified this way: “Simulate provider events and verify suppression and subsequent send behavior.”

Where the sprint fits

In the Production Hardening Sprint, deliverable 1.1, Complete authentication flow review, is where we review and fix signup, login, logout, password reset, email verification, and OAuth callbacks. Deliverable 11.2, Transactional delivery tracking, is where we configure a dedicated transactional provider with delivery tracking for account and transaction messages, and deliverable 11.3, Bounce and complaint handling, is where we process bounces and complaints and suppress further sending where appropriate. The result for every scope item, the work completed and its verification evidence go into the production readiness report, deliverable 13.1. Hosting, paid tools, and API usage remain in your accounts. The wording for each deliverable is in the published scope.

Common questions about verification emails

How long should a verification email take?

Seconds, in the normal case, and I’d look into anything past about a minute. Minutes point to the provider’s own queue or to greylisting at the receiving end. Whatever the delay, the link’s expiry has to allow for it, or a late email arrives with a link that no longer works.

Can you give me an example of a verification code?

Yes. Here is one I made up, 481 920, sent with two short lines around it: “Your code to confirm your email for [app name] is 481 920” and “It expires in 10 minutes, so ignore this email if you did not ask for it.” The six-digit format is my example, not a standard: Supabase’s docs describe a six-digit code, while Lovable lets you set 4 to 8 digits, so check what your own auth provider sends.

What happens if you verify your email?

The account is marked verified: the app sets a flag and a timestamp on your user record, such as email_verified in Auth0 and Firebase. Any limits on unverified accounts lift, and the address becomes the way back into the account through password resets, which, to my mind, is why it matters which address you verify.

What is the correct email format for validation?

Use the HTML specification’s valid email address rule: allowed characters before the ”@”, and a domain made of dot-separated labels after it. The spec calls its rule “a willful violation of RFC 5322”, whose syntax it finds “simultaneously too strict”, “too vague” and “too lax” for practical use. For email verification, format checks only catch typos, so validate lightly and let the email itself be the real test.

Why am I being asked to verify my email?

The service wants proof that the address is yours before it uses it for password resets, receipts and sign-in. If you just signed up or changed your address, use the link or code. If you did neither, someone typed your address, by mistake or not: ignore the email and click nothing, and the account stays unverified.