Skip to content

Let group.register choose the target PDS (per-PDS invite codes) #38

Description

@aspiers

Summary

Let app.certified.group.register create the new group account on a caller-chosen PDS, rather than always the single PDS configured in GROUP_PDS_URL. Today an operator who wants to register new groups on more than one PDS must run multiple CGS instances; import already spans PDSs within one instance, but register does not.

This issue is to decide whether this is worth doing, whether it fits inside register or warrants a separate XRPC, and how to handle the per-PDS invite-code problem.

Current behaviour

register hard-codes the target PDS to the single configured value:

  • src/api/group/register.ts:51const pdsUrl = ctx.config.groupPdsUrl
  • src/api/group/register.ts:54 — the full handle is derived from that PDS's hostname: ${handle}.${pdsHostname}
  • src/api/group/register.ts:76-78 — the invite code comes from a single ctx.config.groupPdsInviteCode
  • src/config.ts:12-13,37-38 — config exposes exactly one groupPdsUrl + one groupPdsInviteCode

By contrast, import (HYPER-469) resolves the PDS from the imported account's own DID document (src/api/group/import.ts:90,100) and stores pds_url per group (src/db/schema.ts:19, finalizeGroup). So the per-group plumbing for multi-PDS already exists — only account creation is pinned to one host.

Motivation

  • A single operator (including the hosted Certified CGS) may want to offer groups on more than one PDS — e.g. a test PDS and a production PDS, or PDSs in different regions/providers — without standing up a separate CGS per PDS.
  • Keeps parity with import, which can already land groups on arbitrary PDSs.

Design question: extend register vs. a new XRPC

Option A — extend register (preferred, pending review). Add an optional pds (or pdsUrl) field to the register lexicon input. When omitted, fall back to today's GROUP_PDS_URL behaviour (fully backwards-compatible). The handler picks the target PDS from an operator-controlled allowlist, derives the handle suffix from that PDS's hostname, and selects the matching invite code.

  • Pros: one method for "create a new group account"; the only axis that changes is where; backwards-compatible via an optional field.
  • Cons: the invite-code/config model has to grow (see below); needs an allowlist so callers can't point register at an arbitrary/hostile PDS.

Option B — separate XRPC (e.g. app.certified.group.registerOn).

  • Pros: keeps the simple method simple.
  • Cons: near-duplicate of register; the only difference is a target-PDS argument, which is a poor reason for a second method. Leaning against this.

Recommendation: Option A, gated by an operator-configured allowlist of permitted PDSs. But this needs a design sign-off before implementation — flagging both options so the call is explicit.

The invite-code challenge

Each PDS may require a different invite code (or none). The current single GROUP_PDS_INVITE_CODE can't express that, so multi-PDS register needs a per-PDS mapping, e.g. config shaped like:

GROUP_PDS_ALLOWLIST = [
  { url: "https://pds1.example",  inviteCode: "..." },   # invite required
  { url: "https://pds2.example" }                         # no invite
]

Notes / constraints this interacts with:

Acceptance criteria (proposed)

  • Decision recorded: extend register (Option A) vs. new XRPC (Option B).
  • If A: register accepts an optional target-PDS field; omitting it preserves current GROUP_PDS_URL behaviour (backwards-compatible).
  • Operator config expresses a per-PDS allowlist with optional per-PDS invite codes.
  • Target PDS is validated against the allowlist; non-allowlisted targets are rejected.
  • Handle suffix and invite code are derived from the chosen PDS.
  • Interaction with [medium] GROUP_PDS_INVITE_CODE stored and forwarded as plaintext — leaked if config is exposed #7 (invite-code secret handling) is addressed for the multi-value shape.

Related


Drafted with Claude Code from a reading of the CGS source at the current checkout. Review the code pointers and design trade-offs before acting.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions