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:51 — const 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)
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.
Summary
Let
app.certified.group.registercreate the new group account on a caller-chosen PDS, rather than always the single PDS configured inGROUP_PDS_URL. Today an operator who wants to register new groups on more than one PDS must run multiple CGS instances;importalready spans PDSs within one instance, butregisterdoes not.This issue is to decide whether this is worth doing, whether it fits inside
registeror warrants a separate XRPC, and how to handle the per-PDS invite-code problem.Current behaviour
registerhard-codes the target PDS to the single configured value:src/api/group/register.ts:51—const pdsUrl = ctx.config.groupPdsUrlsrc/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 singlectx.config.groupPdsInviteCodesrc/config.ts:12-13,37-38— config exposes exactly onegroupPdsUrl+ onegroupPdsInviteCodeBy contrast,
import(HYPER-469) resolves the PDS from the imported account's own DID document (src/api/group/import.ts:90,100) and storespds_urlper 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
import, which can already land groups on arbitrary PDSs.Design question: extend
registervs. a new XRPCOption A — extend
register(preferred, pending review). Add an optionalpds(orpdsUrl) field to theregisterlexicon input. When omitted, fall back to today'sGROUP_PDS_URLbehaviour (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.registerat an arbitrary/hostile PDS.Option B — separate XRPC (e.g.
app.certified.group.registerOn).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_CODEcan't express that, so multi-PDS register needs a per-PDS mapping, e.g. config shaped like:Notes / constraints this interacts with:
GROUP_PDS_INVITE_CODEstored/forwarded as plaintext) — now there would be N such secrets in config. Whatever hardening [medium] GROUP_PDS_INVITE_CODE stored and forwarded as plaintext — leaked if config is exposed #7 lands on should cover the multi-PDS shape, not just the single value.registercreating an account means POSTing credentials to the chosen host, so the target must come from an operator-controlled allowlist (cf. the SSRF note already inimport.ts:19-24), never a raw caller-supplied URL.Acceptance criteria (proposed)
register(Option A) vs. new XRPC (Option B).registeraccepts an optional target-PDS field; omitting it preserves currentGROUP_PDS_URLbehaviour (backwards-compatible).Related
group.import(already resolves + stores PDS per group)