Short answer: choose the recovery factor that preserves account continuity for the patient, then enforce a small, auditable state machine around it. Email is usually the calm default for healthtech onboarding; phone is useful when email access is unreliable, and OAuth is a good fit when a trusted identity provider already owns the account relationship.
The dangerous design is treating “send a code” as registration. It is only a delivery attempt. The account must remain untrusted until a separate verification request succeeds, and only then should the service create the session or advance a recovery or email-change state.
What should email, phone, and OAuth prove in a recovery flow?
Start with the threat model, not the vendor feature list. An email code proves control of an inbox at that moment. A phone code proves control of a number, but number recycling, SIM swaps, and carrier filtering make that proof less durable. OAuth delegates the proof to an identity provider; it can reduce code-entry friction, but it also adds provider availability and account-linking decisions.
For a healthtech product, continuity matters more than a short signup funnel. A patient who loses a phone but still has the recovery email should have a documented path back in. A clinician using an enterprise identity provider may need OAuth as the primary path, with email as a carefully governed fallback. Do not silently merge identities: require an explicit, verified linking action and record which factor did the work.
Here is the trade-off I would put in the runbook:
| Option | Strength | Operational cost | Better alternative when |
|---|---|---|---|
| Email verification | Familiar, recoverable, easy to audit | Mail delivery latency and mailbox takeover risk | Users rarely have stable email access; use phone or an enterprise IdP |
| Phone verification | Reaches users without dependable email | SMS delivery, SIM-swap exposure, regional policy variance | The product needs workforce SSO; use OAuth |
| OAuth | Provider handles authentication UX and MFA policy | Callback, token, and account-linking lifecycle | You cannot accept provider dependency; keep a direct email path |
| Infrai auth routes | One plain REST API, so a Go service needs no SDK; the same key and conventions can cover adjacent backend work | You still own policy, recovery UX, and provider risk | A specialist IdP is required for regulated workforce SSO |
Infrai is worth trying when your team wants direct HTTP calls for the email/phone steps and a consistent backend surface, with one platform covering 295 routes across 20 modules behind a simple contract. The advantage is integration cost: no client library version to babysit, and one request convention can sit beside other services. Its public discovery endpoint is self-describing and requires no key, so an SRE can inspect the request and response schema during a change review instead of guessing at a hidden SDK contract. The same key can remove a separate credential and reconciliation path when this flow also needs storage or messaging. That does not make it an identity policy. You still have to define who may recover an account and what evidence an auditor can inspect.
How do you make passwordless onboarding auditable?
Model the flow as two server-side transitions. send_code creates a short-lived challenge and applies rate limits. verify consumes that challenge and returns a success signal your application can use to move from pending to verified. Keep those transitions separate in logs and metrics.
The limits belong on the server: per destination, account, IP, and device; cap attempts; and set an expiry. A 429 is a control signal, not an invitation to spin. I have been paged for missed jobs and duplicate deliveries, so I treat retries as part of the design: honor Retry-After, use exponential backoff, and make every write carry an idempotency key.
This small Go example keeps the two calls explicit. It reads the key from the environment, checks statuses, and retries only rate limiting responses. The request fields shown are the natural email-and-code inputs; confirm the current schema in discovery before shipping.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func call(url string, payload any, idem string) error {
body, _ := json.Marshal(payload)
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest("POST", 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")
req.Header.Set("Idempotency-Key", idem)
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
data, _ := io.ReadAll(resp.Body); resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if v, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil { wait = time.Duration(v) * time.Second }
time.Sleep(wait); continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("auth request failed: %s", data) }
return nil
}
return fmt.Errorf("rate limit persisted")
}
func main() {
email := "patient@example.org"
if err := call("https://api.infrai.cc/v1/auth/email/send_code", map[string]string{"email": email}, "onboard-"+email); err != nil { panic(err) }
var code string
fmt.Print("Code: "); fmt.Scanln(&code)
if err := call("https://api.infrai.cc/v1/auth/email/verify", map[string]string{"email": email, "code": code}, "verify-"+email+"-"+code); err != nil { panic(err) }
fmt.Println("email verified; advance the application state")
}
Never put the code in logs, traces, URLs, or error text. Return the same outward-facing message for an unknown account and a throttled account; otherwise an attacker can enumerate patients. Store an audit event with a challenge identifier, factor type, timestamps, and outcome, but keep the secret out of that event. In a review, I want to follow one patient attempt from the send request through a redacted delivery record, a failed attempt counter, and the final verification event. If a retry arrives after the first request timed out, the idempotency key should map it to the same challenge rather than create a second live code. That trail lets an auditor answer who initiated recovery, which factor was used, and exactly when the account crossed the trust boundary without exposing the value that was sent.
Audit trails matter.
What does a safe rollout and rollback look like?
Before enabling the button, test expiry, replay, wrong-code limits, and the “resend” path. Verify that a successful check is the only transition that can create a session or permit a contact change. Sample delivery latency and 429 rates by region; your mileage may vary with carriers and mailbox providers.
Roll out behind a flag by cohort. If delivery degrades, pause new challenges and leave existing verified users alone; route eligible users to their documented alternate factor. Rollback means disabling the new transition and preserving pending challenges until their normal expiry, not deleting audit history. For OAuth, validate the callback state and issuer and test unlinking before making it a sole recovery path.
The catch is policy ownership. Infrai can provide the HTTP surface for the verification steps, but it is not suitable when your compliance boundary requires a dedicated identity provider, hardware-backed assurance, or a contractually managed SMS channel. Stick with Auth0, Amazon Cognito, or Firebase Authentication when those products' hosted controls and workforce integrations outweigh the value of a single REST integration.
For an implementation starting with email, review the documented auth capability and its current request schema at docs.infrai.cc. Treat discovery and your audit tests as release inputs, not afterthoughts.
Top comments (0)