halkyon_base: a Serverpod module for sign-in, accounts and instance basics
What every Halkyon server has, taken from Halkyon Learn's: anonymous,
GitHub and email-code sign-in with their endpoints, an account's ways
in and its deletion, the pruning of abandoned anonymous people, of
sign-up attempts and of expired codes, and the instance's basics (the
sign-in ways offered, the source link, the minimum app build, where
people's data and its backups are). Endpoints sit under the nickname
`base`. A host configures it in one place before its pod starts: its
name for the mails, its source link, its minimum build, and hooks that
delete its own data with a person.
The tables keep Learn's names, columns and indexes, so a host moving
onto the module migrates nothing but version rows. Server package under
the SSPL, client under the AGPL, as Learn's are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 14:28:09 +02:00
# halkyon_base
What every Halkyon server has, as a Serverpod module (`halkyon_base_server` , with its
generated client `halkyon_base_client` ): signing in, the account's ways in and its
deletion, the pruning of what nobody can return to, and what an instance tells its apps
before anyone signs in. It comes from Halkyon Learn's server, whose rules it keeps.
## What it provides
Endpoints, under the nickname `base` in a host's client (`client.base.emailCode` ):
| Endpoint | Methods | Who may call |
|---|---|---|
| `anonymousIdp` | `login` (Serverpod's anonymous sign-in, at most 30 new accounts per IP address and hour) | anyone |
| `gitHubUpgrade` | `upgrade` : links a GitHub account to the caller, who becomes `identified` | signed in |
| `gitHubSignIn` | `signIn` : a session for whoever has that GitHub account | without a session |
| `emailCode` | `sendCode` , `verifyCode` : a code by mail; a new address becomes the caller's, another person's gives a switch token | signed in |
| `emailSignIn` | `signIn` : a session for the person a switch token names | without a session |
| `account` | `delete` , `signInWays` | signed in |
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
| `instance` | `basics` : the app it serves, GitHub's client ID if any, whether email sign-in is on, the source link, the oldest app build, where data and backups are | anyone |
halkyon_base: a Serverpod module for sign-in, accounts and instance basics
What every Halkyon server has, taken from Halkyon Learn's: anonymous,
GitHub and email-code sign-in with their endpoints, an account's ways
in and its deletion, the pruning of abandoned anonymous people, of
sign-up attempts and of expired codes, and the instance's basics (the
sign-in ways offered, the source link, the minimum app build, where
people's data and its backups are). Endpoints sit under the nickname
`base`. A host configures it in one place before its pod starts: its
name for the mails, its source link, its minimum build, and hooks that
delete its own data with a person.
The tables keep Learn's names, columns and indexes, so a host moving
onto the module migrates nothing but version rows. Server package under
the SSPL, client under the AGPL, as Learn's are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 14:28:09 +02:00
- GitHub keeps only the numeric GitHub ID, in Serverpod's own table; email keeps only
HMAC-SHA256 of the address, keyed with `emailSignInPepper` (`email_identity` ), and its
codes as Argon2id hashes (`email_code_request` ). Codes are eight Crockford characters,
valid 15 minutes, 3 tries; at most 3 an hour per address and 10 per network.
- Refusals are `GitHubSignInRefusedException` and `EmailSignInRefusedException` , each with a
reason.
- For the host's own code, `package:halkyon_base_server/base.dart` : the configuration,
`tokenManagerBuilders` and `identityProviderBuilders` , the `identified` scope and
`grantIdentified` , `deletePerson` , the pruning (`pruneAbandonedAnonymousPeople` ,
`forgetOldSignUpAttempts` , `forgetExpiredEmailCodes` ), short codes, `sourceCodeUrl` ,
`dataLocation` , `backupLocation` , and the models.
## Taking it into a host
1. Depend on both packages, pinned to a signed commit of `halkyon_shared` :
```yaml
# the server's pubspec.yaml
halkyon_base_server:
git:
url: https://code.halkyon.app/project-halkyon/halkyon_shared.git
path: packages/halkyon_base_server
ref: <signed commit>
# the client's pubspec.yaml: the same with halkyon_base_client
```
2. Name it in the server's `config/generator.yaml` :
```yaml
modules:
halkyon_base:
nickname: base
```
3. Configure it once, before the pod starts, and run its authentication and pruning:
```dart
final pod = Serverpod(args); // the host's generated Serverpod
HalkyonBase.configure(
pod,
HalkyonBase(
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
app: 'todo', // its apps accept only instances naming it; never change it
halkyon_base: a Serverpod module for sign-in, accounts and instance basics
What every Halkyon server has, taken from Halkyon Learn's: anonymous,
GitHub and email-code sign-in with their endpoints, an account's ways
in and its deletion, the pruning of abandoned anonymous people, of
sign-up attempts and of expired codes, and the instance's basics (the
sign-in ways offered, the source link, the minimum app build, where
people's data and its backups are). Endpoints sit under the nickname
`base`. A host configures it in one place before its pod starts: its
name for the mails, its source link, its minimum build, and hooks that
delete its own data with a person.
The tables keep Learn's names, columns and indexes, so a host moving
onto the module migrates nothing but version rows. Server package under
the SSPL, client under the AGPL, as Learn's are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 14:28:09 +02:00
appName: 'Halkyon Todo', // in sign-in mails
upstreamSourceCodeUrl: 'https://code.halkyon.app/project-halkyon/halkyon_todo',
minimumAppBuild: 1, // raise it in the commit that breaks older apps
deletePersonData: [deleteTodosOf], // the host's own data of a person
),
);
pod.initializeAuthServices(
tokenManagerBuilders: tokenManagerBuilders,
identityProviderBuilders: identityProviderBuilders(pod),
);
```
Then schedule the pruning in a future call of the host's own: daily
`pruneAbandonedAnonymousPeople` , hourly `forgetOldSignUpAttempts` and
`forgetExpiredEmailCodes` .
4. A host's deletion (`PersonDataDeletion` ) runs inside `deletePerson` 's transaction,
after the person's sessions ended and before their identities and the person go, in
the order given; it deletes the person's rows in the host's tables, and returns what
has to wait until the deletion is committed (files outside the database), or null.
Pruning deletes through the same function.
5. Create the host's migration. A host taking the tables over from its own models (as
Halkyon Learn does) finds no change once its copies are gone: Serverpod matches tables
by name, and these are identical. `create-migration --empty` then records the module's
version, with no SQL besides.
The passwords it reads: `serverSideSessionKeyHashPepper` ; for GitHub `githubClientId` and
`githubClientSecret` ; for email `emailSignInPepper` and `emailSmtpHost` ,
`emailSmtpPort` , `emailSmtpUser` , `emailSmtpPassword` , `emailFrom` . Never change either
pepper once people sign in: every session or address hash would be lost. The environment:
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
`SOURCE_URL` , `DATA_LOCATION` , `BACKUP_LOCATION` : where the instance keeps people's data
and its backups, each `EU` , a country's code (`DE` ) or a subdivision's (ISO 3166-2,
`US-CA` , `DE-BY` ), the backups also `none` ; where the disks are, not which law governs.
halkyon_base: a Serverpod module for sign-in, accounts and instance basics
What every Halkyon server has, taken from Halkyon Learn's: anonymous,
GitHub and email-code sign-in with their endpoints, an account's ways
in and its deletion, the pruning of abandoned anonymous people, of
sign-up attempts and of expired codes, and the instance's basics (the
sign-in ways offered, the source link, the minimum app build, where
people's data and its backups are). Endpoints sit under the nickname
`base`. A host configures it in one place before its pod starts: its
name for the mails, its source link, its minimum build, and hooks that
delete its own data with a person.
The tables keep Learn's names, columns and indexes, so a host moving
onto the module migrates nothing but version rows. Server package under
the SSPL, client under the AGPL, as Learn's are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 14:28:09 +02:00
## Developing it
`serverpod --no-analytics generate` here after changing a model or an endpoint, and
`serverpod --no-analytics create-migration` after changing a table. Tests need no Docker:
`dart test` here starts Serverpod's embedded PostgreSQL. They read `config/passwords.yaml`
(git-ignored) with throwaway values under `test:` for `database` , `serviceSecret` ,
`serverSideSessionKeyHashPepper` and `emailSignInPepper` .
## Licences
The server package is under the Server Side Public License 1.0, as the Halkyon servers
are; the client package under the GNU Affero General Public License 3.0 or later, as the
Halkyon apps are. So each combines with its counterpart in a host exactly as the host's
own do.