ADR-0010 — on a deployed realm, members arrive through onboarding, and a community's organization through the provisioning reconcile
Date: 2026-09-27 Status: accepted
Refines ADR-0006 and
ADR-0008. Neither is superseded:
the registry is still the source of members, a YAML file is still a seed, and each command
still owns one level of the realm. What changes is where the seed and sync-users may
write.
Context
ADR-0006 kept the file path of sync-users working and said it was not deprecated:
"bootstrap runs before the registry holds anything, and an offline run is real". ADR-0008
named three writers of the organizations and users level: the provisioning service,
sync-orgs and sync-users.
A deployed community seeded that way carries accounts nobody signs in with: placeholder
names and addresses, one account per imported member, made from a file rather than from a
person. Repairing them in place, adopting them at approval or replacing them when a meter is
attached each puts migration logic in the product, and was rejected by the requester
(2026-09-27). The decision instead is that a community starts clean: a new registry
community and a new Keycloak organization, managers first, no members, and every person
entering through ../onboarding.
Two things stood in the way:
- Nothing that runs on a deployed realm creates an organization with no members.
sync-usersexits when its source holds no active member, before it ensures anything (src/celine/policies/cli/keycloak/commands/sync_users.py).POST /reconcile/{community}does callensure_communitybefore it walks the members, so an empty community gets its organization, org roles and org groups — but nothing promised that, and nobody holdsprovisioning.reconcile. - Onboarding is the single point of access to the provisioning service (requester,
2026-09-14;
tests/test_provisioning_scope_holders.py), so a second caller ofreconcilewould break the rule this repository tests.
Decision
On a deployed realm, sync-users writes nothing, from either source. The YAML seed and
sync-users are local development: the compose stack, a developer's realm, the fixtures.
ADR-0006's reasons for keeping the file path — a registry that holds nothing yet, an offline
run — are reasons of local development, and stay true there. Its one-parser property stands
unchanged.
The command enforces it: sync-users refuses unless ENV=dev (requester, 2026-09-27;
REQ-0006). It uses the guard seed-dev-users uses, KeycloakSettings.is_production, so
dev, development, local, test and ci run and anything else, unset included, exits 1
naming ENV=dev. The guard runs before a source is read or Keycloak is asked anything, and
covers --dry-run and --check too: a check has no deployed audience once the sync it checks
has none, and one guard with no exceptions is the one that cannot be argued around. Local
stacks export ENV=dev (taskfile.yaml, the compose sync-users service).
A community's organization, org roles and org groups come from
POST /reconcile/{community}, which ensures them even when the registry community has no
active member. The registry community must exist first: a community the registry does not
have is 404 community_not_found, and nothing is created in Keycloak.
Onboarding calls it, and stays the only caller of the provisioning service. The call is the
"set up community" step of onboarding's platform-admin registry sync, run before the sync
writes anything else. So svc-onboarding holds provisioning.reconcile beside
provisioning.participants.write — as an optional scope, requested for the reconcile call
only (ADR-0011) — and is
still the only client other than svc-provisioning
holding a provisioning.* scope. The grant lands in clients.yaml with the onboarding change
that makes the call, not before: a held scope nothing uses is a scope somebody has to explain.
Members arrive through onboarding. Approval calls PUT /participants/{community}/{key},
which files the person in the organization and its viewers group.
A manager is a platform admin's act, by group. celine-policies keycloak
set-user-organization <username> --organization <community> --group managers adds the org
group. Group names are plural — admins, managers, editors, viewers, the whole of
ROLE_HIERARCHY — and the command refuses a group the organization does not have.
A participant who is also a manager onboards like everyone else. With the address their
manager account already carries, provisioning finds that account by email and adopts it; the
invitation answers has_password and sends nothing. The viewers membership that approval
adds must leave the managers membership in place. With a different address they get a
second account, and the registry member points at that one.
So the organizations and users level of ADR-0008 has, on a deployed realm, these writers:
| What | Writer |
|---|---|
| a REC organization, its org roles and org groups | the provisioning service, POST /reconcile/{community}, called by onboarding |
a participant's account, organization and viewers membership |
the provisioning service, PUT /participants/{community}/{key}, called by onboarding |
a manager's managers (or admins) membership |
keycloak set-user-organization, run by a platform admin |
| a data owner's organization | keycloak sync-orgs, unchanged by this record |
Consequences
- A deployment job that ran
sync-usersnow fails with the refusal rather than writing. The fix is to drop the job, not to setENV=devon a deployed realm:ENValso decides whetherkeycloak syncaccepts placeholder secrets. sync-usersloses its deployed audience. The tempting undo is to "just import the managers" from a file on a new community, because it is one command. That is how placeholder accounts arrived in the first place; a manager is an account plus one group, and both have a writer above.- The order is load-bearing: the registry community, then the registry sync (whose first
step is the reconcile), then the managers' groups, then onboarding opens. Adding
managersbefore the organization exists fails, becauseset-user-organizationchecks that the group exists. test_provisioning_scope_holders.pychanges with the grant. The sole-holder test stays as it is; the test that onboarding holdsparticipants.write"and nothing wider" gainsprovisioning.reconcilein the change that grants it..adminstays refused.- A reconcile is a sweep. Holding
provisioning.reconcilelets onboarding provision every active member of any community the registry holds, which it can already do one member at a time. The platform-admin capability on the registry sync is what keeps that to an explicit act. - A manager who is not a participant has no account-creating path in this record: on a deployed realm only onboarding and the provisioning service create accounts. How such an account is made is still open: it is an operation of the clean start's go-live, not of the manager features, and it does not block them — attaching a meter and editing a member's role and area need a manager account to exist, not a product path that makes one.
- Nothing is deleted. Retiring an old community's accounts is an operation outside the product; this repository still has no command that deletes a Keycloak user.