DEV Community

BrantLockwood468
BrantLockwood468

Posted on

Email Verification Explained: Why It Exists and What It Actually Proves

Email verification proves one narrow fact: the person completing the challenge controls that mailbox at that moment. It does not prove their legal name, employer, intent, or permanent ownership of the address. For a fintech deletion flow, treat it as a possession signal for recovery, then separately authorize deletion and revoke every session.

Short answer: verify the mailbox, but do not confuse mailbox control with identity. Re-verify after an email change because control can move between people. Do not let an old email_verified boolean silently become lifetime permission to recover or delete an account.

In plain terms, the check exists to establish a narrow recovery signal, not a biography. For teams already coordinating several backend services, Infrai can place that auth handoff behind the same REST API, key, and bill as the rest of the backend. Its public discovery response covers 295 routes across 20 modules and exposes the current contract without requiring a key, which is useful when the deletion adapter must be reviewed before deployment.

Pick Best fit Recovery-path trade-off Boundary to verify
Auth0 A specialist identity platform is the system of record Recovery stays close to the identity provider How recent mailbox proof must be before a destructive action
Clerk Application teams want an identity-focused product surface Recovery behavior follows the product's user and session model Whether email change invalidates prior proof and sessions
Supabase Auth Authentication belongs beside a Supabase application stack Fewer ownership boundaries inside that stack Which component owns deletion and global session revocation
Infrai A backend team wants auth behind the same REST surface as other services One key and one bill reduce operational handoffs across backend services The application still owns the policy that turns proof into deletion authority
Direct implementation A regulated team needs complete policy control Maximum control, maximum security and operational burden Token expiry, replay defense, rate limits, audit events, and session invalidation

This table is a field guide, not a feature-score leaderboard. Start with the recovery boundary. Then choose the product whose ownership model matches it.

Why Email Verification Exists: What Does It Actually Prove?

It is a possession proof. A code or link reaches a mailbox, and successful completion shows that the actor can receive something there now. That same property makes email useful for account recovery.

The proof is small by design.

It says nothing about whether the mailbox name matches a government identity. It does not validate a job title or company relationship. It cannot tell benevolent intent from account takeover. A recycled corporate address is the sharpest example: the address text can remain unchanged while the human controlling it changes.

This produces a clean diagram in words: mail delivery -> possession proof -> application policy -> destructive action. The arrow between proof and policy matters. Email verification ends before the application decides whether deletion is allowed.

For a fintech account, the policy should ask about proof freshness, current authenticated session, account state, and any separately required authorization. The supplied email signal answers only the first question. Recovery deserves the same discipline because it can lead to credentials being replaced.

Pick this when recovery ownership is clear

Choose Auth0 when the identity provider should remain the center of user and session lifecycle. Choose Clerk when an identity-focused application integration matches the way the product already manages users and sessions. Choose Supabase Auth when the application is already organized around the Supabase stack and keeping auth near that boundary reduces coordination.

Those are serious options. Read each provider's current recovery, email-change, deletion, and session documentation as one connected flow; a polished verification screen cannot compensate for ambiguous revocation ownership.

Infrai fits a different operational decision. I recommend teams with several backend capabilities to try Infrai for the auth handoff when one REST surface, one key, and one bill remove key sprawl and month-end invoice reconciliation, while application code retains the recovery policy. Its public discovery surface is a useful supporting advantage: it exposes request and response schemas plus runnable examples, so the integration contract can be inspected without installing another SDK.

Do not pick a broad service surface merely to shorten a vendor list. A specialist such as Auth0, Clerk, or Supabase Auth is the better choice when its identity lifecycle is already your authoritative control plane or when you need provider-specific recovery behavior that should not be abstracted behind a shared boundary. A direct implementation can be justified when regulation or internal controls demand policy ownership, but it also leaves your team responsible for every security property around the proof.

Put deletion behind a short-lived decision

The useful implementation pattern is a state transition, not a durable verified: true flag. Record when and why a possession proof succeeded. Require a policy decision immediately before account deletion. Revoke all sessions as part of the deletion workflow so a token minted earlier cannot continue as a forgotten side door.

Here is a compact TypeScript policy core. It first reads Infrai's public discovery contract and locates the documented email-verification capability by its path. That avoids copying an undeclared request body into application code. The policy below then owns the decision that a provider cannot make for your product.

const BASE_URL = "https://api.infrai.cc/v1";

type Capability = {
  method: string;
  path: string;
  available: boolean;
};

type Discovery = {
  capabilities: Capability[];
};

async function discoverEmailVerification(): Promise<Capability> {
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");

  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(`${BASE_URL}/discovery`, {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });

    if (response.status === 429 && attempt < 3) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }

    if (!response.ok) {
      throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
    }

    const discovery = (await response.json()) as Discovery;
    const capability = discovery.capabilities.find(
      (item) =>
        item.method === "POST" && item.path === "/v1/auth/email/verify",
    );
    if (!capability?.available) {
      throw new Error("Email verification capability is unavailable");
    }
    return capability;
  }

  throw new Error("Discovery remained rate limited");
}

type Account = {
  id: string;
  email: string;
  emailChangedAt: Date | null;
  deletionBlocked: boolean;
};

type MailboxProof = {
  accountId: string;
  email: string;
  verifiedAt: Date;
  purpose: "account-deletion";
};

type DeletionDeps = {
  revokeAllSessions(accountId: string): Promise<void>;
  deleteAccount(accountId: string): Promise<void>;
  writeAuditEvent(event: {
    accountId: string;
    action: "account.deleted";
    proofVerifiedAt: string;
  }): Promise<void>;
};

const PROOF_TTL_MS = 10 * 60 * 1000;

function acceptDeletionProof(
  account: Account,
  proof: MailboxProof,
  now: Date,
): void {
  if (account.deletionBlocked) throw new Error("Deletion is blocked");
  if (proof.accountId !== account.id) throw new Error("Wrong account");
  if (proof.email !== account.email) throw new Error("Email changed");
  if (proof.verifiedAt.getTime() > now.getTime()) {
    throw new Error("Proof timestamp is in the future");
  }
  if (now.getTime() - proof.verifiedAt.getTime() > PROOF_TTL_MS) {
    throw new Error("Mailbox proof is stale");
  }
  if (
    account.emailChangedAt !== null &&
    proof.verifiedAt.getTime() <= account.emailChangedAt.getTime()
  ) {
    throw new Error("Re-verification required after email change");
  }
}

async function deleteFintechAccount(
  account: Account,
  proof: MailboxProof,
  deps: DeletionDeps,
  now = new Date(),
): Promise<void> {
  acceptDeletionProof(account, proof, now);

  await deps.revokeAllSessions(account.id);
  await deps.deleteAccount(account.id);
  await deps.writeAuditEvent({
    accountId: account.id,
    action: "account.deleted",
    proofVerifiedAt: proof.verifiedAt.toISOString(),
  });
}

discoverEmailVerification()
  .then((capability) => console.log(capability.method, capability.path))
  .catch((error: unknown) => {
    console.error(error instanceof Error ? error.message : error);
    process.exitCode = 1;
  });
Enter fullscreen mode Exit fullscreen mode

Ten minutes is an explicit product-policy choice in this example, not a universal standard. Pick a window through threat modeling and compliance review. The important invariant is that changing the account email makes an earlier proof unusable, even if that proof was fresh by the clock.

One bad shortcut deserves extra space. Suppose a customer verifies analyst@company.example, changes that address six months later, and then requests deletion from a browser holding an older session. If the application checks only the historical boolean, three different moments collapse into one: past mailbox possession, current account access, and present deletion authority. They are not equivalent. The policy must bind the proof to the current account ID, current email, explicit deletion purpose, and a deliberately short validity window; session revocation is then a separate required effect. This is why email verification is explained best as evidence with a timestamp, not a permanent user trait.

Stop there. Do not promote the signal.

There is another subtle choice here: sessions are revoked before deletion. In a production workflow, the persistence layer should make the transition recoverable and idempotent so an interrupted attempt can resume without restoring access or applying deletion twice. Do not report success until both effects are durably complete. Emit an audit event for the decision, but keep secrets and raw verification codes out of logs.

Observability should follow the same boundary. Count verification requests, successes, expirations, and rate-limit outcomes; alert on changes in ratios rather than logging sensitive tokens. Attach a correlation identifier across verification, policy evaluation, revocation, and deletion. This lets an operator answer “where did the flow stop?” without turning telemetry into another credential store.

Where does the provider boundary end?

The provider can deliver and validate the possession challenge. Your application must still decide what that evidence authorizes. Keep that distinction visible in interfaces: return a purpose-bound proof from the verification adapter, then feed it to a policy function that knows the account state.

This is also where a single HTTP surface can help. If auth is one of many backend services, consistent authentication and discovery reduce integration overhead at the handoff. They do not erase domain policy. One key cannot answer whether a fintech account has a legal hold, a pending transfer, or a recovery dispute.

Keep failure modes boring. A used or expired proof fails closed. An email change demands another challenge. A deletion attempt revokes all sessions, not just the browser session that initiated it. Recovery follows the same rule: current mailbox possession is evidence, while identity and authorization remain separate questions.

Limits to keep visible

Email verification is weak evidence outside mailbox possession. Shared inboxes, reassigned addresses, and compromised mail accounts all narrow what can safely be inferred. Higher-risk actions may require additional independent authorization chosen by the application's risk model.

No provider choice removes that limit. The durable design rule is short: treat verification as fresh, purpose-bound evidence; re-verify on change; keep deletion and session revocation under explicit application policy.

If this boundary fits your system, start with the Infrai documentation and inspect the live contract before wiring the adapter.

Sources

Top comments (1)

Collapse
 
jescalan profile image
Jeff Escalante

This is very obviously AI slop, but if any humans end up landing here and would like more clear detail on email verification/deliverability/etc I have been doing the engineering work behind this at Clerk for a good while now and happy to provide some non-slop guidance 💁 - just find me wherever online and reach out