Scout — Full Product Context → feature documentation

User handles

Every Scout profile carries a unique, permanent handle — the name shown on posts, comments, photos and public profiles, and the thing @-mentions resolve against.

Summary

Every Scout profile carries a unique, permanent handle — the name shown on posts, comments, photos and public profiles, and the thing @-mentions resolve against. Handles are derived from the account's email address by a one-time backfill and cannot be changed by the user: no editor ships with this release. Guests keep the generated anon-<animal>-<n> name they already displayed, promoted into a real handle.

Before this existed, members had no name at all. Profile.displayName was set to null on signup and wiped again on guest→member promotion, so what users saw was a computed fallback — the local part of their email address — which two different accounts could produce identically.

Status (shipped / beta-badged / flagged off)

Shipped, unconditional. No feature flag gates handles; they are on for every account. There is no opt-out, because the handle replaces a name that was already being displayed.

User-facing surfaces

There is no surface for changing a handle. See "What this feature does NOT do".

How it works (end-to-end)

  1. DerivationnormalizeHandle() (backend/src/profile/handle.ts) takes the email local part (or a guest's anon-* name) and applies one rule: lowercase · drop the +tag · fold accents via NFKD · strip anything outside [a-z0-9-] · collapse repeated hyphens · trim leading/trailing hyphens · pad to a 3-character minimum.

    saxal28+09524576@gmail.com → saxal28
    jane.mccarthy@gmail.com    → janemccarthy
    A.Lan_Sax+beta@work.co     → alansax
    josé@gmail.com             → jose
    anon-otter-11 (a guest)    → anon-otter-11   (unchanged)
    

    Hyphens are permitted specifically so guests need no special case: no email local part contains one after +tag removal, so allowing them changes nothing for members while leaving anon-otter-11 intact.

  2. Collision handlinghandleCandidates() returns the bare form, then name2 … name9, then a candidate derived from the profile id (janemccarthy-abcdef). The id-derived tail makes the list unexhaustible: the local database has 33 plus-addressed variants of one address, all normalizing to saxal28, and they walk past every numeric suffix.

  3. ClaimingpickHandle() (backend/src/profile/handle-assignment.ts) SELECTs which candidates are already taken and returns the first free one. It reads before it writes deliberately: every caller runs inside a Prisma interactive transaction, and in Postgres a constraint violation aborts the whole transaction, so a try/catch retry loop over candidates cannot work there.

  4. Assignment happens at six auth call sites (backend/src/auth/auth-engine.service.ts): password signup and promotion, guest creation, OAuth signup and promotion, and magic-link signup. Creates pre-generate their own UUID rather than letting the database default it, because the terminal fallback candidate is derived from the id.

  5. Promotion re-derives. When a guest becomes a member, the handle is recomputed from the new email rather than merely surviving — otherwise the account would wear anon-otter-11 permanently, since nothing can edit it.

  6. RenderingdisplayNameFor() (backend/src/patch-photos/username.ts) is handle-first, falling back to the email local part, then displayName, then "Guest". The fallback chain is not dead code: refresh tokens are non-rotating, so a client can present an access token minted before handles shipped.

  7. The JWT carries it. handle is signed into the access token (auth-engine.service.ts:151,172) and read on the client (mobile/src/lib/auth/claims.ts). The claim is optional on both sides for the same non-rotating-refresh reason.

Data model (Prisma)

Distinct from Profile.slug, which is a URL segment that always carries a hash suffix and is minted lazily on first public-profile view.

API surface

None. Handles are read through existing payloads (post/comment bylines, profile responses) and the JWT claim. There is no endpoint to set, change, or search handles.

Key files

Configuration and flags

None. No environment variable or feature flag affects handles.

Edge cases and known limits

What this feature does NOT do

Tests that cover it

Open questions