Email Verification
Sent on registration. Optional — users aren't blocked from signing in if unverified.
Files
- Schema:
src/lib/db/schema/email-verifications.ts(token PK, user_id, email, expires_at, used_at). - Helpers:
src/lib/auth/email-verification.ts—createVerificationToken,consumeVerificationToken,sendVerificationEmail. - Send route (automatic on registration):
src/app/api/auth/register/route.ts. - Verify route:
GET /api/auth/verify-email?token=....
Flow
- User registers → server creates
usersrow, firessendVerificationEmail(userId, email, baseUrl). sendVerificationEmailcallscreateVerificationToken(48-char nanoid, 24h expiry) and dispatches an email via the app's ownsendEmail()provider usingSYSTEM_FROM_EMAIL.- Email contains
<baseUrl>/api/auth/verify-email?token=<token>. - On click, the handler:
- Looks up token — rejects if expired, used, or unknown.
- Sets
users.email_verified = now(). - Marks token
used_at. - Writes audit log (
user.email_verified). - Redirects to
/overview?verified=true.
Env
SYSTEM_FROM_EMAIL— verified SES identity. Fallback:no-reply@sendoka.com(won't work without DKIM set up in SES for that domain).NEXTAUTH_URL— base URL for link construction.
Security
- Tokens are 48-char random (
nanoid(48)) — ~276 bits. Safe to use in a URL. - Single-use via
used_atcolumn. - 24h TTL.
- No enumeration protection needed — tokens aren't email-derived.
Limitations
- No UI for "resend verification email" — add later if needed.
- No gating: unverified users have full access. Change by adding a check in dashboard layout or specific routes if required.