VibeWeek
Home/Grow/Email Verification Flow

Email Verification Flow

⬅️ Day 6: Grow Overview

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