Skip to content

Verified vs. Onboarded User States

users.verified and users.onboarded are two boolean columns that were added to the users table in the same migration, alongside tos_accepted_at and alert_subscription_status, but they track two unrelated facts about a user and must never be used as a proxy for one another [@onboarding-migration]. onboarded means the user has accepted the terms and conditions. verified means the user has confirmed a location and is therefore eligible for weather alerts. A user can be one without the other in either direction, and the rest of the codebase — query services, broadcast filters, and MCP tools — always filters on both independently rather than collapsing them into a single "is this user set up" flag.

Where each flag gets set

onboarded is set in exactly one place, UserController::acceptTos(), and always alongside tos_accepted_at:

$user->update(['tos_accepted_at' => now(), 'onboarded' => true]);

This is a pure consent action — the user tapped "I agree" — and carries no information about whether Yaya knows where they are [@user-controller].

verified is set alongside alert_subscription_status = Active in every code path that confirms a user's location, not in a single controller action. UserController::updateLocation() sets verified => true and alert_subscription_status => AlertSubscriptionStatusEnum::Active in the same update() call, with the comment "Setting location confirms user and activates alert subscription" [@user-controller]. LocationShareHandler, which fires when a Telegram user taps the location-share button, does the identical update and explains why: "A confirmed location is what makes a user eligible for weather alerts, so mirror every other location-confirming path... mark the user verified, flip the subscription Active, and push the subscription to Supabase — the store that actually drives alert delivery. Without this the coordinates would be saved but no alerts would ever fire" [@location-share-handler]. Both fields are cast to boolean on the User model [@user-model].

Why they cannot be merged

Because the two flags are set by unrelated user actions — accepting terms versus sharing a location — a user can be in any of four states: onboarded and verified, onboarded but not verified (accepted terms, never shared a location), verified but not onboarded (shared a location through a flow that does not require terms acceptance, such as Telegram's location-share button), or neither. Treating onboarded as if it implied verified, or vice versa, would either wrongly include terms-only users in alert-eligibility logic, or wrongly exclude location-verified-but-not-onboarded users from engagement flows that specifically target onboarding completion.

The codebase enforces this by always filtering on both as separate, independent parameters rather than deriving one from the other. UserQueryService applies verified and onboarded as two separate optional where clauses on users.verified and users.onboarded [@user-query-service]. QueryUsersTool, one of the internal MCP tools AI agents and the ops team use to query users, exposes both as distinct optional boolean filters and documents the distinction directly in its schema: verified is described as "only users who have confirmed their location," and onboarded as "only users who have accepted the terms and conditions" [@query-users-tool]. Any new query or report added to this repo should follow the same pattern — expose both as independent filters, and never assume one implies the other.

For the wider vocabulary this state pair sits inside, see Atram, Yaya, FSP, and Rada vocabulary.