2026-10-19 14:39:00 +02:00
|
|
|
# ADR 0014: What every Halkyon server has, as a Serverpod module of its own
|
|
|
|
|
|
|
|
|
|
- **Status:** accepted
|
|
|
|
|
- **Date:** 2026-10-19
|
2026-10-19 20:39:09 +02:00
|
|
|
- **Amended by:** [ADR 0015](0015-one-repository.md) (2026-10-19): halkyon_shared's packages
|
|
|
|
|
now live in this repository's `packages/`, which the apps take by path. Where this record
|
|
|
|
|
says the module, `halkyon_ui` and `halkyon_app` are pinned by git to signed commits of
|
|
|
|
|
halkyon_shared's main, read: taken by path, a change to the module and its hosts one
|
|
|
|
|
commit.
|
feat(base)!: no anonymous accounts on the server (ADR 0020, D25)
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>
2026-10-24 16:07:27 +02:00
|
|
|
- **Amended by:** [ADR 0020](0020-sign-in-with-simplelogin.md), D25 (24 Oct 2026): the
|
|
|
|
|
module signs nobody in anonymously and adds GitHub to nobody; its pruning deletes the
|
|
|
|
|
anonymous accounts left from before.
|
2026-10-19 14:39:00 +02:00
|
|
|
|
|
|
|
|
## Context
|
|
|
|
|
|
|
|
|
|
Halkyon is to have other services beside Learn (a to-do app, a training app), each a
|
|
|
|
|
Flutter app with a Serverpod server of its own, run by whoever runs an instance. Their
|
|
|
|
|
look already comes from `halkyon_ui` in halkyon_shared. What each of their servers needs
|
|
|
|
|
besides its own features is what Learn's has built since September: signing in
|
|
|
|
|
anonymously, with GitHub and with a code by mail (ADR 0010), the account's ways in and its
|
|
|
|
|
deletion in ADR 0001 rule 6's order, the pruning of what nobody can return to, and what an
|
|
|
|
|
instance tells its apps before anyone signs in (its sign-in ways, its source, the oldest
|
|
|
|
|
app build it works with, where it keeps people's data).
|
|
|
|
|
|
|
|
|
|
The app's side of signing in (its gateway, sessions, the start screen) can only be shared
|
|
|
|
|
once these endpoints are shared: an app's generated client calls its own server's
|
|
|
|
|
endpoints, under their names. The developer chose to share the server side first (19 Oct),
|
|
|
|
|
rather than letting each app's server copy Learn's.
|
|
|
|
|
|
|
|
|
|
Serverpod's way of sharing server code with endpoints, models and tables is a module: a
|
|
|
|
|
server package and a generated client package, which a host project names in its
|
|
|
|
|
`generator.yaml` and reaches as `client.modules.<nickname>`. Learn already runs
|
|
|
|
|
Serverpod's own modules (`serverpod_auth_idp`, `serverpod_auth_core`) and the sync
|
|
|
|
|
engine's.
|
|
|
|
|
|
|
|
|
|
## Decision
|
|
|
|
|
|
|
|
|
|
1. **`halkyon_base`, a Serverpod module in halkyon_shared.** `halkyon_base_server` and
|
|
|
|
|
`halkyon_base_client`, nickname `base`, pinned by git to a signed commit of
|
|
|
|
|
halkyon_shared's main as Learn pins the sync fork. It holds the anonymous, GitHub and
|
|
|
|
|
email sign-ins with their endpoints, models and tables (`email_identity`,
|
|
|
|
|
`email_code_request`), the scopes and `grantIdentified`, the code mails, the account's
|
|
|
|
|
endpoint (`delete`, `signInWays`), `deletePerson`, the pruning functions, and the
|
|
|
|
|
instance's basics (`instance.basics()`, `InstanceBasics`).
|
|
|
|
|
2. **A host configures it in one place, before its pod starts.**
|
refactor(app): instances, sign-in and sync plumbing come from halkyon_app
The instances a device keeps an account on, the first start, the
account switcher, signing in and switching person, the gates that build
the dependencies per instance and per person, and the sync service with
its status, card and banner move to halkyon_app (halkyon_shared, ADR
0014), on halkyon_base's client. Learn supplies its frame (name, word,
icon, theme, launch screen, and the two texts only it can say), its
dependencies, whether the device holds content, and its world map's
country names; every text reads as before. An instance counts only
where its basics name the app `learn`, which learnBase now sets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 16:07:44 +02:00
|
|
|
`HalkyonBase.configure(pod, HalkyonBase(...))`: the app it serves (`app`, which its
|
|
|
|
|
instances name in their basics); the app's name, which sign-in mails name; where the project publishes its source, which the instance names unless its
|
2026-10-19 14:39:00 +02:00
|
|
|
operator sets `SOURCE_URL`; the oldest app build the server works with; and what the
|
|
|
|
|
host deletes of a person who goes. Learn's is `learnBase()`
|
|
|
|
|
(`lib/src/auth/learn_base.dart`), set up in `server.dart`.
|
|
|
|
|
3. **A person's own data is the host's, deleted through a hook.** `deletePerson` ends the
|
|
|
|
|
person's sessions first, then runs the host's deletions inside its transaction, then
|
|
|
|
|
deletes their identities and the person; what must wait until the deletion holds
|
|
|
|
|
(Learn's release repositories) runs after the commit. Learn's hook,
|
|
|
|
|
`deleteLearnData`, deletes their space and its files, profile, discussions, published
|
|
|
|
|
studysets, blocks, what they read, assistant connections and file allowance, as
|
|
|
|
|
`deletePerson` did before. The pruning goes through the same deletion. Each host
|
|
|
|
|
schedules the pruning in its own future call.
|
|
|
|
|
4. **The tables move as they are.** Serverpod (checked with v4.0.4, in a throwaway
|
|
|
|
|
project and module, then on Learn) matches tables by name alone when it compares a
|
|
|
|
|
migration's definitions, and ignores which module owns them. So the moved models keep
|
|
|
|
|
their class, table, column and index names, and Learn's migration
|
|
|
|
|
(`20261019121406578-halkyon-base`, made with `create-migration --empty`) holds no SQL
|
|
|
|
|
but the rows recording each module's version; its definition gives the two tables to
|
|
|
|
|
`halkyon_base`. The rows stay where they are, and the password keys keep their names
|
|
|
|
|
(`emailSignInPepper` among them), so every address's hash stays valid. The device's
|
|
|
|
|
local migration creates the two tables, empty, as it holds every module's tables
|
|
|
|
|
(Serverpod's own auth tables among them); nothing on a device reads them.
|
|
|
|
|
5. **Released apps are told to update, rather than served by wrappers.** A module's
|
|
|
|
|
endpoints are called under its nickname, and its models go over the wire under its
|
|
|
|
|
name (`halkyon_base.EmailSignInRefusedException`), so Learn's released apps (up to
|
|
|
|
|
v0.17.0, build 20) call endpoints that are gone and can't read the refusals. Learn
|
|
|
|
|
keeps no wrappers: `minimumAppBuild` is 21, the first build on the module, and
|
|
|
|
|
Learn's own `instance.info()` stays with every field it had, as released apps read
|
|
|
|
|
`minimumAppBuild` there. Its other Learn fields (the assistant connector, Jellyfin)
|
|
|
|
|
stay Learn's; the fields it shares with `InstanceBasics` are filled from the module.
|
|
|
|
|
6. **Licences as Learn's.** The server package is under the SSPL, as Learn's server; the
|
|
|
|
|
client package under the AGPL, as Learn's client and app; each combines with its
|
|
|
|
|
counterpart in every app as Learn's own packages do.
|
|
|
|
|
|
|
|
|
|
## Consequences
|
|
|
|
|
|
|
|
|
|
- **The server with the module deploys only together with the release of build 21.**
|
|
|
|
|
Until people update, released apps can't sign in, switch accounts or start afresh, and
|
|
|
|
|
say the server needs a newer app. Not before 22 Oct 2026 either: the hackathon's judges
|
|
|
|
|
use v0.17.0 until then.
|
|
|
|
|
- Another Halkyon server gets all of this by naming the module and configuring it, and its
|
refactor(app): instances, sign-in and sync plumbing come from halkyon_app
The instances a device keeps an account on, the first start, the
account switcher, signing in and switching person, the gates that build
the dependencies per instance and per person, and the sync service with
its status, card and banner move to halkyon_app (halkyon_shared, ADR
0014), on halkyon_base's client. Learn supplies its frame (name, word,
icon, theme, launch screen, and the two texts only it can say), its
dependencies, whether the device holds content, and its world map's
country names; every text reads as before. An instance counts only
where its basics name the app `learn`, which learnBase now sets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 16:07:44 +02:00
|
|
|
app shares the app side, `halkyon_app` (19 Oct 2026: the instances, signing in, the
|
|
|
|
|
gates and the sync plumbing, with texts of their own), which sits on the module's client.
|
|
|
|
|
An instance's basics name the app it serves (`app`, set by the host's configuration:
|
|
|
|
|
Learn's is `learn`), and an app accepts only an instance of its own, so another Halkyon
|
|
|
|
|
app's address is refused before the app moves there.
|
2026-10-19 14:39:00 +02:00
|
|
|
- A change to sign-in, deletion or the instance's basics is made in halkyon_shared and
|
|
|
|
|
reaches Learn by moving the pin; Learn's tests of its own deletion and its
|
|
|
|
|
`instance.info()` set the module up as the server does (`useLearnBase`).
|
|
|
|
|
- Serverpod's version moves in the module too, together with every other package.
|
|
|
|
|
- A table holding a person's data in a host gets a line in that host's deletion hook;
|
|
|
|
|
the module can't know it.
|
feat(app): one Profile for every app, the person's own tab first, then each app's (ADR 0014)
The public profile was already one per person for every service of an
instance (the register, ADR 0016); its app side was Learn's alone. It
moves into halkyon_app: the model, the repository on the module's
endpoints, the form, the avatar drawn on the device and the sheet with
someone's profile. They take the Public prefix (PublicProfileRepository,
BasePublicProfileGateway), as Train's own ProfileRepository holds the
body's figures. AppDependencies gains profiles and files, the picker the
avatar is chosen with, which moves out of Learn too.
HalkyonProfile is every app's Profile screen: the person's own tab first,
the same in every app (the account and the ways they sign in, the app's
own cards, then the public profile), then the app's own tabs, folding into
the title. Learn adds its inbox and Statistics. Train adds Progress, Days
and, where nothing records, Plans, and so opens on the person's tab, no
longer on Progress. Todo, which has no Profile tab, opens it from a row at
the top of Browse, where Todoist keeps the account.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 09:44:38 +01:00
|
|
|
|
|
|
|
|
## Amendment (2 Nov 2026): one Profile for every app
|
|
|
|
|
|
|
|
|
|
The developer asked for the profile to be the same in function and look across the
|
|
|
|
|
apps. The server side already was: the register keeps one public profile per person for
|
|
|
|
|
every service of an instance (ADR 0016). The app side was Learn's alone; it moved into
|
|
|
|
|
`halkyon_app`:
|
|
|
|
|
|
|
|
|
|
- **The public profile**: the model, the repository and its gateway on the module's
|
|
|
|
|
endpoints (`PublicProfileRepository`, `BasePublicProfileGateway`), the form
|
|
|
|
|
(`PublicProfileSection`) and the sheet showing someone's (`showProfileOf`). They take
|
|
|
|
|
the `Public` prefix because Train's own `ProfileRepository` holds the body's figures.
|
|
|
|
|
`AppDependencies` gains `profiles` and `files` (the picker the avatar is chosen with).
|
|
|
|
|
- **The screen** (`HalkyonProfile`): the person's own tab first, the same in every app
|
|
|
|
|
(the account and the ways they sign in, the app's own cards, then the public profile),
|
|
|
|
|
then the app's own tabs, the row folding into the title (`FoldingTabs`). Learn adds its
|
|
|
|
|
inbox and Statistics, Train its Progress, Days and Plans; Todo, which has no Profile
|
|
|
|
|
tab, opens it from the first row of Browse, as Todoist keeps the account there.
|
|
|
|
|
- **Chosen without the developer, flagged:** the person's tab comes first in Train too,
|
|
|
|
|
where Profile used to open on Progress, so that every app opens it alike.
|
|
|
|
|
|
feat(app,learn): the device's appearance and language, halkyon_app's, as Learn's Settings show them
The developer asked for Todo, Train and Meditate to offer Learn's
Appearance and Language (ADR 0014's amendment of 4 Nov 2026). Their
model, store, cubit and card move into halkyon_app: Appearance and Look
(a BCP 47 tag, null for the device's language), FileLookStore keeping it
in the app's support folder as JSON written by rename (per device, not
synced, read before the first frame), LookCubit for the apps to provide
above InstanceGate, and LookSettingsCard / DeviceLookCard with the texts
moved from Learn's arbs (look*, the same words). Each app passes the
languages it speaks, named in their own language.
Learn keeps its stored choice where it was, among its DeviceSettings,
and feeds the shared card from them: no migration, so nobody's language
or appearance moves, and nothing changes on its screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 22:30:29 +01:00
|
|
|
|
|
|
|
|
## Amendment (4 Nov 2026): every app's appearance and language
|
|
|
|
|
|
|
|
|
|
The developer asked for Todo, Train and Meditate to offer what Learn's Settings offer
|
|
|
|
|
first: the app's appearance (the device's theme, light, dark) and its language (the
|
|
|
|
|
device's, or one the app speaks, each named in its own language). They had passed a fixed
|
|
|
|
|
look (the system's theme and language). It moved into `halkyon_app`:
|
|
|
|
|
|
|
|
|
|
- **The model** (`Appearance`, `Look`, `AppLanguage`, `localeOfTag`): the appearance and a
|
|
|
|
|
BCP 47 tag, null for the device's language, as Learn's device settings kept them.
|
|
|
|
|
- **Kept on the device, per device** (`FileLookStore`, `<app support>/look.json`): not
|
|
|
|
|
synced, read before the first frame and applied before anyone signs in, as Learn's.
|
|
|
|
|
Alternatives: the person's synced settings (they would apply only after signing in, and
|
|
|
|
|
the start screens would speak another language than the app); shared preferences (a
|
|
|
|
|
plugin more, where a small JSON file written by rename does, as the instances and
|
|
|
|
|
what's new are kept).
|
|
|
|
|
- **Applied** by a `LookCubit` that each app opens in `main` and provides above
|
|
|
|
|
`InstanceGate` (its `readLook`), so `SessionGate`'s start screens and the person's
|
|
|
|
|
`MaterialApp` follow it at once.
|
|
|
|
|
- **Shown** by `DeviceLookCard` (on the cubit) or `LookSettingsCard` (given the look and
|
|
|
|
|
what to do with a choice): two menus, side by side from medium windows, the texts
|
|
|
|
|
halkyon_app's (`look*`, moved from Learn's `settingsAppearance*`, `settingsLanguage*`,
|
|
|
|
|
the same words). Each app passes its own languages (`todoLanguages`, `trainLanguages`,
|
|
|
|
|
`meditateLanguages`, Learn's `appLanguages`), today all three it speaks: English (UK),
|
|
|
|
|
English (US), Deutsch. The card comes first of the app's own settings, right after the
|
|
|
|
|
Sync and operator's cards, as in Learn.
|
|
|
|
|
- **Learn keeps its stored choice where it is**: the appearance and language stay among
|
|
|
|
|
its `DeviceSettings` (`device_settings.json`), its card feeding the shared one. Chosen
|
|
|
|
|
over moving them into `look.json` and reading the old values once, which is less code in
|
|
|
|
|
the end but a migration that, done wrong, loses someone's language; keeping them needs
|
|
|
|
|
none and changes nothing anyone sees. Moving them can follow later, once.
|