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>
97 lines
6.4 KiB
Markdown
97 lines
6.4 KiB
Markdown
# 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](0020-sign-in-with-simplelogin.md), 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
|
|
|
|
1. **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.
|
|
2. **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.
|
|
3. **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.
|
|
4. **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.
|
|
5. **Trust between servers:** a secret the instance's services share (`instanceSecret`;
|
|
`INSTANCE_SECRET` in `deploy/.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.
|
|
6. **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.
|
|
7. **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.
|
|
8. **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.
|
|
9. **Released apps:** the profile endpoints move, so the change rides on build 21's raise of
|
|
`minimumAppBuild` if 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:** `AccountsMode` in halkyon_base, read from the environment:
|
|
`ACCOUNT_REGISTER_URL` makes a server a member of the register at that web server;
|
|
otherwise it holds the register, telling the web servers `INSTANCE_MEMBERS` names when
|
|
an account goes. Learn always holds it (`learnAccounts` refuses `ACCOUNT_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:
|
|
`LocalRegister` on the register, `RemoteRegister` on 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.
|