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?