Skip to content

Make handle selection on signup optional #22

@aspiers

Description

@aspiers

Problem

PR #13 made handle selection mandatory for all new user signups — every new user is redirected to /auth/choose-handle before their account is created. Different client apps using the same ePDS may have different UX needs around handle selection.

This cannot be a global per-deployment setting because different client apps may want different behavior.

Three handle assignment modes

Per-client configuration should support three possible outcomes for new user signups:

  1. random — handle is auto-generated server-side (e.g. a3x9kf.pds.example.com). The user never sees the handle picker. This is the fastest signup path.

  2. picker-with-random — the handle picker is shown, but includes a "Generate random handle" button. The user can either type their own handle or click the button to get a random one. Good for apps where some users care about their handle and others don't.

  3. picker — the handle picker is shown and the user must choose their own handle. No random option. For apps that want every user to have a meaningful handle.

Recommended approach: A + B + C with precedence

The handle mode should be resolvable from multiple sources with a clear precedence order (highest to lowest):

A (per-request) → B (client metadata) → C (server default)

Option A: Per-request query parameter (highest priority)

The client app includes epds_handle_mode=random|picker|picker-with-random in the PAR request or as a query parameter on the /oauth/authorize redirect. This allows a single client to vary behavior per-request (e.g. a "quick signup" flow vs. a "full profile setup" flow).

Option B: Client OAuth metadata (middle priority)

The client's client-metadata.json includes an ePDS-specific field:

{
  "client_id": "https://app.example.com/client-metadata.json",
  "scope": "atproto transition:generic",
  "epds_handle_mode": "picker-with-random",
  ...
}

This lets each client app declare its default preference without needing any server-side configuration. The auth-service reads the client metadata (already fetched during the OAuth flow) and extracts the field. Unknown values are ignored (fall through to server default).

Option C: Server default (lowest priority)

A deployment-wide default set via environment variable (e.g. EPDS_DEFAULT_HANDLE_MODE=picker, defaulting to picker to preserve current behavior). This covers clients that don't specify a preference via A or B.

Resolution logic

mode = request_param.epds_handle_mode     # Option A
    ?? client_metadata.epds_handle_mode   # Option B
    ?? env.EPDS_DEFAULT_HANDLE_MODE       # Option C
    ?? 'picker'                           # hardcoded fallback

This gives maximum flexibility:

  • Clients that don't care get the server default
  • Clients that want consistent behavior set it in their client-metadata.json
  • Clients that want per-request control pass it as a parameter, overriding everything

Implementation notes

  • complete.ts resolves the handle mode using the precedence chain above:
    • random → skip handle picker, send callback without handle param
    • picker or picker-with-random → redirect to /auth/choose-handle
  • choose-handle.ts receives the mode and conditionally renders the "Generate random handle" button (shown only for picker-with-random)
  • When the user clicks the random button, the picker generates and submits a random handle client-side (or calls an API endpoint to get one)
  • When the handle picker is skipped entirely (random mode), epds-callback falls through to the existing random handle generation path in pds-core/src/index.ts
  • The resolved handle mode needs to be stored in the auth_flow table so it persists across the OTP verification step
  • Reading client metadata for Option B: the auth-service may need to fetch the client's metadata JSON. The client_id is already available in the auth_flow table. For clients using a URL-based client_id, the metadata is at that URL. This could be cached.
  • The choose-handle.ts route and its tests remain in place — the route simply won't be reached in random mode
  • Account settings handle change (POST /account/handle) remains available regardless of handle mode

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions