hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Production Auth Patterns with @hectoday/http

Before They Start

  • Why Production Auth Is Different
  • Project Setup

Email Verification

  • Why Verify Emails
  • Building Email Verification
  • Restricting Unverified Accounts

Session Management

  • Tracking Sessions Across Devices
  • Listing and Revoking Sessions
  • Session Security

Step-Up Authentication

  • What Is Step-Up Auth
  • Building Step-Up Auth
  • Applying Step-Up to Sensitive Routes

Account Deletion

  • The Right to Be Forgotten
  • Building Account Deletion
  • Data Cleanup

SAML and Enterprise SSO

  • What Is SAML
  • Building a SAML Service Provider
  • Just-in-Time Provisioning

Putting It All Together

  • Production Auth Checklist
  • Capstone: Production-Ready Auth

Restricting Unverified Accounts

What unverified users can and cannot do

A common approach: let unverified users access basic features but block sensitive or social actions.

Allow without verification: View their own profile, change settings, log out, verify their email (obviously).

Block until verified: Create content, send messages, invite members, access billing, change email address.

The specific split depends on your app. A content platform might allow reading but block posting. A team tool might block everything except the verification step.

The requireVerified function

// src/auth.ts — add this
export function requireVerified(user: AuthUser): true | Response {
  const row = db.prepare("SELECT email_verified FROM users WHERE id = ?").get(user.id) as {
    email_verified: number;
  };

  if (row.email_verified !== 1) {
    return Response.json(
      { error: "Email not verified. Check your inbox or POST /resend-verification." },
      { status: 403 },
    );
  }

  return true;
}

Use it in route handlers that require verification:

route.post("/notes", {
  resolve: (c) => {
    const user = authenticate(c.request);
    if (user instanceof Response) return user;

    const verified = requireVerified(user);
    if (verified instanceof Response) return verified;

    // ... create note
  },
});

Same true | Response pattern as authenticate, requireRole, and requirePermission. The checks compose naturally:

const user = authenticate(c.request); // Who are you?
if (user instanceof Response) return user;

const verified = requireVerified(user); // Is your email verified?
if (verified instanceof Response) return verified;

const perm = requirePermission(user, orgId, "notes:create"); // Do you have permission?
if (perm instanceof Response) return perm;

What about the login response?

Include emailVerified in the login response so the frontend knows whether to show the verification banner:

return Response.json({
  user: {
    id: user.id,
    email: user.email,
    name: user.name,
    emailVerified: user.email_verified === 1,
  },
});

Cleaning up stale unverified accounts

Users who sign up and never verify clutter your database. Clean them up periodically:

// Run daily (via a cron job or scheduled task)
function cleanupUnverifiedAccounts(): void {
  const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000; // 7 days
  const cutoffDate = new Date(cutoff).toISOString();

  // Find unverified users older than 7 days
  const stale = db
    .prepare("SELECT id FROM users WHERE email_verified = 0 AND created_at < ?")
    .all(cutoffDate) as { id: string }[];

  for (const user of stale) {
    // Delete related data
    db.prepare("DELETE FROM email_verifications WHERE user_id = ?").run(user.id);
    db.prepare("DELETE FROM sessions WHERE user_id = ?").run(user.id);
    db.prepare("DELETE FROM users WHERE id = ?").run(user.id);
  }

  if (stale.length > 0) {
    console.log(`Cleaned up ${stale.length} unverified accounts older than 7 days.`);
  }
}

[!NOTE] The cleanup period (7 days) should match your verification token expiry (24 hours) with extra margin. If the token expires in 24 hours and the account is deleted after 7 days, the user has 7 days to sign up again and receive a fresh token.

Exercises

Exercise 1: Add requireVerified to a route. Sign up without verifying. Try accessing the route. You should get 403 with the verification prompt.

Exercise 2: Verify the email. Try the route again. It should work.

Exercise 3: Implement the stale account cleanup. Create a test account, do not verify it, and run the cleanup (or set the cutoff to 5 seconds for testing). The account should be deleted.

Why do we allow unverified users to log in at all?

← Building Email Verification Tracking Sessions Across Devices →

© 2026 hectoday. All rights reserved.