Verified on a throwaway emulator with a real paired account: signing in on a new
device sent a fully set-up user to CREATE_PROFILE instead of RECOVERY. Not a rare
race — it happened on every run.
The previous commit guessed at the cause and was wrong: it assumed the profile
read *failed*, and retried on exceptions. Instrumenting the decision showed the
read succeeds and returns a document that exists with no fields at all:
DIAG null=false nameBlank=true sexBlank=true couple=false -> create_profile
DIAG null=false nameBlank=true sexBlank=true couple=false -> create_profile
DIAG null=false nameBlank=false sexBlank=false sexLen=33 couple=true -> home
The first two land within a millisecond of sign-in; the real document arrives
215ms later. Sign-in registers the FCM token, and that merge-write materialises
users/{uid} before the profile has synced. Reading from the server does not dodge
it — Source.SERVER returns the same field-less document — so the read has to be
*trusted* before it is acted on, not just sourced differently.
resolveDestination now treats a field-less document as "ask again" rather than
"new user", retrying until one with real identity on it arrives; only that decides.
A read that never succeeds routes Home, which is non-destructive and routes to
Recovery on its own. New users are unaffected: every path that creates a user
writes email and createdAt before onboarding, and a document that genuinely stays
empty still falls through to profile setup.
This mattered because createUser is a whole-document set(), not a merge, and the
CreateProfile submit calls it: tapping through the screen that should never have
been shown overwrites the real displayName with the locked placeholder and drops
coupleId/partnerId, unpairing the couple.
Keeps getUserFromServer for the decision so an offline read throws rather than
answering from cache. Landmine written up as C-AUTH-001 — the general trap is that
exists() == true does not mean populated, and blank fields right after sign-in mean
"not synced yet".
Verified live end to end on the throwaway: sign-in now lands on Recovery, and a
Title-Cased phrase (previously rejected) unlocks to Home, "Connected with Ben".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>