Summary
The email-code sign-in flow keeps its "a code has been sent" state only in React memory. Any remount — a reload, a restored tab, or returning to a tab the browser discarded while the user was in their mail client — resets the form to its initial state. The verification code field disappears and the user is asked to send a new code.
The code they already received is still valid on the server. There is simply no longer any field to type it into.
Because the code is delivered out of band by email, leaving the page is the normal path through this flow rather than an edge case. On mobile in particular, switching to a mail app and back frequently reloads the tab.
Fanout version
Source build at a1d783f1 (v2026.9.0-1-ga1d783f1), on main.
Reproduction
- Run Fanout with
auth.mode=local and SMTP configured.
- Open the login page, enter a registered email address, and click Send code.
- The form switches correctly: the copy reads "Enter the verification code sent to …", a Verification code field appears, and the button becomes Verify code.
- Reload the page — or switch to another app and return after the browser has discarded the tab.
- The form is back to its initial state: the email field is empty, the Verification code field is gone, and the button reads Send code again.
Expected behavior
Returning to the login page while a code is outstanding should keep the code entry field available, so the code that was already delivered can be used.
Actual behavior
The form resets to the "Send code" state and the delivered code cannot be entered.
The code itself remains valid. Calling the API directly after the reload, with the same address and the code from step 2, succeeds:
POST /api/auth/verify -> 200 {"status":"authenticated"}
GET /api/auth/me -> 200 (session established)
So this is purely lost client state, not a credential or backend problem.
Cause
ui/host/src/auth.tsx:
- Line 91 —
const [codeSent, setCodeSent] = useState(false);
- Line 222 — the Verification code field renders behind
codeSent &&
Nothing persists codeSent or the entered address. There is no sessionStorage, localStorage, cookie, or URL parameter backing the pending-verification state, so every remount starts over.
This is not specific to a local build: the affected chunk (auth-DhIxmh_D.js) is content-hash identical between a local just build and a deployed instance.
Suggested fix
Persist the pending state — the address plus a sent-at timestamp — in sessionStorage, and restore it on mount. Expire the restore after the code's five minute lifetime so a stale tab does not present a field for a code that can no longer work.
Deployment
Source build.
Summary
The email-code sign-in flow keeps its "a code has been sent" state only in React memory. Any remount — a reload, a restored tab, or returning to a tab the browser discarded while the user was in their mail client — resets the form to its initial state. The verification code field disappears and the user is asked to send a new code.
The code they already received is still valid on the server. There is simply no longer any field to type it into.
Because the code is delivered out of band by email, leaving the page is the normal path through this flow rather than an edge case. On mobile in particular, switching to a mail app and back frequently reloads the tab.
Fanout version
Source build at
a1d783f1(v2026.9.0-1-ga1d783f1), onmain.Reproduction
auth.mode=localand SMTP configured.Expected behavior
Returning to the login page while a code is outstanding should keep the code entry field available, so the code that was already delivered can be used.
Actual behavior
The form resets to the "Send code" state and the delivered code cannot be entered.
The code itself remains valid. Calling the API directly after the reload, with the same address and the code from step 2, succeeds:
So this is purely lost client state, not a credential or backend problem.
Cause
ui/host/src/auth.tsx:const [codeSent, setCodeSent] = useState(false);codeSent &&Nothing persists
codeSentor the entered address. There is no sessionStorage, localStorage, cookie, or URL parameter backing the pending-verification state, so every remount starts over.This is not specific to a local build: the affected chunk (
auth-DhIxmh_D.js) is content-hash identical between a localjust buildand a deployed instance.Suggested fix
Persist the pending state — the address plus a sent-at timestamp — in
sessionStorage, and restore it on mount. Expire the restore after the code's five minute lifetime so a stale tab does not present a field for a code that can no longer work.Deployment
Source build.