ADR-0130: Code-first invites, piloted per property
Date: 2026-08-25 Status: Accepted — removal trigger executed in PR #7300 (code-first is now the default for everyone; the flag and /welcome are deleted) Owner: Fede
Context
During the 2026-08-24 CONAM property-manager onboarding, the invitee could not
sign in for ~40 minutes: every click on her magic link showed
error=INVALID_TOKEN. The reconstructed causal chain (SendGrid per-message
click events, Better Auth session records, Vercel request logs):
- Magic-link tokens are single-use; the first click consumes them.
- The invitee's Microsoft 365 mailbox runs Defender, which machine-clicked
every link 4× within seconds of delivery and re-scanned hourly all night
(0 opens / 20–27 clicks per email — 69 machine clicks across 3 emails).
The scanner's GETs did NOT consume tokens (the
/auth/confirmPOST-form pattern held), but they made the click data unreadable live. - After the one click that DID work, the first-time user was routed to unannounced MFA setup, which both people on the call read as failure — driving the resend-and-reclick loop that burned the remaining links.
- A used link landed on a bare login form with no recovery path.
The structural problem: a single-use artifact in an inbox where machines click everything and humans forward and re-click. No amount of link-flow polish removes that class; a typed 6-digit code does, because scanners cannot type.
Decision
- Invitees of pilot properties get a code-first invite: a welcome email
whose CTA is a plain, tokenless link to
/welcome, where they request an emailed 6-digit code and type it. Nothing in the email is single-use. - The pilot is gated by
CODE_FIRST_INVITE_PROPERTY_IDS(comma-separated property ids; unset = off everywhere). First pilot:appfolio-45(The Willows). - The flow decision moves server-side (
POST /api/admin/users/invite-email), with the same invite authority as the user-create route (requireUser+canInviteRole). The admin UI no longer fires auth emails from the browser. - Two fixes ship unconditionally (not gated): the used-link recovery notice
on
/login?error=INVALID_TOKEN, and a double-submit guard on/auth/confirm.
The gate is a pilot with a removal trigger, not a permanent arm
Hot rule 13 ("no arms by default") is the objection this section answers. The flag exists because the owner explicitly requested a staged rollout on a real customer-facing auth surface ("implement the POC on The Willows and report back before we turn anything on", 2026-08-24). It is scoped to expire:
- Removal trigger: after the Willows pilot validates the flow on real
invitees (target: the first 2–3 real invites complete without support
intervention), the flag is DELETED and code-first becomes the only
first-time invite path — one implementation, per the one-source-of-truth
rule. The magic-link email remains only for returning-user sign-in from
/login. - Owner: Fede. If the pilot stalls >60 days without a decision, the default is to remove the fork in whichever direction the pilot evidence points.
Alternatives considered
- Make invite links reusable or longer-lived. Rejected: a reusable bearer-token link in an email is a credential that survives forwarding — strictly worse security than single-use, and still unreadable under scanner click-storms. Longer expiry was already in place (48h) and was not the failure.
- Disable SendGrid click-tracking on auth emails first. Cheap and queued as its own change (tokens currently land in SendGrid's activity log — a custody problem independent of this ADR). But it only removes the tracking redirect; it does not stop Defender clicking the underlying link, and does nothing for the forward/re-click/MFA-surprise failure modes. It is a complement, not an alternative.
- Ship code-first for everyone immediately, no flag. The end state, and rejected only as the first step: this changes what every future customer receives at their most fragile moment (first contact), and the owner required a report-then-enable gate. The flag above is the bridge to this alternative, with its removal trigger written down.
Verification
Real-network e2e on the preview deployment (2026-08-25, PR #6252): invite →
SendGrid-delivered welcome email verified to contain zero tokens → /welcome
→ real OTP email → typed code → session → announced MFA setup. Plus 14 unit
tests (flag parsing, flow selection, no-token-in-email assertion) and the
adjacent middleware/login/confirm suites.