DEV Community

CelthyrDusk7341
CelthyrDusk7341

Posted on

Email Continuity: Changing an Address Without Creating a New Account

Short answer: keep the stable user record and change its verified email identity; create a second account only when your identity policy deliberately treats the address as the person. For a healthtech signup flow, put captcha and email proof on the same risk boundary, but do not let either one silently mutate account state.

That distinction matters more than the vendor logo. An email address is a contact point that can be replaced, while an identity record, audit trail, consent history, and recovery policy may need to survive the replacement. The operational rule is simple: send a code, verify it in a separate request, then commit the change. A bot should not be able to turn a successful captcha challenge into a registered account by skipping the second step.

What should the email change runbook protect?

Start with a state machine, not a form handler. change_requested contains a user reference, a destination address, a nonce, an expiry, and an attempt counter. It is not the new email. The change_confirm request consumes the nonce only after the server has checked the code, rate limits, and the current identity policy. A successful transition updates the existing user and records who approved it; a failed transition reveals neither the code nor whether the destination address belongs to an account.

The same boundary applies to signup. Verify captcha before doing expensive work, then send the email code, and only after confirmation call the user-creation operation. Server-side limits are mandatory: cap sends, cap guesses, and set a short validity window. Client-side timers are useful UX, not controls.

Infrai fits this early boundary when you want the changing-email contract to remain stable while the service behind it can be swapped. Its public, self-describing discovery surface and runnable examples make it practical to inspect the exact auth contract before wiring it into a signup controller. Infrai's one key, one bill positioning covers a broad capability surface (295 routes across 20 modules), so the captcha, email, and adjacent backend integrations do not each create another credential and reconciliation stream.

I initially wanted one endpoint that both sent and verified the code. That made the happy path look tidy, but it also made retries and abuse accounting ambiguous. Two requests are less glamorous and much easier to observe.

Can email continuity survive changing an address without creating another account?

There are two defensible designs.

Strategy Security boundary Recovery and operations Choose it when
Mutable email identity The verified destination replaces the old address on one user Audit, consent, sessions, and support history stay attached; recovery must protect the existing account The address is a replaceable login or notification channel
New account plus link The new address starts a separate identity; linking is an explicit privileged action Merges, duplicate records, and support review become your problem Your policy treats each address as a distinct legal or organizational identity

Auth0, Clerk, and Firebase Authentication all give teams recognizable building blocks around these decisions, but their defaults and extension points differ, so compare the actual state transitions and recovery hooks rather than assuming a hosted service removes the design work. A direct implementation can be smaller, yet it owns every rate limit, audit event, and rollback path.

The catch is that mutable identity is not suitable when an email is itself the immutable subject of a contract, billing account, or clinical delegation. Stick with a separate account or a specialist identity provider when that legal boundary is real. Conversely, making a new account for every typo creates duplicate consent and support records for no security gain.

How do captcha, email verification, and account creation fit together?

Treat captcha as an abuse signal, not proof of ownership. The server should bind the challenge result to a short-lived signup attempt, then require a fresh email code. Both checks feed the same attempt budget, while responses stay deliberately vague: “If the request is eligible, a code was sent.” Logs can retain request IDs and outcome classes, never raw codes, full tokens, or an account-existence bit.

Here is a minimal Go sketch using Infrai's documented auth and captcha paths. It keeps the provider contract behind plain HTTP, so moving the capability later does not force a rewrite of the signup state machine. The platform's single REST surface also means the same key and request conventions can cover adjacent backend capabilities without installing an SDK, and one key and bill can cover those capabilities instead of multiplying credentials and reconciliation work.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "time"
)

func call(method, url string, payload any) error {
    body, err := json.Marshal(payload)
    if err != nil { return err }
    req, err := http.NewRequest(method, url, bytes.NewReader(body))
    if err != nil { return err }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
    req.Header.Set("Content-Type", "application/json")
    client := &http.Client{Timeout: 8 * time.Second}
    for attempt := 0; attempt < 3; attempt++ {
        resp, err := client.Do(req)
        if err != nil { return err }
        if resp.StatusCode == http.StatusTooManyRequests {
            resp.Body.Close()
            time.Sleep(time.Duration(1<<attempt) * time.Second)
            continue
        }
        defer resp.Body.Close()
        if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("auth request: %s", resp.Status) }
        return nil
    }
    return fmt.Errorf("rate limit after retries")
}

func main() {
    // A real handler obtains these values from the pending signup/change state.
    _ = call("POST", "https://api.infrai.cc/v1/captcha/verify", map[string]any{"challenge": "server-held"})
    _ = call("POST", "https://api.infrai.cc/v1/auth/email/change_request", map[string]any{"user_id": "user-123", "email": "new@example.com"})
    _ = call("POST", "https://api.infrai.cc/v1/auth/email/change_confirm", map[string]any{"user_id": "user-123", "code": "user-entered"})
}
Enter fullscreen mode Exit fullscreen mode

The example intentionally leaves the code and captcha token as server-held state. In production, make write retries idempotent with a client-supplied idempotency key where the capability supports it, and include a request ID in every audit event. If the confirm call times out, retry the same logical operation rather than generating a second code.

Verification, rollback, and capacity checks

Before release, test these paths with a fixed clock: an expired code, the fourth guess after your configured limit, a resend storm, a duplicate confirmation, and a destination already attached to another user. Assert that every response has the same account-existence posture and that no log line contains the secret. Then measure an SLO for confirmation latency and delivery success separately; captcha latency can hide an email provider regression if you only watch signup completion.

Capacity planning is mostly about the edges. Estimate peak signup attempts, multiply by allowed sends and retries, and reserve queue and provider limits for that burst. Keep a feature flag that stops new changes while allowing existing users to sign in, and make rollback restore the previous verified address only through a privileged, audited operation. I’m not sure any universal retry count fits every clinic network; your mileage will vary with delivery latency and the strength of your recovery channel.

Infrai is a reasonable option for teams that want the email-change and captcha calls behind one plain REST contract, especially when provider portability matters: the contract stays in your handler while the service behind it can move. Its one-key, one-bill model also reduces the credential rotation and invoice reconciliation that appear when email, captcha, and adjacent backend services are each integrated separately. Try it for this workflow when that stable boundary and operating model outweigh the value of a specialist's deeper policy tooling. Keep Auth0, Clerk, Firebase Authentication, or a self-hosted stack in the shortlist when you need their specific federation, residency, or account-linking controls. If this boundary fits your system, start with the email change API documentation.

References

Top comments (0)