brain/knowledge/decisions/000030-chat-onboarding-asks-only-users-with-no-row-and-backfills-everyone-else.md
The first-run onboarding card ("Who am I teaming up with?") renders when the caller has no chat_personalization row at all, a state the service synthesizes as UNSET and never persists. To keep that from firing for the entire existing user base on ship day, the shipping migration backfills one terminal row per existing user rather than gating the card on a hardcoded ship date.
The card's gate is isEmpty && !incognito && (isFirstRun || promptOpen), where isFirstRun is status === UNSET. UNSET means "no row exists", not "new account", so on first deploy every user who had never answered would have met it, including the grandfathered cloud users from the 200-user chat rollout cap. Those people have been chatting for months and would have opened a normal new chat to find a new-user welcome takeover where their greeting used to be.
The obvious fix is user.created > SHIP_DATE. It works, and it leaves a date constant in the code that nobody ever deletes and no one dares change.
Backfilling puts the fact in the data, where it is true, instead of in a branch that has to keep being true. After the migration UNSET means exactly what it says: a user who has never been asked. The gate needs no second clause, no clock, and no knowledge of when we shipped.
It also keeps the door open in a way the date constant does not. A backfilled row is a real row we can query, so "existing users we never onboarded" is a population we can count and later invite deliberately, rather than a set defined by an inequality against a magic number.
The legacy rows take a distinct terminal status, not the existing SKIPPED. SKIPPED means a user saw the question and declined it, which is a genuine signal about that person. Reusing it would blur those two groups together permanently and make the invite-them-later query impossible to write.
Rejected: gating on user.created against a ship-date constant, for the reasons above.
INSERT ... SELECT. It is the hardest thing in this feature to reverse, since un-writing it cannot distinguish a backfilled row from a real one. The distinct legacy status is what makes it reversible at all.feat/chat-onboarding-personalization, as BackfillChatPersonalizationForExistingUsers. It skips users with a null platformId, which the user table still permits, because the target column is NOT NULL.