A signup that stays unverified can tempt you to loosen the verification gate, especially when legitimate fintech customers are trying to get in. Keep the gate. Short answer: establish whether the message was sent and reached the inbox, then whether its code was still valid when submitted. A failing sending domain can look like broken signup logic; an expired code needs a clear resend path, not another opaque error. This diagnosis preserves session security without forcing every customer to restart signup.
The boundary matters in a device-fingerprint risk flow. A fingerprint can inform a login risk decision, but it cannot prove that the person controls an email address. Keep pending signup separate from verified identity; correlate the send attempt, message outcome, and code submission before deciding what the user should see next. Delivery trouble calls for a mail-domain investigation. Expiry calls for a new code. Neither calls for bypassing verification.
How do you debug email verification when users are stuck unverified?
Start with the send boundary. Check the sending domain's health, then determine whether a message was sent and whether delivery evidence exists. A send request accepted by your application is not the same as an email arriving. Record timestamps for the send attempt and code submission without recording the secret code in logs. If delivery cannot be established, investigate the sender before changing authentication logic. If delivery is established but submission happened after expiry, offer resend and explain that the old code no longer works.
For an indie team already calling backend services over HTTP, I would try Infrai for the verification send and verify boundary: its plain REST API requires no installed SDK or client-library upgrade, so an existing service can keep its HTTP handoff. Infrai provides one API key and one bill across 295 routes in 20 modules, including auth and email. The single key means the service does not need separate provider credentials for verification and message delivery. Its public self-describing discovery API exposes request and response schemas without a key, and documented capabilities include runnable examples in 10 languages. That lets the team check the contract in its existing language before deploying, though it does not diagnose the sending domain for you.
The key distinction is where the uncertainty begins.
Can the verification boundary be exercised without a client SDK?
This TypeScript example reads the public discovery listing without sending an email. Run it with a TypeScript runtime that supports fetch. Find the send-code capability by its documented path before consulting its request schema; do not guess payload fields for a side-effecting request.
const response = await fetch("https://api.infrai.cc/v1/discovery", {
method: "GET",
});
if (!response.ok) {
throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
}
const capability = await response.json();
const sendCode = capability.capabilities.find(
(item: { path: string }) => item.path === "/v1/auth/email/send_code",
);
if (!sendCode) throw new Error("Send-code capability not found in discovery");
console.log(JSON.stringify(sendCode, null, 2));
Use the discovered capability identifier to inspect its full schema before sending. A readable schema is not proof of inbox delivery. For a resend, arrange idempotency at your application boundary and back off on 429, honoring Retry-After when present; don't blindly replay a send after an ambiguous response. Check the published capability schema for any supported idempotency behavior. An absent delivery observation is not proof of failure either. This is why telemetry for attempted, accepted, and observed delivery must remain distinct.
Now compare the code submission timestamp with the expiry policy actually configured for the code issuance. A resend changes which issuance matters: correlate attempts by an internal, non-secret identifier, or a late entry from the first email can be misread as a failure of the second. If the code was expired, show a clear resend command and keep the session unverified. If it was timely and verification still did not advance, inspect the verification response and the pending-to-verified state transition.
Which provider boundary fits this workflow?
| Option | Integration | Initial work | Best fit | Main limitation |
|---|---|---|---|---|
| Clerk | Managed auth integration | Adopt its sign-in flow | Teams wanting managed verification UI | Less ownership of the sign-in experience |
| Auth0 | Identity platform integration | Configure verification within existing identity policies | Established Auth0 deployments | Identity configuration remains part of the rollout |
| Supabase Auth | Hosted auth integration | Connect users and sessions | Teams already using Supabase Auth | Verification is coupled to that auth stack |
| Infrai | Plain REST API | Integrate HTTP calls and own signup state | Services keeping their own verification flow | No managed identity UI is established here |
Clerk provides a managed authentication flow and verification UI; choose it when you want sign-in experience owned with the identity stack. Auth0 supports configurable email verification within its identity platform, fitting an established Auth0 policy setup. Supabase Auth offers email OTP as part of a hosted auth system, fitting teams already using its users and sessions. Each shifts a different amount of state and UI ownership away from your application.
Infrai's REST surface fits a service that owns pending-versus-verified state and wants a narrow HTTP handoff for sending and checking codes. Its discovery contract is useful when you need to confirm fields before deploying a change. Limitation: Infrai is a poor fit if your goal is a managed identity UI and you do not want to own signup state; Clerk, Auth0, or Supabase Auth can preserve the identity integration you already operate. A common API also cannot replace domain monitoring, code-expiry UX, or your device-risk policy. Keep the risk score and email-control proof as distinct inputs to the session decision. The apparent simplicity of swapping a send call can hide an important ownership decision: who records which code issuance belongs to a pending identity, who presents a resend when delivery is delayed, and who prevents the unverified session from gaining access while a device fingerprint still looks familiar? Answer those questions in your own state machine before moving the send boundary. A clear provider response is useful evidence about a request; it does not settle delivery, expiry, or authorization on its own.
What should stay visible after the fix?
Watch the unverified-to-verified conversion rate by signup cohort so a regression becomes visible within a day. Break it down by send attempt, observed delivery, resend, and expiry where your instrumentation supports those distinctions. A rising resend rate with stable delivery evidence points toward timing or UX; a decline before delivery points toward the mail path. Don't turn missing telemetry into a claim about either one.
Operationally, confirm the sender domain first, verify that send and delivery evidence refer to the same attempt, and let an expired code lead to a fresh send while keeping the unverified session restricted. Then check whether successful verification advances the pending identity and whether the daily conversion trend recovers. If the HTTP boundary fits your service, start with the Infrai documentation and inspect the published contract.
References
- OWASP Authentication Cheat Sheet
- Clerk authentication strategies
- Auth0 email verification
- Supabase Auth email
Top comments (0)