Email Verification Flow
If you're building a B2B SaaS in 2026, email verification at signup is the gate that confirms users are real. The naive approach: send a link; require click before they can use the product. The structured approach: balance verification rigor with onboarding friction (block-on-verify vs delay-verify), use proper token security, handle bounces / typos / catch-all addresses, integrate with auth provider, support resend + email change, instrument the funnel. Email verification is also the primary anti-fraud signal — abusers use disposable / fake / typo'd emails. Done well, it's invisible. Done poorly, you lose 10-30% of signups to friction. (Distinct from password-reset-magic-link, which is recovery flow, not initial verification.)
1. Decide block-on-verify vs delay-verify
The single biggest decision shapes everything.
Decide verification timing.
Pattern A: Block until verified (strict)
- User signs up → email sent → can't access product until clicked
- Pro: high data quality; reduces fake signups; fraud defense
- Con: 10-30% drop in completed signups (friction)
- Used by: SOC 2-required products, financial / regulated, anything with fraud risk
Pattern B: Delay verification (friendly)
- User signs up → enters product immediately → email sent in background
- Banner reminds: "Verify your email"
- Block specific actions if unverified (e.g., invite teammates)
- Pro: low friction; better activation
- Con: more fake / abuse signups
- Used by: most modern PLG B2B SaaS (Linear, Notion, Loom)
Pattern C: Reverse trial / soft-block
- Full access for N days unverified
- Hard-block after grace period
- Compromise
Pattern D: Risk-based
- Verify only if signup looks suspicious (disposable email, IP risk, etc.)
- Trust-by-default; verify-when-needed
- Used by: Stripe, modern fraud-aware products
Recommendation:
- 2026 B2B SaaS default: Pattern B (delay) with selective gates
- Reserve Pattern A for high-stakes products
Decide based on:
- Fraud / abuse risk (high → strict)
- Time-to-value (long → strict OK; short → friction kills)
- Customer expectation (B2B vs B2C)
- Compliance (SOC 2 / regulated → may require)
For [PRODUCT], output:
1. Recommended pattern
2. Selective gates (what to block until verified)
3. Time-to-verify SLA
4. Drop-off measurement
5. Anti-abuse considerations
The Linear / Notion default: enter product immediately; verify in background; selective gates for irreversible actions (invites, billing). Friction-minimal but still verified.
2. Token generation + security
Verification tokens are security-sensitive.
Generate verification tokens correctly.
Properties:
Cryptographically random:
- crypto.randomBytes(32).toString('base64url') — 32 bytes = 256 bits
- Don't use Math.random or sequential IDs
- Don't use predictable patterns
Single-use:
- Mark consumed in DB after first use
- Reject reuse (replay attack)
Expiring:
- 24 hour expiration typical
- 1 hour for sensitive flows
- Store created_at; check at verify
Bound to user:
- Token tied to user_id (not just email)
- If user changes email, old token invalidates
Hashed at rest:
- Store hash(token), not plaintext
- Hash with SHA-256
- Compare hashes at verify
Storage:
email_verifications table:
- id (UUID)
- user_id (fk)
- email (the email being verified)
- token_hash (SHA-256 of token)
- created_at, expires_at, consumed_at, consumed_ip
Token in URL:
- /verify-email?token=...
- Tokens longer than 32 chars typical
- URL-safe base64
Anti-patterns:
- Token = email + timestamp + secret (predictable)
- Token never expires
- Token reusable forever
- Token stored plaintext (DB leak = compromised)
Output:
1. Token generation code
2. Storage schema
3. Hashing
4. Expiration policy
5. Verification logic
The plaintext-token-storage trap: if DB leaks, attackers can verify any pending email. Always hash. Same as passwords.
3. Verification email — content + UX
The email itself matters.
Design verification email.
Content:
Subject:
- "Verify your email for [Product]"
- Or: "Welcome to [Product] - confirm your email"
- Don't: clickbait
Body:
- Greeting: "Hi [name],"
- Reason: "Confirm your email so we can send important updates."
- CTA: prominent button "Verify email"
- URL: also as text (some clients strip buttons)
- Expiration: "This link expires in 24 hours"
- Footer: "Didn't sign up? Ignore this email."
Design:
HTML email:
- Branded; clean
- Responsive (mobile-friendly)
- Single clear CTA button
- Fallback plain text
Branding:
- Logo
- Brand colors
- From name: "[Product Name] <noreply@product.com>"
Deliverability:
- SPF / DKIM / DMARC configured
- Warm-up sender domain
- Avoid spam triggers (excessive caps, words like "free")
- See email-deliverability-chat
Multi-language:
- Detect user locale from signup
- Send in user's language
- See internationalization-chat
Sending:
Provider:
- Transactional email (Resend / Postmark / SES / Mailgun)
- Don't use marketing email tools (Mailchimp etc.) for transactional
- See email-providers VibeReference
Speed:
- Send within 10 seconds of signup
- User waits with "check your email"
Retry:
- Exponential backoff if API fails
- Queue (Vercel Queues / Inngest)
Output:
1. Email template
2. HTML + plain text
3. Send infrastructure
4. Multi-language handling
5. Speed SLA
The "check your email" expectation: user clicks signup; immediately checks email. If your email arrives 30 seconds later, half close the tab. Send fast.
4. Verify endpoint — handle clicks
The endpoint that processes verification clicks.
Implement verify endpoint.
Endpoint: GET /verify-email?token=xxx
Flow:
1. Extract token from query
2. Hash token
3. Lookup in DB by hash
4. Check: exists, not expired, not consumed
5. Mark consumed (with IP + timestamp)
6. Mark user.email_verified = true
7. Redirect to product / success page
Validation:
If token invalid (no match):
- Show error page: "Link invalid or expired"
- Offer resend option
- Don't leak whether email exists
If token expired:
- Show "expired" page
- Offer resend option
If token already consumed:
- If first user is current user → "already verified; continue"
- Else: ambiguous; offer to log in
- Or: allow re-verification (refresh)
If token valid:
- Mark verified
- Auto-login (some apps) OR
- Show success → "Continue to product"
- If user not logged in: prompt login
Edge cases:
Email already changed:
- Original email being verified, but user changed to different email
- Verify the original (close that loop)
- Or: invalidate all pending tokens on email change
User deleted:
- Token still in DB
- Reject gracefully
Security:
Rate limit:
- Per-IP per-token (3 attempts max)
- Anti-enumeration
Audit log:
- Verification event logged
- For incident response
Output:
1. Verify endpoint
2. Token validation
3. Success / error states
4. Auto-login strategy
5. Audit logging
The "auto-login on verify" UX: clicking link logs you in + verifies. Reduces friction. Most B2B SaaS does this. Some refuse for security reasons.
5. Resend verification
Users lose email; need easy resend.
Implement resend verification.
UX:
Banner in product:
- "Verify your email" with "Resend" button
- Visible until verified
Standalone page:
- /verify-pending
- "Didn't get email? [Resend]"
- Show email being verified (so user can confirm)
Settings page:
- Account settings → "Email not verified" → Resend
Rate limiting:
Per-user:
- Max 5 resends per 24 hours
- Cooldown 60 seconds between
Per-email:
- Max 3 different users per 24h (anti-abuse; prevents spamming someone else's email)
Per-IP:
- Max 10/hour (anti-bot)
Implementation:
POST /verify-email/resend
- Authenticate (must be logged in)
- Check rate limit
- Generate new token (invalidates previous)
- Send email
- Show toast: "Email sent"
Anti-abuse:
- Don't allow unauthenticated resend
- Don't reveal whether email exists
- Captcha on suspicious patterns
Email change:
- Sometimes user signed up with typo
- Allow editing email + resend
- Verify new email
Output:
1. Resend UI
2. Rate limiting
3. Token rotation
4. Email-change support
5. Anti-abuse
The "edit email + resend" critical feature: typos at signup are common ("johm@company.com" instead of "john@company.com"). Without edit, users have to re-signup.
6. Selective gates — what to block before verify
In Pattern B (delay verify), some actions still need to wait.
Decide what to gate behind email verification.
Always gated (high-stakes):
- Inviting teammates (could be fraudulent)
- Billing actions (payment + subscription)
- Sensitive data export
- API key generation
- Admin actions
Sometimes gated:
- Posting publicly (avoid spam)
- High-value transactions
- Trial-to-paid conversion
Rarely gated:
- Reading product
- Creating personal items (notes, projects)
- Light usage
Per-action gate:
if (!user.email_verified && requiresVerification(action)) {
return showVerificationRequired();
}
Gate UI:
Modal:
- "Verify email to invite team members"
- Buttons: "Resend verification" / "Cancel"
- Don't block product entirely
Banner:
- "Verify email" persistent banner
- Click → verification page
Settings warning:
- Flag in account settings
- "Email unverified" badge
Specific gates:
Invite teammate:
- "You'll be able to invite once you verify."
- Modal or inline error
Subscription upgrade:
- "Verify email before upgrading"
- Don't take payment from unverified
API access:
- Require verified email for API key generation
- Reject API requests from unverified accounts? (some products)
Free tier limits:
- Tighter limits for unverified
- Loosen on verify
Output:
1. Gate matrix per action
2. Gate UI patterns
3. Severity levels
4. Bypass for trusted-IP (rare)
5. Test cases
The "block invite until verified" rule: prevents abuse where bad actors signup with fake email + invite real users. Tight default.
7. Email change flow
User wants to change their email address. Treat carefully.
Implement email change.
Flow:
1. User in settings → "Change email"
2. Enter new email
3. Optionally: re-enter password (for security)
4. Send verification to new email
5. New email click verifies + sets as primary
States:
Pre-change:
- old_email = current
- new_email = pending change (in DB)
Post-verify:
- old_email = no longer primary
- new_email = primary
- Old email user notified (security)
Security:
Re-authentication:
- Require password before change
- Or: send confirmation to OLD email too
- Anti-account-takeover
Notify old email:
- "Your email was changed to [new]. Wasn't you? [Revert]"
- 24-hour revert window
- Reduces account takeover damage
Multi-factor:
- If 2FA enabled: require 2FA before email change
Email merge:
If new email belongs to existing account:
- Reject change OR
- Offer merge flow (advanced; complex)
- Most products: reject
Audit:
- Log email change event
- Useful for security investigations
Edge cases:
User mid-change:
- Pending change in DB
- Allow cancel
- Allow re-trigger
Verification expires:
- Show "verification expired; click to resend"
Output:
1. Email-change UI
2. Verification flow
3. Old-email notification
4. Re-authentication requirement
5. Audit + revert
The notify-old-email rule: if attacker compromises account, they may change email. Notifying old email gives victim chance to revert before damage.
8. Disposable / typo / catch-all email handling
Anti-abuse + data quality.
Detect bad emails.
Disposable / temporary email:
Detection:
- Maintain blocklist of disposable domains (mailinator.com, 10minutemail.com, etc.)
- Or: use service (Verifalia, Kickbox, ZeroBounce)
- Block at signup
Common disposable:
- mailinator.com
- 10minutemail.com
- guerrillamail.com
- temp-mail.org
- tempmail.com (~50+ common ones)
Library: disposable-email-domains npm
Typo detection:
Common typos:
- gmial.com → gmail.com
- yahooo.com → yahoo.com
- hotmail.co → hotmail.com
Detection:
- mailcheck.js library
- "Did you mean gmail.com?" suggestion
- One-click fix at signup
Catch-all detection:
Catch-all = anything@company.com goes to one inbox
- Hard to detect; affects email validation
- Use email validation service to flag
Email validation services:
- ZeroBounce, NeverBounce, Verifalia, Kickbox, Hunter
- Validate: deliverable / disposable / role-based / etc.
- $0.001-0.01 per validation
- Use at signup; cache result
Role-based emails:
info@, admin@, sales@, support@:
- Often used for shared inboxes
- Lower-quality leads
- Some products block; some allow
When to validate:
- At signup (server-side)
- Before sending high-value emails (avoid bounces)
Anti-patterns:
- Block all role-based (loses real users)
- No validation (high bounces; reputation hit)
- Slow validation blocks signup (do async if possible)
Output:
1. Disposable email blocklist + service
2. Typo detection
3. Catch-all + role-based handling
4. Validation service integration
5. Cache + reuse
The typo-suggestion pattern: 5% of signups have typo. "Did you mean gmail.com?" caught 80% of those. Easy win.
9. Bounce handling
Verification emails bounce sometimes. Plan it.
Handle email bounces.
Bounce types:
Hard bounce:
- Permanent (mailbox doesn't exist)
- Mark email invalid; require change
Soft bounce:
- Temporary (full mailbox, server down)
- Retry sending
- Mark invalid after 5 soft bounces
Block / spam:
- Filtered as spam
- Investigate sender reputation
- Affects deliverability
Detection:
Email provider webhooks:
- Resend / Postmark / SendGrid / SES emit bounce events
- Subscribe via webhook
- Update DB: user.email_status = bounced
Queue + retry:
- If transient: retry 3x with backoff
- If permanent: stop; notify
User UX:
When email is hard-bounced:
- Show in product: "Your email is invalid. [Change]"
- Block password reset / verifications
- User must update email
Verification specific:
- If verify email bounces: tell user
- Show "Your email may not be valid" warning
- Easier to typo-fix than discover later
Bounce metrics:
Track:
- Bounce rate (target <1%)
- Specific patterns (one provider blocking?)
- Reputation score (Postmark / Mailgun show)
Anti-spam:
If high bounce rate:
- Investigate (data quality issue)
- Sender reputation hit
- Deliverability tanks
Output:
1. Webhook setup per provider
2. Bounce-event handling
3. User notification
4. Retry policy
5. Monitoring
The bounce-rate cliff: above 5% bounce rate, deliverability collapses. Major email providers blacklist senders. Active monitoring critical.
10. Measure verification funnel
Optimize what you measure.
Track verification funnel.
Metrics:
Top-of-funnel:
- Signups attempted
- Signups completed (initial form)
Email-related:
- Verification emails sent
- Bounce rate
- Spam folder rate (estimated)
Verification:
- % of users who click within 1 hour
- % within 24 hours
- % ever (target: 80%+)
Drop-off:
- % stuck at "verify pending" forever
- Time-to-verify median
Resend:
- % of users who request resend
- Resend → verify rate
- Why resend? (lost email / typo)
Conversion:
- Verified users → activated
- Verified vs unverified retention
Tools:
Product analytics:
- PostHog / Amplitude / Mixpanel
- Funnel: signup → email sent → verify clicked → activate
Email analytics:
- Resend / Postmark / SendGrid dashboards
- Open rates (proxy for "they got it")
Anti-patterns:
- Don't track verification rate (don't know your friction)
- Treat all unverified the same (some never will; some just lost email)
Optimize:
Low verification rate (<60%):
- Test subject lines
- Check spam folder
- Review email design
- Speed of send
- Offer resend prominently
High typo rate:
- Add typo detection
- Validate domain at signup
Output:
1. Funnel metrics
2. Tooling
3. Anti-patterns
4. Optimization triggers
5. A/B test framework
The 80%+ verification rate target: below that, you're losing users to friction. Investigate: speed of email, spam folder, design, friction.
What Done Looks Like
A v1 email verification system:
- Block-on-verify vs delay decision made
- Cryptographic token (32 bytes random; hashed at rest)
- 24-hour expiration; single-use
- Branded HTML email + plain text fallback
- Verify endpoint with proper validation + auto-login
- Resend with rate limits
- Selective gates (invite / billing / API blocked until verified)
- Email-change flow with security
- Disposable / typo detection
- Bounce handling via webhook
- Funnel metrics tracked
- 80%+ verification rate
Add later when product is mature:
- Risk-based verification (skip when low risk)
- Email verification API (programmatic)
- Phone-as-fallback verification
- Multi-factor at signup
- Trust score
- Localization
The mistake to avoid: token in plaintext in DB. DB leak compromises all pending verifications.
The second mistake: slow verification email. Send in <10 seconds; otherwise users close tab.
The third mistake: no edit-email + resend flow. Typos lock users out.
See Also
- Password Reset & Magic Link — recovery flow (companion)
- Two-Factor Auth — adjacent security
- Social Login & OAuth — adjacent auth
- SSO & Enterprise Auth — enterprise auth
- Email Template Implementation — email templating
- Email Deliverability — sender reputation
- Onboarding Email Sequence — adjacent emails
- Form Validation UX — signup form
- Multi-Step Forms & Wizards — signup wizards
- Captcha & Bot Protection — anti-abuse
- Rate Limiting & Abuse — rate limiting
- Toast Notifications UI — feedback
- Empty States, Loading & Error States — error UI
- Settings & Account Management Pages — account settings
- Audit Logs — audit
- Internationalization — localization
- VibeReference: Email Providers — Resend / Postmark / SES
- VibeReference: Resend — Resend deep-dive
- VibeReference: Authentication — auth foundation
- VibeReference: Auth Providers — Clerk / Auth0
- VibeReference: Better Auth — modern auth
- VibeReference: Email Verification & Validation Tools — ZeroBounce / NeverBounce