Friend Discovery Parity
Status
Decided (data + architect/product review). Phone dropped; email permanently excluded from public discovery; username discovery wired as a low-cost parity fast-follow; QR restored as a parity feature (native-first, opaque revocable code). Username and QR are parity follow-ups, not blockers for migration cutover — unless the roadmap owner defines “feature parity” as literally no known UX deltas.
Context
The legacy Flutter app let you add a friend five ways; the new app supports one (display-name prefix). Surfaced during the PWA test pass (Friends flow): a user could only be found by name.
tmp/range_day/lib/components/user/add_friend/add_friend_widget.dart had four text fields plus QR, backed by public.users columns user_tag (username), phone_number, name:
| Legacy method | New app status | Notes |
|---|---|---|
| Name | ✅ supported | display_name prefix search (user-api) |
| Username | ⚠️ migrated but not searchable | column + unique index exist (0014_migration_support); ETL maps user_tag → username; search_users still queries display_name only |
| ⚠️ deliberately excluded | never exposed/searchable (privacy — user-api) | |
| Phone number | ❌ dropped | no column in the new schema; not migrated |
| QR code (scan/display) | ❌ not built | was a legacy feature — a genuine parity gap |
Data — how often were the optional fields actually used?
Measured from the legacy Supabase dump (tmp/range_day_supabase.backup, plain-text pg_dump), n = 71,357 app users (public.users):
| Field | Populated | % of users |
|---|---|---|
username (user_tag) | 1,692 | 2.4% |
phone (public.users.phone_number) | 397 | 0.6% |
phone (auth.users.phone, login) | 0 | 0.0% |
Usernames are real, user-chosen handles (aimbetter, that_sedan_m3, glockiver…) — and they’re preserved by the migration (minus empties / >30 chars / collisions, which the ETL drops). ~97.6% of users have no username, so name search already covers the realistic discovery path for almost everyone.
Analysis per method
- Phone — drop. Not in the schema, not migrated, 0.6% / 0% usage, and phone-based discovery carries the same enumeration risk as email. Settled.
- Email — keep excluded (permanent). It’s the universal identifier ⇒ searchable email is the biggest enumeration vector. Note: inviting people by email is a separate org-admin workflow (org invites), not public discovery — that’s fine and unaffected. Settled.
- Username — cheap now, modest value. The expensive parts (column, unique index, migrated data) are already done. Making it discoverable is just: proto field + extend
SearchUsers+ client UI. Discovery value is modest (2.4% have a handle), but it was a legacy parity method and the handles are already preserved — so it’s a low-cost fast-follow, not a blocker. - QR — the real parity call. It existed in the legacy app, so dropping it is a conscious parity decision, not a non-event. Its value (in-person “scan to add” at the range/event) is independent of the usage stats above.
Decision
- Phone — dropped. Not in the schema, not migrated, 0.6% / 0% usage, enumeration risk.
- Email — permanently excluded from public discovery. Email org invites remain a separate admin workflow.
- Username — wire discovery as a low-cost parity fast-follow. The column + unique index + migrated data already exist; the remaining work is a proto field + generated DTOs + extending
search_users+ client UI (theUserproto does not exposeusernameyet —proto/user/v1/user.proto). Specifics:SearchUsersmatchesdisplay_nameprefix ORusernameprefix (no email matching).- Results expose
usernameso clients can disambiguate same-named users. - Keep the existing min-query-length and pagination rules.
- QR — restore as a parity feature (it existed in the legacy app), per the architect’s security guidance:
- native-first (camera + barcode libs); PWA later (
getUserMedia+ a JS/WASM decoder). - the QR payload is an opaque friend/invite code — never a raw user ID, email, or phone — and codes are revocable, ideally expiring.
- Note: the legacy app embedded the raw user UID in the QR (
add_friend_widget.dart:740showscurrentUserUid; the scan handler at:628looks upusers.iddirectly). We deliberately improve on that with the opaque code (revocability + non-enumeration), rather than matching it.
- native-first (camera + barcode libs); PWA later (
Scope: username and QR are parity follow-ups, not blockers for migration cutover — unless the roadmap owner defines “feature parity” as literally no known UX deltas. The schema-touching / privacy parts of each go through the spec → architect-review path.
Open Questions
- Scheduling only — when to land username search and the QR flow (both parity follow-ups, not cutover blockers). No design questions remain.
Related
- Legacy App Analysis
- user-api
- Backlog concept: Friend Discovery Parity (captured here; no roadmap item scheduled)