Drop: Two-Stage Onboarding with Mandatory Verification Gate
One Commit, One Decision
Some days produce fifty commits across five repos. January 21st produced one. A single commit in the Drop project adding a two-stage onboarding system with a mandatory verification gate between signup and full access.
The commit was small but the decision it encodes is not. Drop had been letting users straight through after signup — create account, land on dashboard, start using the product. The problem: unverified email addresses were accumulating in the user table, creating support burden when password resets failed because the address was a typo, and making any future email-based communication unreliable.
The Two-Stage Pattern
The implementation splits onboarding into two discrete states:
Stage 1: Account Created — User submits the signup form, credentials get stored, and the user lands on a verification-pending screen. No access to the main application. The screen explains what to expect and provides a resend button.
Stage 2: Verified — User clicks the email verification link, their account transitions to verified status, and they redirect to the actual dashboard. Only verified users can proceed past the gate.
The gate itself is a middleware check. Every authenticated route checks user.emailVerified before rendering. Unverified users get redirected to the verification-pending page regardless of what URL they try to access. No exceptions, no bypass, no “skip for now” option.
Why Mandatory
The temptation with verification gates is to make them skippable. Let the user in but nag them later. This feels user-friendly but creates a worse problem: a population of users in a half-verified state that you have to handle everywhere. Every feature that sends email needs a conditional check. Every notification path needs a fallback.
Making verification mandatory is a one-time cost at the gate. Making it optional is an ongoing tax on every feature that follows.
The tradeoff is real — some percentage of signups will bounce at the verification step because they used a throwaway email or don’t check their inbox. That’s acceptable. Users who won’t verify an email address are users who can’t be reached for anything: password resets, billing notifications, security alerts, product updates. They’re not actually your users yet.
Implementation Notes
The verification flow uses a time-limited token sent via email. Standard pattern — nothing novel here. The token expires after 24 hours and includes the user’s email hash to prevent reuse across accounts. Clicking the link hits a server endpoint that validates the token, marks the account as verified, and issues a session redirect.
The interesting bit is the middleware gate. It runs on every authenticated route, not as a per-page check. This means adding new routes to the application doesn’t require remembering to add the verification check. New routes are gated by default. The middleware checks user.emailVerified before any route handler executes — if false, it short-circuits with a redirect to the verification-pending page.
This is a better pattern than per-page guards because it’s impossible to accidentally ship an unprotected route. New developers adding features don’t need to know about the verification requirement. The middleware enforces it automatically.
The verification-pending page includes:
- Clear messaging about what’s happening (“Check your email”)
- The email address they used (so they can spot typos)
- A resend button with rate limiting (one resend per 60 seconds)
- A “use a different email” option that effectively restarts signup
- A help text section for common issues (check spam folder, corporate email filters)
The resend rate limiting is server-side, not just a disabled button. Without it, a frustrated user clicking “resend” twenty times generates twenty emails, which looks like spam to email providers and can get your sending domain flagged. The rate limiter uses a per-user cooldown stored in the session — simple, stateless, and sufficient for the expected traffic pattern.
Session Stats
| Metric | Value |
|---|---|
| Commits | 1 |
| Repo | drop |
| Pattern | Two-stage onboarding with mandatory verification |
| Key decision | Mandatory gate vs. optional nag |
| Lines changed | ~120 (middleware + verification page + email template) |
| Dependencies | Email service (already configured) |
The Data Cleanup Angle
There’s a secondary benefit to mandatory verification that isn’t obvious at implementation time: your user table becomes trustworthy. Before the gate, the users table contained a mix of real addresses, typos (jeremy@gmial.com), and disposable domains (user@mailinator.com). Any analytics built on that table — signup conversion rates, user cohort analysis, email open rates — was polluted by phantom accounts.
After the gate, every row in the user table represents someone who demonstrated control over a real email address. The data is clean by construction, not by periodic cleanup scripts. This matters more than it sounds. Every future feature that touches user email — password resets, billing, notifications, marketing — can trust the address without additional validation.
What Comes Next
The verification gate is the foundation for everything that requires a trusted email address. Password reset flows, billing notifications, team invitations — all of these now have a guarantee that the email on file is real and accessible. One commit today, but it unblocks a category of features that would each need their own email validation hack without it.
Thin days happen. The measure isn’t commit volume; it’s whether the commit that shipped was the right one to ship. Today’s commit was a gate — and gates, by definition, control everything that flows through them afterward.
Related Posts
- Leading Complex System Onboarding — Enterprise-scale onboarding patterns at a much larger scope
- HustleStats Production Auth Debugging — What happens when auth flows break in production
- DevOps Onboarding at Scale — Universal templates for onboarding infrastructure