ADR-0015 — an intermediate realm holds mirrors of prod's members, written by mirror-members
Date: 2026-11-02 Status: accepted
Refines ADR-0010 and ADR-0008. Neither is superseded: on prod, members still arrive through onboarding and nothing else; each command still owns one level. What is added is a writer of accounts on intermediate realms (staging, preprod), which are deployed realms and which ADR-0010 did not tell apart from prod.
Context
Staging and preprod are internal development instances fed with the pipelines' outputs, and
those outputs are computed from prod's members only. Settlement, points and the webapp key a
person by their Keycloak sub. For an intermediate realm to show prod's outputs to anybody,
it needs accounts whose id is prod's sub for each member, and nothing of the person
behind it: prod's member data must not be copied into a less protected environment.
ADR-0010 says that on a deployed realm members arrive through onboarding. On an intermediate realm that produces native accounts nobody's outputs are computed for, and a person would have to onboard twice, giving their e-mail and name to a non-prod system.
Three facts shaped the decision:
POST /usersassigns its own id;POST /partialImportkeeps theidit is given (measured on Keycloak 26.7.3, 2026-10-30). Partial import needsmanage-realm.- Keycloak drops a user attribute the realm's user profile does not declare, and no realm here declares one. A user attribute cannot mark an account as a mirror.
- Fine-grained admin permissions can grant
impersonate-membersover one realm group to a user policy naming accounts.
Decision
celine-policies keycloak mirror-members writes mirrors of a source realm's members into
an intermediate realm. It reads prod's registry (GET /admin/export/mirror, scope
rec-registry.mirror, client svc-member-mirror of prod's realm) or the same document from a
file, and for every member with an account subject converges one account:
- id = the subject, username = the member key, enabled while the member is active;
- no e-mail, name, credential or attribute;
- in the community's organization (alias = community key) and its
viewersgroup, as provisioning files a participant; - in the realm group
/member-mirrors, which marks it as a mirror and carries the source issuer as itsceline_mirrorattribute.
It never runs on prod. It refuses ENV prod or production, and ENV unset (which
reads as prod), and a target Keycloak whose host is the source issuer's host. The guard runs
before the source is read or Keycloak is asked anything.
It never touches an account it did not make. An account outside /member-mirrors is not
written whatever it collides with; a collision is counted and the run exits 2. A mirror whose
member left prod or went inactive is disabled, never deleted. Organization memberships are
added, never removed.
The team enters a mirror by impersonation. With --impersonator (or
CELINE_MEMBER_MIRROR_IMPERSONATORS), the command keeps a user policy naming those accounts
and a Groups permission on /member-mirrors with impersonate-members, view and
view-members, and nothing that writes. Their names are celine-member-mirror:*, outside the
celine-policies: prefix keycloak sync reads and prunes. The list is the deployment's.
It writes with the target realm's admin client (celine-admin-cli, the credential the
policies shell already holds), not with a client of its own: partial import needs
manage-realm, organization membership manage-organizations and the user writes
manage-users, which is svc-provisioning's realm-wide set. A dedicated client would be a
second realm administrator under another name, and clients.yaml is one declaration for every
environment, so it would also exist in prod's realm, where the command must never run.
So, on an intermediate realm, the organizations and users level of ADR-0010 gains one writer:
| What | Writer |
|---|---|
a mirror of a prod member: account, organization, viewers, /member-mirrors |
keycloak mirror-members, from prod's registry |
| who may impersonate a mirror | keycloak mirror-members --impersonator, the deployment's list |
Why this does not contradict "members arrive through onboarding"
ADR-0010 exists so that no account is made from a file rather than from a person. A mirror is
made from prod, where that person did onboard: the account is the trace of an onboarding that
happened, not a substitute for one. On prod nothing changes and the command refuses to run.
sync-users and the YAML seed stay local development (REQ-0006).
Consequences
- An intermediate realm's own onboarding keeps working for a community no flow reads; its
accounts are outside
/member-mirrorsand the mirror never touches them. - A changed member key in prod is reported, not renamed: the mirror keeps its old username until someone decides. A key reassigned to another subject is reported too.
- A stopped mirror freezes the intermediate realm's membership. The job needs a freshness check like the pipeline mirrors.
- The source issuer is part of the mirror group. Pointing a realm at another source means
a new group, not adopting the old one: the command refuses a
/member-mirrorsthat carries another source. - The tempting undo is a user attribute as the marker, because it reads more naturally.
Keycloak drops it without a word unless the user profile declares it, and a later admin
PUTof the user can lose it; a group membership is kept and is also what the impersonation grant is scoped to.