halkyon/docs/adr/0014-halkyon-base-module.md
Tizian 「ティツィアーン」 1b62c8098c
docs: one repository for every app and shared package (ADR 0015)
The root AGENTS.md holds what applies to every app and package (how
work is done, Serverpod, checks with Melos and the tools, CI); Learn's
own is apps/learn/AGENTS.md, Todo's a note that it is still the
scaffold. The root README introduces the project, Learn's moves to its
folder; LICENSE.md says which licence covers which directory, Todo's
packages unlicensed until decided; one CONTRIBUTING.md. ADR 0015
records the decision; ADR 0014 and 0003 note what moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-22 18:11:04 +02:00

6.4 KiB

ADR 0014: What every Halkyon server has, as a Serverpod module of its own

  • Status: accepted
  • Date: 2026-10-19
  • Amended by: ADR 0015 (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.

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. 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 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 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.
  • 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.