Short answer: For a new property-management app, choose passwordless codes only if delivery and recovery can bear the entire login load. Keep passwords with optional OTP when a buyer requires passwords or mailbox availability cannot support login. An app-owned login contract lets you migrate off a managed provider without making its user IDs, mail configuration, and retry semantics your permanent architecture. One failed delivery can keep a tenant out of the rent portal.
Should a new product use passwordless login or password plus optional OTP?
No password means no new password hash, reset token, or password breach list. It also means an email outage is a login outage when email codes are the only route. A phone code might fit a tenant who seldom checks email, but it moves delivery and recovery decisions to the phone number.
Numbers change.
For staff with access to multiple buildings, authentication must remain separate from authorization: control of a mailbox or handset does not prove authority over unit 4B. Record the mapping between the app's tenant or staff ID and the provider identity in your own data layer. Treat a changed phone number and a changed recovery address as security events, not ordinary profile edits. OWASP's authentication guidance covers throttling and recovery as part of the same problem. If a tenant previously registered an email address for account recovery but now requests a code on a newly registered phone, the app must decide whether that change requires an existing session, a separate verification channel, or staffed review; the code-delivery vendor cannot make that tenancy decision for it.
Passwords plus optional OTP keep a familiar access route for enterprise buyers, some of whom may require passwords. They also retain password storage, resets, and their associated risk even if most users choose codes. Optional OTP is not mandatory second-factor protection when a user can bypass it. Ask buyers what their actual policy requires before removing passwords.
Which contract survives a managed-provider migration?
Keep four application operations stable: start a challenge, verify it, establish an application session, and recover access. A vendor's delivery receipt is not a lease authorization decision. Nor does a successful send prove the code reached a mailbox. The login boundary should produce an application principal that the property data layer can check independently.
Consider the seam between auth and the email on which auth depends. Supabase Auth plus SendGrid entails two signups and two credential sets; you write the glue that connects identity events, mail-domain configuration, delivery monitoring, and your own audit trail. Infrai offers auth and email under one API key and a common REST interface, so the app's contract can stay put while the provider behind it changes. Its public discovery surface supplies request and response schemas, which helps check the two sides of this handoff before replacing an integration. There is a trade-off: one vendor to trust, one bill, and one outage surface. If you require independently operated identity and mail vendors, this combined approach is a poor fit; a separate auth provider and email provider give you that control at the cost of operating the join yourself.
The Python example below uses provider-valid JSON bodies supplied through environment variables, rather than pretending undocumented fields are known. A successful code-request response gates a separate operational email notification using the same key. The notice body must be prepared independently and must never contain the code or the auth response. Set AUTH_SEND_BODY_JSON and EMAIL_NOTICE_BODY_JSON to bodies validated against the provider's schemas; set AUTH_OPERATION_ID and EMAIL_OPERATION_ID to distinct stable IDs for these logical actions. Set INFRAI_BASE_URL to the provider's versioned API base URL. A response confirms acceptance, not delivery.
import json
import os
import time
import urllib.error
import urllib.request
BASE = os.environ["INFRAI_BASE_URL"].rstrip("/")
KEY = os.environ["INFRAI_API_KEY"]
def post(path, payload, operation_id):
body = json.dumps(payload).encode("utf-8")
for attempt in range(4):
request = urllib.request.Request(
BASE + path, data=body, method="POST",
headers={
"Authorization": "Bearer " + KEY,
"Content-Type": "application/json",
"Idempotency-Key": operation_id,
},
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
return json.load(response)
except urllib.error.HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == 3:
raise RuntimeError(f"{path}: HTTP {error.code}: {detail}") from error
retry_after = error.headers.get("Retry-After")
wait = float(retry_after) if retry_after and retry_after.isdigit() else 2 ** attempt
time.sleep(wait)
raise RuntimeError("Retry limit reached")
auth_result = post(
"/auth/email/send_code",
json.loads(os.environ["AUTH_SEND_BODY_JSON"]),
os.environ["AUTH_OPERATION_ID"],
)
if auth_result is not None:
post(
"/email/batch/send",
json.loads(os.environ["EMAIL_NOTICE_BODY_JSON"]),
os.environ["EMAIL_OPERATION_ID"],
)
Keep each operation ID stable across retries of the same action, but never reuse it for a new attempt. Do not send the response object to the notification recipient: it could contain sensitive authentication material. A phone-code migration needs its own verified request schema and delivery policy before this email-based example is adapted.
How do the actual options differ?
| Choice | Useful fit | Limitation to check |
|---|---|---|
| Supabase Auth | An existing Supabase app with documented OTP and password flows | Decide whether its email delivery fits production or requires a second provider |
| Firebase Authentication | Existing Firebase clients using documented phone and email sign-in | Review phone verification and account linking against recovery policy |
| Auth0 | Enterprise identity requirements and configurable passwordless connections | Confirm required connection types and recovery rules before deleting passwords |
| Infrai | One-key auth and email when the app owns its login boundary | A combined vendor concentrates operational dependency; validate request schemas and real delivery |
None of these options decides who can replace a tenant's number or how staff regain access to a locked account. Vendor documentation establishes mechanisms, not your availability target. This is why I would test recovery before arguing over the number of SDKs: a clean integration does not help someone who no longer controls the registered channel.
Delivery is not identity.
What is the smallest defensible rollout?
Reconcile existing identities against leases and staff records before enabling a cohort. Test expiry, resends, throttling, failed verification, unreachable mailboxes, and recovery while the old provider is still available for rollback. Do not silently create two tenant accounts when an old email and a new phone resolve to different people.
Then make the policy choice explicit: passwordless-only if channel recovery is credible and buyers accept it, or password plus optional OTP if password access is required and you can own its storage and reset obligations. Measure successful sign-in and actual delivery separately. An HTTP success is not a login SLO.
References
- https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- https://supabase.com/docs/guides/auth/auth-otp
- https://supabase.com/docs/guides/auth/passwords
- https://firebase.google.com/docs/auth/web/phone-auth
- https://auth0.com/docs/authenticate/passwordless
- https://docs.sendgrid.com/for-developers/sending-email/api-getting-started
Top comments (0)