RP subsystems¶
Multiple subsystems under a single RP. The RP is registered by an admin only; subsystems are auto-registered by trusting the RP (find-or-create). On the phone's consent screen the subsystem name is shown in LARGE text, with the parent RP name below in SMALL text.
Wire (the core idea)¶
No new field is added to the RP call — the relyingPartyName in the body is reused as the subsystem
name. For a registered RP this field was previously overwritten by rp.Name and rendered
insignificant (an impersonation block). Now:
Server (newSession, store.go):
1. Resolves the RP from the secret (Bearer) (CreatorRP) — it does not trust the UUID in the body.
2. Resolves relyingPartyName as a subsystem within that RP via resolveSubsystem (find-or-create).
Key = lower(trim(name)) (name_key), unique per RP.
3. In the session: RPName = rp.Name (authoritative, small text), SubsystemName (large text), SubsystemID.
If relyingPartyName is empty or sent as the same name as the RP → the default subsystem (same name
as the RP). Legacy RPs (e.g. the web demo "Demo Bank") send their own name, so they map directly to the
default — backward-compatible.
First-party (operator) / dev ("") callers have no subsystem (RP not registered) — SubsystemName is
empty and the phone shows only RPName on its own.
Registration and protection¶
- When an RP is registered, a default subsystem with the same name as itself is always created (
Register). - Cap: each RP may have
SMARTID_RP_SUBSYSTEM_MAX(default 50) subsystems. If exceeded, new auto-creation is refused (ErrSubsystemCap→ HTTP 429). Existing subsystems keep working. - The admin can only edit subsystems: rename / merge / deactivate. No manual creation (the RP auto-creates them).
Counters¶
On the subsystem row, auth_count / sign_count / last_used_at are incremented atomically each time
a session ends successfully (OK) (recordSessionActivity, crypto_wired.go). The RP's total = SUM
of its subsystems. The admin dashboard shows "Subsystems (active/total)" + per-subsystem counters within
the RP list.
Endpoints (admin, /v3/admin/*)¶
| Method | Path | Description |
|---|---|---|
| GET | relying-parties/{id}/subsystems |
RP's subsystems + counters (rp:read) |
| PATCH | relying-parties/{id}/subsystems/{sid} |
rename (rp:write) |
| POST | relying-parties/{id}/subsystems/{sid}/deactivate | /activate |
active state |
| POST | relying-parties/{id}/subsystems/{sid}/merge {targetId} |
merge src→dst counters |
Files¶
- DB:
server/migrations/V30__rp_subsystems.sql(+ default backfill for legacy RPs). - Model/repo:
persistence/models.go(Subsystem,SubsystemID/Nameon session),repo/repo.go(Subsystems),repo/inmem,repo/pg,postgres/subsystem_repo.go. - Logic:
service/memory/store.go(resolveSubsystem, admin ops),crypto_wired.go(counters). - Phone: iOS
SessionInfo.subsystemName+ContentView/RequestView; AndroidSessionInfo+RequestView.kt— subsystem LARGE, RP below in SMALL (if the names are the same, only the RP). - Admin:
admin/src/app/(app)/rps/page.tsx(subsystem expansion + management),page.tsx(dashboard).
Security note¶
The subsystem name is free-text supplied by the RP (an RP could give a name like "Google"). Protections: (a) the subsystem is scoped to the RP; (b) the authoritative RP name is always shown below in SMALL text; (c) cap + admin merge/rename/deactivate. This is consistent with the "trust the RP" model.
Push notifications display the subsystem name as the primary one (<subsystem> (<RP>) — when the
name differs from the RP); if empty, the RP. apns.go body + payload includes subsystemName. When the
phone opens, from sessionInfo the consent screen confirms the subsystem in LARGE and the RP below in SMALL.