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 methodNew app statusNotes
Name✅ supporteddisplay_name prefix search (user-api)
Username⚠️ migrated but not searchablecolumn + unique index exist (0014_migration_support); ETL maps user_tagusername; search_users still queries display_name only
Email⚠️ deliberately excludednever exposed/searchable (privacy — user-api)
Phone number❌ droppedno column in the new schema; not migrated
QR code (scan/display)❌ not builtwas 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):

FieldPopulated% of users
username (user_tag)1,6922.4%
phone (public.users.phone_number)3970.6%
phone (auth.users.phone, login)00.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

  1. Phone — dropped. Not in the schema, not migrated, 0.6% / 0% usage, enumeration risk.
  2. Email — permanently excluded from public discovery. Email org invites remain a separate admin workflow.
  3. 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 (the User proto does not expose username yet — proto/user/v1/user.proto). Specifics:
    • SearchUsers matches display_name prefix OR username prefix (no email matching).
    • Results expose username so clients can disambiguate same-named users.
    • Keep the existing min-query-length and pagination rules.
  4. 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:740 shows currentUserUid; the scan handler at :628 looks up users.id directly). We deliberately improve on that with the opaque code (revocability + non-enumeration), rather than matching it.

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.