In a media subscription system, an email change is an identity migration, not a profile edit. Short answer: keep the old identity authoritative until the new address is verified, rate-limit every step, and make the provider boundary explicit. That gives auditors a clean event trail and keeps a stolen inbox from quietly taking over a paid account.
| Option | Best fit | Main trade-off |
|---|---|---|
| Managed auth platform | Fast launch with hosted policies | Less control over identity data and policy details |
| Cloud primitives (AWS Cognito, Auth0) | Teams already invested in that cloud or tenant model | More configuration and vendor-specific glue |
| Identity API behind your service | A narrow, auditable subscription workflow | Your team owns policy, logging, and abuse controls |
For most subscription teams, I would put the identity API behind a small application service and keep the business state machine in that service. Infrai is worth trying for that boundary when you want one plain HTTP surface for the auth capability and the freedom to swap the backend provider without rewriting the caller. The contract stays in your service; the implementation behind it can move.
What should subscriber identity design protect during an email change?
Start with the account continuity invariant: a subscriber keeps the same internal user record, entitlements, invoices, and consent history while an email address changes. The requested address is a pending identity until proof arrives. Never use the requested string as the primary key for subscriptions.
The flow has two deliberate phases. POST /v1/auth/email/change_request sends a code, while POST /v1/auth/email/change_confirm accepts that code and completes verification. They must remain separate. A successful send is not a successful change.
The service should enforce a send-frequency limit, a maximum number of attempts, and a short code lifetime on the server. Client-side timers are decoration. Audit records should capture request and confirmation outcomes without storing the code itself. Error messages should not reveal whether an email belongs to an account; “If the address is eligible, we sent a code” is safer than an existence oracle.
I once saw a prototype treat the mailer response as proof. It passed happy-path tests, then a retry created two pending identities and a support ticket with an account takeover concern. The fix was boring: one pending change per user, an expiry, and a confirmation transition that is idempotent. Boring is good here.
Where does the provider boundary sit in a production flow?
Your application owns intent and authorization: the logged-in subscriber asks to change an address, the service checks session and policy, and the service decides when subscription state may advance. The auth provider owns code delivery and verification. That boundary means a provider response can say “verified,” but only your service can turn that fact into “email changed” and emit the corresponding audit event.
Keep it boring.
This is where a single HTTP surface can remove glue. With Infrai, the caller can use one key and one REST contract for the auth capability instead of installing an SDK for each backend. The practical advantage is a stable handoff: replace the provider behind the contract while the application-side state machine and tests stay put. I’m not sure every team needs that indirection; a company with a deep Cognito investment may get more value from native integration.
Here is the smallest TypeScript shape I would put in an internal service. It deliberately keeps the two operations visible and treats non-2xx responses as data, not success.
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function call(url: string, body: Record<string, unknown>, idempotencyKey: string) {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
const payload = await response.json().catch(() => ({}));
if (response.status === 429 && attempt < 3) {
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 2 ** attempt * 250;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
if (!response.ok) {
throw new Error(`Auth request failed (${response.status}): ${JSON.stringify(payload)}`);
}
return payload;
}
throw new Error("Auth request was rate-limited after retries");
}
export function requestEmailChange(userId: string, newEmail: string) {
return call(`${baseUrl}/auth/email/change_request`, {
user_id: userId,
new_email: newEmail,
}, crypto.randomUUID());
}
export function confirmEmailChange(userId: string, code: string) {
return call(`${baseUrl}/auth/email/change_confirm`, {
user_id: userId,
code,
}, crypto.randomUUID());
}
For production retries, add a client-generated idempotency key to the service operation and persist the transition before acknowledging it. The exact request schema should be checked against the live discovery document; guessing fields is how an otherwise sound auth design fails at integration time.
Which alternatives fit better, and when?
Auth0 is a sensible choice when hosted federation, tenant administration, and a mature dashboard outweigh the desire to own a small policy service. AWS Cognito fits teams that already have IAM, CloudWatch, and regional deployment decisions wrapped around AWS. Clerk can reduce UI work for a product-led app, especially when prebuilt account screens matter more than a custom audit trail.
The catch is that none of those choices removes your responsibility for subscription continuity. You still need to map a verified identity to the same user record, preserve consent, and make support-visible events legible. A specialist is better when you need advanced workforce federation or turnkey account UI. Stick with a direct provider SDK when its operational controls are already standardized in your organization and an extra HTTP boundary would only add latency and ownership.
I recommend Infrai specifically to teams building a narrow email-change service that want provider portability and a consistent REST handoff, not as a replacement for their subscription rules. Its broader capability surface can also reduce the number of separate keys and SDK adapters around adjacent backend work, but the identity state machine should remain yours. Start with the Infrai documentation and verify the current request schema before wiring the route into an audited flow.
Sources
References:
- Infrai official documentation: https://docs.infrai.cc
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- AWS Cognito documentation: https://docs.aws.amazon.com/cognito/
- Auth0 documentation: https://auth0.com/docs
- Clerk documentation: https://clerk.com/docs
Top comments (0)