The server's half of D25: every account has a way in, and the anonymous accounts left from before go with the daily pruning. - The module signs nobody in anonymously: `anonymousIdp` and `gitHubUpgrade` are gone, and so is linking GitHub at the register. - A password is set only by someone whose address is theirs (a `firstPassword` code) or who signs in with GitHub alone (a fresh sign-in and a `signUp` code); an anonymous account is refused `wrongCredentials` before any code is used. - SimpleLogin refuses to link onto a caller without `identified` (`wrongCaller`), the member's endpoint too, so nothing turns an anonymous account into one with a way in. - `pruneAnonymousPeople` deletes every anonymous account left, however new, never one identified. Learn and Todo schedule it as `prune-accounts` and cancel the old `prune-abandoned-anonymous-people` entry once. - Learn: someone without `identified` keeps no files; `FILE_LIMIT_ANONYMOUS_MB` is gone. `minimumAppBuild` is 23, as builds up to 22 start with an anonymous account. - Learn's sync tests make their person and session by SQL in the development database, as Serverpod stores sessions. - Docs, store listings and ADRs 0010, 0014, 0016, 0017 and 0019 amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
6.4 KiB
6.4 KiB
ADR 0016: One account across an instance's services
- Status: decided 19 Oct 2026, by the developer.
- Builds on: ADR 0010 (email sign-in), ADR 0011 (profile pages and the account domain), ADR 0014 (halkyon_base), ADR 0015 (one repository).
- Followed by: ADR 0017 (password, TOTP and the keys), ADR 0018 (the agenda).
- Amended by: ADR 0020, D25 (24 Oct 2026): no account is anonymous any more, so rule 6 holds only for those left from before, which add no way in; a member no longer tells the register who is anonymous there.
Context
An operator may run several Halkyon services side by side (learn.example.com, todo.example.com, later train. and meditate.). Each has its own server, database and sessions, and until now its own accounts: the same address signed in to Learn and to Todo reached two people, with two handles and two deletions.
The reason to join them is Todo as the agenda of all the others, both ways: Learn's cards due today, Train's planned exercise and Meditate's session appear in Todo, and moving one there moves it in the app it belongs to (ADR 0018). That needs one person behind them all. Handles on the account domain (ADR 0011) need it too: two services handing out handles on one domain would hand the same one to two people.
Decision
- Within one operator's instance only. Across operators would need an OpenID Connect provider with web sign-in pages, browser sessions and consent; not now.
- A register in one service's server. halkyon_base runs in one of two modes. The service the operator names holds the register (on learn.halkyon.eu, Learn's server: it holds the identities and handles already, so nobody's account moves); the others are its members and ask it. With a single service nothing changes: it is its own register.
- One account ID everywhere. A member's own person (its
AuthUser) takes the register's account ID, so a deletion, a handle or an agenda item names the same person in every service. - Each app signs in itself. Apps on one device can't read each other's sessions, so Todo signs in on its own (today with an email code or GitHub, after ADR 0017 with the password and TOTP) and reaches the same account: the member's server runs the sign-in by asking the register (it mails the codes and resolves addresses and GitHub IDs), then opens a session of its own for that account. As Proton's apps do.
- Trust between servers: a secret the instance's services share (
instanceSecret;INSTANCE_SECRETindeploy/.env), sent with every call a member makes; members reach the register at an address the operator gives (its address on the instance's own network when on one host). Nothing a person's device sends can call the register as a member. - Anonymous accounts stay per service. Becoming identified (an address or GitHub added) makes the anonymous person an account of the register under the same ID, or switches to the account the identity belongs to, as linking and switching do today.
- Handles and profiles move into halkyon_base, served by the register (the tables unchanged, as the identities moved in ADR 0014): one handle per account on the account domain, shown alike by every service. What fixes a handle (Learn: the first publish) is the host's, told to the module.
- Deleting the account deletes it everywhere. From any app: a member passes it to the register, which deletes the account and its ways in, then tells every member to delete that person's data; a member it can't reach is told again until it answers, and each member also deletes, as it prunes, people the register no longer knows.
- Released apps: the profile endpoints move, so the change rides on build 21's raise of
minimumAppBuildif it is through its checks by 22 Oct, else on a later release with a raise of its own.
How it is built (19 Oct 2026)
- Modes:
AccountsModein halkyon_base, read from the environment:ACCOUNT_REGISTER_URLmakes a server a member of the register at that web server; otherwise it holds the register, telling the web serversINSTANCE_MEMBERSnames when an account goes. Learn always holds it (learnAccountsrefusesACCOUNT_REGISTER_URL): Explore searches studysets by their authors' handles in the same query as the releases, so the profiles must be in Learn's database. - One interface:
AccountRegister, asked by every endpoint and host function for profiles, codes, GitHub IDs, ways in, deletions and which accounts exist:LocalRegisteron the register,RemoteRegisteron a member (POST of JSON to/halkyon-base/register/<operation>on the register's web server, the secret as a bearer token, compared in constant time), an in-memory one in tests. A member answers the register at/halkyon-base/member/account-gone. Caddy forwards neither. A server doesn't start where it would call another without the secret. - One ID: a person a way in proves is made a person on the member under the
register's account ID (
issueSession); a member's anonymous person adding a way in becomes an account of the register under theirs. - Deleting: notices per member in the account's deletion transaction
(
account_deletion_notice), told at once and hourly, those tried least first, until each confirms; dropped for a member no longer named. A member's daily pruning asks the register about all its identified people before deleting any, and deletes nobody while the register knows none of them, or can't be reached.
Consequences
- The register's server learns when someone signs in to which service of the instance (the privacy page says so); the services already share one operator.
- An operator running one service configures nothing new. One running several names the register, gives every service the shared secret, and the register its members.
- If the register's server is down, members can't sign anyone in, create handles or delete an identified account; sessions already open keep working, and the register's notices wait.
- A restore of the register's database from a backup forgets the accounts made since; a member's pruning would then delete those people's data there, so it waits until they are dealt with.
- Learn's sign-in, profile and deletion code paths go through the module's register mode on learn.halkyon.eu; Todo's through its member mode.
- Passwords, TOTP and end-to-end keys (ADR 0017) and the agenda (ADR 0018) build on this.