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:
-
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.
-
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.
-
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
Problem
PR #13 made handle selection mandatory for all new user signups — every new user is redirected to
/auth/choose-handlebefore 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:
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.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.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-randomin the PAR request or as a query parameter on the/oauth/authorizeredirect. 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.jsonincludes 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 topickerto preserve current behavior). This covers clients that don't specify a preference via A or B.Resolution logic
This gives maximum flexibility:
client-metadata.jsonImplementation notes
complete.tsresolves the handle mode using the precedence chain above:random→ skip handle picker, send callback withouthandleparampickerorpicker-with-random→ redirect to/auth/choose-handlechoose-handle.tsreceives the mode and conditionally renders the "Generate random handle" button (shown only forpicker-with-random)randommode),epds-callbackfalls through to the existing random handle generation path inpds-core/src/index.tsauth_flowtable so it persists across the OTP verification stepclient_idis already available in theauth_flowtable. For clients using a URL-basedclient_id, the metadata is at that URL. This could be cached.choose-handle.tsroute and its tests remain in place — the route simply won't be reached inrandommodePOST /account/handle) remains available regardless of handle mode