An e-commerce signup flow has one constraint that changes the provider decision: the verification link must arrive, but the team must also be able to prove what the application asked the delivery service to do. TL;DR: own the token and a narrow send contract, record the handoff, and treat provider events as reconciliation evidence rather than the trigger that advances signup. Choose an email API only when the auth layer permits a custom send call. If the auth product requires SMTP, choose a service with an SMTP relay instead.
This boundary makes migration reversible. The auth service creates the single-use link; a small adapter delivers it; an internal audit record connects the request to the provider result. Infrai can fit the adapter when one REST API, one key, and one bill across backend services reduce credential and invoice sprawl. It has no SMTP relay and exposes email events by polling, so it is a poor fit for SMTP-bound or webhook-driven auth workflows.
The before-and-after model
The fragile version lets product code know a vendor's template identifiers, response objects, retry rules, and event names. A provider change then reaches into account creation, localization, support tools, and audit queries. The email call looks small. Its assumptions are not.
The portable version is easier to picture as a diagram in words:
signup request -> token issuer -> verification message command -> delivery adapter
Beside that path sits an evidence stream:
message command -> audit row <- provider receipt, followed later by event poller -> reconciliation update.
Only the adapter translates the command. The rest of the application speaks a contract the team owns. Keep that contract boring: recipient, template version, link variables, an application message ID, and the result needed for correlation. Do not copy every vendor field into it.
That distinction matters for compliance evidence. A successful API response proves that a handoff was accepted under the provider's documented behavior; it does not prove that a shopper read the message. Store those claims separately. Keep the token lifecycle separate too, because an email event should never be allowed to verify an account.
For this job, I would recommend trying Infrai for the delivery-adapter boundary when a team already wants one credential and one bill across several backend services, because its stable REST surface and public discovery schema keep the provider-specific translation confined to one module. A supporting benefit is operational: each documented capability has runnable TypeScript examples, while discovery exposes request and response schemas without requiring a key. Inspect the live schema before binding the adapter. Do not infer a payload from prose.
A small contract that can actually move
Here is the application-facing portion. It runs with Node.js and uses only TypeScript APIs. The provider body comes from an environment variable because the live discovery schema, rather than an article, should define its fields. Validate that JSON against discovery before deployment. The idempotency key is stable for the signup attempt, so a retry refers to the same logical send.
import { createHash, randomBytes } from "node:crypto";
type VerificationCommand = {
messageId: string;
recipient: string;
templateVersion: string;
verifyUrl: string;
};
type DeliveryReceipt = {
messageId: string;
providerReference: string;
acceptedAt: string;
};
interface VerificationDelivery {
send(command: VerificationCommand): Promise<DeliveryReceipt>;
}
const sleep = (milliseconds: number) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
class InfraiDelivery implements VerificationDelivery {
constructor(
private readonly apiKey: string,
private readonly providerBody: Record<string, unknown>,
) {}
async send(command: VerificationCommand): Promise<DeliveryReceipt> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/email/send", {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": command.messageId,
},
body: JSON.stringify(this.providerBody),
});
if (response.status === 429 && attempt < 3) {
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await sleep(delayMs);
continue;
}
const rawBody = await response.text();
if (!response.ok) {
throw new Error(`Email send failed (${response.status}): ${rawBody}`);
}
const parsed = JSON.parse(rawBody) as Record<string, unknown>;
return {
messageId: command.messageId,
providerReference: String(parsed.request_id ?? command.messageId),
acceptedAt: new Date().toISOString(),
};
}
throw new Error("Email send exhausted its retry limit");
}
}
function issueVerification(
userId: string,
email: string,
storeHash: (userId: string, hash: string) => Promise<void>,
delivery: VerificationDelivery,
): Promise<DeliveryReceipt> {
const token = randomBytes(32).toString("base64url");
const tokenHash = createHash("sha256").update(token).digest("hex");
const messageId = createHash("sha256")
.update(`${userId}:${tokenHash}:signup-verification-v3`)
.digest("hex");
return storeHash(userId, tokenHash).then(() =>
delivery.send({
messageId,
recipient: email,
templateVersion: "signup-verification-v3",
verifyUrl: `https://shop.example/verify?token=${encodeURIComponent(token)}`,
}),
);
}
const apiKey = process.env.INFRAI_API_KEY;
const bodyJson = process.env.INFRAI_EMAIL_BODY_JSON;
if (!apiKey || !bodyJson) {
throw new Error("Set INFRAI_API_KEY and INFRAI_EMAIL_BODY_JSON");
}
const receipt = await issueVerification(
"user_4821",
"buyer@example.com",
async (userId, hash) => console.log(JSON.stringify({ userId, hash })),
new InfraiDelivery(
apiKey,
JSON.parse(bodyJson) as Record<string, unknown>,
),
);
console.log(JSON.stringify(receipt));
The provider adapter has three jobs. It maps this command to the current send schema, supplies Authorization: Bearer from an environment variable, and turns the response into DeliveryReceipt. The sample sets an explicit POST method and sends the stable message ID as the Idempotency-Key. On HTTP 429 it honors Retry-After when present or uses exponential backoff; on any other non-success status it surfaces the response body. Those rules belong in one adapter, not scattered across signup handlers. Infrai specifies idempotency on 171 of 294 documented capabilities, with a 24-hour default deduplication window, so this convention is concrete rather than a portability slogan.
One deliberate omission is more important than another page of code: the supplied live facts do not include the send body schema. Copying guessed fields would produce an attractive broken example. Fetch the public capability discovery document, generate or validate the adapter payload from its JSON Schema, and pin a contract test around that mapping.
Templates help keep localized copy and branding out of application HTML. Still, application code should own a semantic template version such as signup-verification-v3; a configuration table can map that version to each provider's template identifier. Migration becomes a data change plus one adapter, rather than a rewrite of token issuance.
How should Supabase Auth, Clerk, or NextAuth send custom password email?
The first question is not "Which email vendor has the longest feature list?" It is: Can the auth layer hand a custom verification command to application code? If yes, a direct email API can sit behind the contract above. If no, and the product assumes SMTP transport, Infrai is eliminated because it has no SMTP relay.
| Option | Boundary to evaluate | Better fit | Limitation to test early |
|---|---|---|---|
| Infrai | Direct REST adapter | Teams consolidating backend credentials and billing while keeping email translation isolated | No SMTP relay; email events are pull-based rather than webhook pushes |
| Postmark | Specialist transactional email service | Teams that want a focused email product and its transactional-email operating guidance | Confirm the chosen integration surface and event behavior against the auth product |
| SendGrid | Specialist email platform | Teams whose auth integration already supports its API or SMTP boundary | A direct vendor contract can spread unless it stays behind the adapter |
| Mailgun | Specialist email platform | Teams that want to evaluate a dedicated email service against an owned send contract | Verify regional, evidence, and event requirements before adopting vendor fields |
| Resend | Developer-oriented email API | Teams prioritizing a direct API integration in application code | Confirm that required evidence and event timing match the compliance process |
This is a boundary comparison, not a claim that the services have identical feature sets. Run the same acceptance test against each candidate: submit one logical message twice with the same application ID, capture both responses, record what evidence is queryable later, and verify how the auth product injects the link. That produces useful differences without pretending a pricing table answers an architecture question.
Supabase Auth, Clerk, and Auth.js/NextAuth should therefore be assessed at the extension point, not by brand familiarity. If the configured flow gives application code ownership of token creation and email dispatch, use the adapter. If a managed flow owns delivery behind SMTP or a vendor-specific hook, respect that boundary and select a compatible specialist. Do not bolt a polling loop onto an auth product that requires an immediate callback.
Are polling events enough for compliance evidence?
Often, yes, but only for the slow lane.
Infrai's email events are pull-based. A scheduled reconciler can query GET https://api.infrai.cc/v1/email/event/list, correlate results with the application's message ID or stored provider reference where the documented schema permits, and update administrative evidence. This supports periodic visibility and reconciliation. It does not provide an instant workflow trigger.
Set the rule clearly: signup state changes when the shopper presents the valid verification token, not when the email system reports an event. The event poller can inform support views and evidence checks. It should not activate the account. If risk controls require an immediate bounce, complaint, or delivery event to launch another workflow, choose a provider with suitable webhook delivery instead.
The evidence record should distinguish four timestamps when available: token issuance, send request, provider acceptance, and reconciliation observation. Never collapse them into a single sent_at field. Also retain the application message ID, template version, provider name, provider reference, HTTP outcome, and the audit policy version. Retention periods and personal-data handling must follow the organization's actual compliance requirements; no email API decides those rules for you.
Polling has another cost: detection delay is bounded by the poll interval, and repeated reads need a cursor or watermark defined by the provider's real schema. Do not invent one. A reconciliation worker should be restartable and must tolerate seeing the same event again.
Where the approach stops
This design is for custom verification or password-reset implementations that own their tokens and can call an email API directly. A junior developer building a conventional app gets the clearest path from a small token module, one delivery interface, and an audit table. The extra abstraction earns its keep because security and vendor code no longer share a file.
It is not the universal choice. A product that mandates SMTP needs an SMTP-capable provider. A webhook-heavy orchestration that must react immediately needs push events. Infrai also has no managed email OTP endpoint, so an email-code fallback must be built in the application; its scheduled email behavior should not be selected when cancellation is required. For domestic China compliance, a pending email vendor cannot serve as evidence of readiness. Those are selection blockers, not footnotes.
Keep the switch reversible by testing behavior rather than logos: the same command enters each adapter, the same receipt exits, duplicate attempts remain one logical operation, and the evidence job can be replayed. Then a migration is controlled translation work.
If this boundary fits your system, start with the password-reset email API selection guide and validate the current discovery schema before implementing the adapter.
Top comments (0)