feat(halkyon,myrmex,train,meditate)!: a register for each app, at halkyon.li and myrmex.li (ADR 0045, step 5)
All checks were successful
CI / check (push) Successful in 46m25s

halkyon.li's deploy under Halkyon Days' names (D13, D23): the compose
files compose.halkyon.yaml, compose.myrmex.yaml and
compose.myrmex-search.yaml, the service myrmex-server, the images
halkyon-halkyon-server, halkyon-halkyon-dictionaries and
halkyon-myrmex-server, the databases halkyon_halkyon and halkyon_myrmex
and the superuser halkyon_halkyon, and every variable under its
service's name (LEARN_ is HALKYON_, TODO_ MYRMEX_). The Compose project
and its network keep halkyon-learn, as the volumes are named after it.

Every server is a register of its own (D7, D9, D20): no
ACCOUNT_REGISTER_URL, INSTANCE_SECRET or INSTANCE_MEMBER_ any more; each
reads its sign-in settings as <APP>_SIGN_UP, <APP>_SIMPLELOGIN_…, and so
on (the mail server alone is shared), signs in with a SimpleLogin app of
its own, and serves its register's pages itself: Myrmex's, Train's and
Meditate's servers add the SimpleLogin callback, the privacy page and
the profile pages, and their Caddy sites forward them.

Halkyon answers at halkyon.li and Myrmex at myrmex.li (D22):
learn.halkyon.li and todo.halkyon.li leave ramiel's HAProxy and Caddy;
Halkyon's and Myrmex's builds, snaps and F-Droid listings connect to the
new addresses, and Halkyon's App Links name halkyon.li.

The switch is a script run once by hand (D24), as the deployed
redeploy.sh can't carry the migration records nor rename an open
database: deploy/host/switch-0045.sh rewrites .env (env-0045.sh: every
secret kept, a decoy key made for each new register), dumps every
database, renames them and the superuser through a temporary superuser
in one transaction, fast-forwards, carries the migration records to the
modules' new names, migrates, copies the developer's account (its keys
and its operator's scope, D21) into the other registers, and starts
everything. deploy/host/link-simplelogin.sh then links each register's
new SimpleLogin identity to that account, found by the four words of its
wait screen. Their SQL was run against PostgreSQL 16 on the databases
the migrations define. redeploy.sh no longer carries records.

BREAKING CHANGE: learn.halkyon.li and todo.halkyon.li no longer answer;
the apps connect to halkyon.li and myrmex.li, and each app's accounts
are its own.

Whats-New: Each app now keeps its own accounts; Halkyon is at halkyon.li and Myrmex at myrmex.li.
Whats-New-De: Jede App führt jetzt ihre eigenen Konten; Halkyon ist unter halkyon.li erreichbar und Myrmex unter myrmex.li.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-13 02:04:48 +01:00
commit b4540e53cd
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
62 changed files with 1812 additions and 608 deletions

View file

@ -9,7 +9,7 @@ Halkyon's apps and the packages they share, in one repository and one pub worksp
- **`apps/<app>/`**: each app's packages, as `serverpod create` lays them out (`halkyon_<app>_server`, `_client`, `_flutter`), plus what is the app's alone (Learn's curator, Learn's content in pure Dart shared with its agent, `halkyone_halkyon_core`, Halkyon's ADR 0044, D49, and the agent, `halkyone_halkyon_agent`, an MCP server for Claude Code that suggests within the studysets shared with it, D51, the scripts writing Learn's assets in `apps/halkyon/tool/`, run from `apps/halkyon`; the apps' icons are the root's `tool/app_icons.py`, ADR 0031). Learn is the app that runs; Train, Todo and Meditate run too (ADR 0030, ADR 0033, ADR 0038), Todo and Meditate not released yet.
- **`packages/`**: what the apps share, each with its README and licence, which the apps take by path (`path: ../../../packages/<package>`), so a change to a shared package and to the apps using it is one commit:
- `halkyone_ui`: Halkyon's look, the theme around cobalt and copper, each app's own colour as its primary (`AppPrimary`, Myrmex's strawberry; ADR 0045, D18), Noto Sans bundled, the window-size classes and the widgets every screen is built of (the navigation bar flush with the foot with its fade, and the rail, a banner above every tab, `NavigationFrame.banner`, `FoldingTabs`' tab row folding into the title), with texts of its own (English, German).
- `halkyone_base_server`, `halkyone_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (a password with TOTP and the resets of ADR 0017, SimpleLogin on `halkyon_oidc`, email codes proving an address; no anonymous accounts since ADR 0020, D25, nor GitHub since D36), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, who may sign up (`SIGN_UP`: open, approval, closed) with the accounts waiting for approval and the instance's operators who approve them (Halkyon Learn's ADR 0021, step 1), the operator's assistants forwarded on halkyone_llm with their grants, each person's budget, the efforts they allow and counted figures, and the instance's privacy page made from its settings (step 2), people's files, kept by space and SHA-256 in the server's private storage with the room each may take and the pruning, for a host that keeps them (`HalkyonBase.files`, `FileStore`: which of its rows name a file and what it takes; Halkyon's ADR 0043; Learn's `asset` rows, Todo's attachments), the spaces people share, with their members, roles (Owner, Editor, Viewer) and invitations, kept in step with the host's sync engine through its `SpaceGrants` (`HalkyonBase.spaces`, Halkyon's ADR 0044, step 2: Learn's `SyncSpaceGrants`; handed on as an Owner's account goes), and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how.
- `halkyone_base_server`, `halkyone_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (a password with TOTP and the resets of ADR 0017, SimpleLogin on `halkyon_oidc`, email codes proving an address; no anonymous accounts since ADR 0020, D25, nor GitHub since D36), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, who may sign up (`SIGN_UP`: open, approval, closed) with the accounts waiting for approval and the instance's operators who approve them (Halkyon Learn's ADR 0021, step 1), the operator's assistants forwarded on halkyone_llm with their grants, each person's budget, the efforts they allow and counted figures, and the instance's privacy page made from its settings (step 2), people's files, kept by space and SHA-256 in the server's private storage with the room each may take and the pruning, for a host that keeps them (`HalkyonBase.files`, `FileStore`: which of its rows name a file and what it takes; Halkyon's ADR 0043; Learn's `asset` rows, Todo's attachments), the spaces people share, with their members, roles (Owner, Editor, Viewer) and invitations, kept in step with the host's sync engine through its `SpaceGrants` (`HalkyonBase.spaces`, Halkyon's ADR 0044, step 2: Learn's `SyncSpaceGrants`; handed on as an Owner's account goes), and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how. halkyon.li shares none since Halkyon Days (ADR 0045, D7, D9): each server there is a register of its own, signing in with its own SimpleLogin app (D20).
- `halkyone_app`: every app's shell on the module's client (ADR 0014): the instances a device keeps accounts on and the first start, signing in and switching person, the gates building an app's dependencies per instance and per person, the sync service and where it stands, held back on a metered network as the device's `MobileDataGate` allows (ADR 0042: Android's flag through halkyone_app's own Android plugin, `HalkyonAppPlugin.java`, in Java so that it applies no Kotlin Gradle Plugin, `FakeNetworkWatch` in `testing.dart`; `mobileDataBannerOf` above every tab in each app's frame; the setting in `SyncStatusCard`; an app's files ask `SyncService.filesAllowed`), the account section of an app's settings (`AccountTiles`), every app's Profile (`HalkyonProfile`, decided 2026-11-02: the person's own tab, the same in every app, with their account, the ways they sign in and their public profile, one for all apps, then the app's own tabs) and people's public profiles (`PublicProfileRepository`, `PublicProfileSection`, `showProfileOf`), the wait screen of an account waiting for approval and the operator's card, badge and screen (ADR 0021), the assistants of an app's settings (`AssistantsCard`: granted, by provider, agreed to, what is left of the person's budget, used, and the person's own keys) and the operator's grants from that screen (`AssistantGrantsScreen`: several assistants to several people at once, each with one budget, D57 to D63), with the repositories asking them through the module, at an effort the operator allows, or a person's own key directly (step 2) and what an app's warning before a private studyset goes names (`providerTermsOf`, `OwnAssistant.onThisMachine`, step 3), About with the instance's source link (`showAppAbout`), people's files on the device and the module's store (`FileCache`, `FileServerGateway`, `SealedFile`, `HLKF`, the format every app seals a file in), the keys of spaces people share, sealed to each member on the device and held to the public keys it pinned on first sight, with a safety code to compare them by (`SharedKeys`, `KeyPins`, `FakeSharedKeysServer` in `testing.dart`; Halkyon's ADR 0044, D3, D18) and a file opened in the system's app (`FileOpener`, `SystemFileOpener`: on Android through `HalkyonAppPlugin.java` and its own FileProvider class, `HalkyonFileProvider`; Halkyon's ADR 0043), the device's own speech recognition (`SpeechInput`, `DeviceSpeechInput`: on Android the voice input app handling `RECOGNIZE_SPEECH` where one is installed, `VoiceInputApp` on each app's `VoiceInput.kt`, `one.halky/voice_input`, FUTO Voice Input for one, which records itself and returns text, else `speech_to_text`; `FakeSpeechInput` in `testing.dart`; none on Linux; Meditate's ADR 0038, D18) and dictation into a text at its cursor, listening again after each pause (`Dictation`, Meditate's ADR 0038, D15; an app that listens declares the microphone and its usage texts itself), what's new after an update (`WhatsNew`, `WhatsNewOnUpdate`, `WhatsNewTile`, from the app's `assets/whats_new/`, Train's ADR 0030, D61), the device's appearance and language, chosen in Settings and applied before anyone signs in (`Look`, `FileLookStore`, `LookCubit`, `DeviceLookCard`; Learn keeps them among its own device settings and shows `LookSettingsCard`; ADR 0014's amendment of 4 Nov 2026), the language every app picks (`resolveAppLocale`) and countries' and subdivisions' names (Unicode CLDR's, bundled), with texts that name the app.
- `halkyone_editor` (ADR 0033, D37, 6 Nov 2026): rich text, the general parts of Learn's editor on our fork of `appflowy_editor`: `RichContent` (the editor's document JSON, which Learn and Todo store), `RichContentEditor`, `RichContentView`, the format bar, links, the clipboard keeping formats, superscript and subscript, and `EditorBlocks`, through which an app adds blocks, marks within lines and their buttons (`basicBlocks` without any), with texts of its own (English, German). What is Learn's alone stays in Learn on top of it.
- `halkyone_content` (pure Dart, Halkyon's ADR 0044, D49): what seals and opens a person's synced content, moved out of halkyone_app, which exports it, so Learn's agent shares it: the envelope and `ContentCipher`, `SealedFile`, `UnlockedAccountKeys` and `newAccountKeys`, shared spaces' gateway, `SharedKeys` and `KeyPins`, `FileCache` and `FileServerGateway`, with their fakes in its `testing.dart` (which halkyone_app's re-exports); and `rich_text.dart`, `RichContent`, moved out of halkyone_editor, which exports it.
@ -18,7 +18,7 @@ Halkyon's apps and the packages they share, in one repository and one pub worksp
- `halkyone_llm` (pure Dart, Halkyon Learn's ADR 0021, D15 and D46): language models' APIs streamed over plain HTTP, no SDKs: Anthropic's Messages API, OpenAI's chat completions (each known host's fields, D45) and its Responses API, behind one interface (`LlmClient`), with `FakeLlmClient` for tests; tested on replies written from the providers' documentation and on those `tool/record_llm.dart` records with a key (D36). The module forwards the operator's assistants with it, and the apps call a person's own provider. BSD-3-Clause as `halkyon_oidc`, `publish_to: none`.
- `halkyon_oidc_server`, `halkyon_oidc_client` (ADR 0023): signing in with OpenID Connect, with a SimpleLogin preset, as a Serverpod module that halkyone_base builds on; `halkyon_oidc_flutter`, the device's half: the provider's page opened, the way back caught (the app's scheme, or the loopback on Linux and Windows, `LoopbackWebAuth`) and its state checked. Nothing of Halkyon's is in it, so that others can use it with Serverpod: under BSD-3-Clause, Serverpod's own licence, where the other packages are AGPL; `publish_to: none` until it is published to pub.dev, which is asked again first.
- **`third_party/`**: others' packages patched for us, each a pub release copied whole with only what its README names changed, overridden by path in the root `pubspec.yaml` (today the two Android plugins still applying the Kotlin Gradle Plugin themselves, which AGP 9's built-in Kotlin refuses soon); not our code, so left out of the analysis and the format check; the images drop its overrides (`tool/workspace_only.sh`): they have Dart without Flutter, and pub refuses an override depending on the Flutter SDK, used or not. Drop an override once its package's release has the change.
- **`deploy/`**: the instance deploy at the root, learn.halkyon.eu's today (Learn's `AGENTS.md`, *CI and deploys*), where other services' compose files join Learn's (Train's `compose.train.yaml`, Todo's `compose.todo.yaml`, each switched on in `.env`). **`snap/`**: what the apps' snaps share (`snap/local/`, the secret backend); each app's recipe is its own (`apps/halkyon/snap/`, `apps/myrmex/snap/`, `apps/train/snap/`), which the release tool builds each in a clone of the repository with it in place of `snap/`, as the build needs the repository whole (ADR 0045, step 3; Myrmex's `AGENTS.md`, *Releases*). **`docs/adr/`**: the decision records of every app and package, one numbering. **`tool/`**: the repository's tools (below).
- **`deploy/`**: the instance deploy at the root, halkyon.li's (Halkyon's `AGENTS.md`, *CI and deploys*), where other services' compose files join Halkyon's (Train's `compose.train.yaml`, Myrmex's `compose.myrmex.yaml`, Meditate's `compose.meditate.yaml`, each switched on in `.env`); `deploy/host/switch-0045.sh` switched it to Halkyon Days' names and registers once (ADR 0045, D24). **`snap/`**: what the apps' snaps share (`snap/local/`, the secret backend); each app's recipe is its own (`apps/halkyon/snap/`, `apps/myrmex/snap/`, `apps/train/snap/`), which the release tool builds each in a clone of the repository with it in place of `snap/`, as the build needs the repository whole (ADR 0045, step 3; Myrmex's `AGENTS.md`, *Releases*). **`docs/adr/`**: the decision records of every app and package, one numbering. **`tool/`**: the repository's tools (below).
- **One workspace, one lock**: the root `pubspec.yaml` lists every package, `pubspec.lock` holds one version of each dependency for all of them, and its `dependency_overrides` hold for every app (the offline sync fork, Learn's `AGENTS.md`; `cli_util`, below). **Serverpod's version moves as one**: every package pins Serverpod's packages to the same version (4.0.4), the module's, Meditate's, Todo's and Train's too, together with the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`) and the sync packages Learn's server depends on.
## How work is done here

File diff suppressed because one or more lines are too long

View file

@ -5,7 +5,7 @@
#
# docker build -f apps/halkyon/halkyone_halkyon_curator/Dockerfile .
#
# A container of its own beside the server (deploy/compose.learn.yaml, decided 2026-10-09):
# A container of its own beside the server (deploy/compose.halkyon.yaml, decided 2026-10-09):
# the curator (AGPL) never enters the server's image or process, and a conversion
# that fails can't touch the server. They share only the folder of files.

View file

@ -54,7 +54,7 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Links to join a shared studyset (Halkyon's ADR 0044, D39), opened by the
app itself (App Links): on learn.halkyon.eu, whose assetlinks.json names
app itself (App Links): on halkyon.li, whose assetlinks.json names
this app's key (deploy/Caddyfile); go_router takes /join/<token>. -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter android:autoVerify="true">
@ -63,7 +63,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="learn.halkyon.eu"
android:host="halkyon.li"
android:pathPrefix="/join/" />
</intent-filter>
</activity>

View file

@ -1,11 +1,12 @@
# Configuration for the production instance learn.halkyon.eu.
# Configuration for the production instance halkyon.li (Halkyon Days' ADR 0045, D22;
# learn.halkyon.eu, then learn.halkyon.li before it).
# Caddy terminates TLS on 443 and forwards to the API server on 8080, and the
# web server's paths to it on 8082; Insights doesn't run.
# Configuration for the main API server.
apiServer:
port: 8080
publicHost: learn.halkyon.eu
publicHost: halkyon.li
publicPort: 443
publicScheme: https
@ -15,25 +16,25 @@ apiServer:
# port through an SSH tunnel, never through Caddy.
# The web server serves profile pages (ADR 0011), the dictionaries devices
# download (ADR 0012), SimpleLogin's forwarding page (ADR 0020) and the calls
# between the instance's services (ADR 0016). Caddy forwards the paths of the
# first three alone to it (deploy/Caddyfile), never the last; everything else
# stays on the API server.
# download (ADR 0012), SimpleLogin's forwarding page (ADR 0020), the privacy
# page, the join pages, and the calls between the instance's services (ADR
# 0016), where it has any. Caddy forwards the pages' paths alone to it
# (deploy/Caddyfile), never the calls; everything else stays on the API server.
webServer:
port: 8082
publicHost: learn.halkyon.eu
publicHost: halkyon.li
publicPort: 443
publicScheme: https
# The database: the `postgres` service of deploy/compose.learn.yaml, reachable only inside
# The database: the `postgres` service of deploy/compose.halkyon.yaml, reachable only inside
# the Compose network (it publishes no port), so no TLS between server and database.
# The password comes from the environment (SERVERPOD_DATABASE_PASSWORD, set in
# deploy/.env); there is no production passwords.yaml.
database:
host: postgres
port: 5432
name: halkyon_learn
user: halkyon_learn
name: halkyon_halkyon
user: halkyon_halkyon
requireSsl: false
#isUnixSocket: true # defaults to false
#maxConnectionCount: 10 # Defaults to 10, a negative or null value removes the limit

View file

@ -3,7 +3,7 @@ import 'package:halkyone_halkyon_server/src/dictionary/dictionary_loader.dart';
import 'package:serverpod/serverpod.dart';
/// Rereads the dictionary folder and loads the files that changed: the
/// dictionaries service replaces them weekly (`deploy/compose.learn.yaml`); the
/// dictionaries service replaces them weekly (`deploy/compose.halkyon.yaml`); the
/// server schedules it hourly when it starts (`server.dart`): an hour
/// with nothing changed costs it a hash of each file.
class DictionaryReloadFutureCall extends FutureCall {

View file

@ -3,7 +3,7 @@
import '../../tool/src/release/app_release.dart';
/// Halkyon's releases: the APK and the snap, both connecting to
/// learn.halkyon.eu, the project's instance, published on code.halkyon.app,
/// halkyon.li, the project's instance, published on code.halkyon.app,
/// in the project's F-Droid repository and in the Snap Store's edge channel.
const halkyonRelease = AppRelease(
id: 'halkyon',
@ -12,7 +12,7 @@ const halkyonRelease = AppRelease(
directory: 'apps/halkyon',
flutterPackage: 'apps/halkyon/halkyone_halkyon_flutter',
artifactName: 'halkyon',
serverUrl: 'https://learn.halkyon.eu/',
serverUrl: 'https://halkyon.li/',
forgejo: 'https://code.halkyon.app',
repository: 'halkyon-days/halkyon',
// A PKCS #12 keystore outside the repository (also in the maintainer's

View file

@ -6,7 +6,7 @@
# recipe was the root's snap/). By hand, the same: in such a clone, `snapcraft pack`.
#
# It builds what is committed (the source is the clone's HEAD, the tag), never a
# working tree with its build caches, and always for learn.halkyon.eu. Flutter is
# working tree with its build caches, and always for halkyon.li. Flutter is
# pinned like the local and CI one (FLUTTER_VERSION in .forgejo/workflows/ci.yaml):
# snapcraft's own Flutter plugin can only take a channel and can't pass
# --dart-define, so the build is spelled out below.
@ -32,7 +32,7 @@ description: |
merges that keep your own edits.
You sign in with SimpleLogin, by Proton, or with an email address, a
password and an authenticator app. It connects to learn.halkyon.eu, the project's
password and an authenticator app. It connects to halkyon.li, the project's
instance, or to any other: everything an instance needs is in the repository,
to run your own. The app is AGPL-3.0 and the server SSPL-1.0, so all of its
code is public.
@ -136,7 +136,7 @@ parts:
craftctl set version="$(sed -n 's/^version: *\([^+]*\).*/\1/p' pubspec.yaml)"
flutter pub get --enforce-lockfile
flutter build linux --release \
--dart-define=SERVER_URL=https://learn.halkyon.eu/
--dart-define=SERVER_URL=https://halkyon.li/
mkdir -p "$CRAFT_PART_INSTALL/bin"
cp -r build/linux/x64/release/bundle/. "$CRAFT_PART_INSTALL/bin/"
# ffmpeg's speech recognition (libsphinxbase) links libblas.so.3 and

View file

@ -3,11 +3,11 @@
What holds for every app and package of this repository is in the [root AGENTS.md](../../AGENTS.md); it applies here in full, and Learn's patterns (`apps/halkyon/AGENTS.md`) wherever Meditate needs the same thing. What Meditate is and why: [ADR 0038](../../docs/adr/0038-halkyon-meditate.md) (proposed 3 Nov 2026, built by an agent without the developer's interjection; its decisions await their reading).
- **Packages**: `halkyone_meditate_server`, `halkyone_meditate_client` (generated, never edited), `halkyone_meditate_flutter`; app ID `app.halkyon.meditate` on every platform; no web build. Licences as Learn's (server SSPL, client and app AGPL; `LICENSE.md`). Copied from Todo's tree on 3 Nov 2026 and stripped to the shell, so its frame, keys and sealed rows are Todo's and Train's.
- **The server** includes `halkyone_base` (ADR 0014) and configures it in `server.dart` (`meditateBase()`, `lib/src/meditate_base.dart`): its basics say it serves `meditate` (never change it once apps use it), sign-in mails name Halkyon Meditate, the source is this repository, `minimumAppBuild` 1, a person's space deleted with them (`deletePersonData: [deleteMeditateData]`, `lib/src/meditate_data.dart`). **Accounts** (ADR 0016): Meditate holds its own register when it runs alone, and is a member of the instance's register where the operator sets `ACCOUNT_REGISTER_URL` (`AccountsMode.fromEnvironment()`) with the instance's shared `INSTANCE_SECRET`; `INSTANCE_MEMBER_MEDITATE` names it to the register. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled once each. Synced tables (ADR 0038, D7; `database: sync`, every row sealed): `content_key`, `meditate_settings` (one row a person, under an ID keyed by the account), `sitting`, `dream` (D14) and `lesson_done` (D16), each indexing `spaceId`. Migrations: `initial` (Serverpod's, the auth modules', the module's and the sync engine's tables), `sittings` (the three tables), `dreams` (`dream`) and `library` (`lesson_done`). Tests: `test/integration/meditate_server_test.dart` (the module's endpoints through the server's own dispatch, `halkyone_base.<endpoint>`, and the sync engine's connector refusing calls without a session), `test/integration/sitting/person_deletion_test.dart`, the library's in `test/unit/library/` (the manifest, the files over HTTP) and `test/integration/library/` (the endpoint).
- **The server** includes `halkyone_base` (ADR 0014) and configures it in `server.dart` (`meditateBase()`, `lib/src/meditate_base.dart`): its basics say it serves `meditate` (never change it once apps use it), sign-in mails name Halkyon Meditate, the source is this repository, `minimumAppBuild` 1, a person's space deleted with them (`deletePersonData: [deleteMeditateData]`, `lib/src/meditate_data.dart`). **Accounts** (ADR 0016): Meditate holds its own register when it runs alone, and is a member of the instance's register where the operator sets `ACCOUNT_REGISTER_URL` (`AccountsMode.fromEnvironment()`) with the instance's shared `INSTANCE_SECRET`; on halkyon.li it holds its own since Halkyon Days (ADR 0045, D9), with its own SimpleLogin app and `MEDITATE_…` settings. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled once each. Synced tables (ADR 0038, D7; `database: sync`, every row sealed): `content_key`, `meditate_settings` (one row a person, under an ID keyed by the account), `sitting`, `dream` (D14) and `lesson_done` (D16), each indexing `spaceId`. Migrations: `initial` (Serverpod's, the auth modules', the module's and the sync engine's tables), `sittings` (the three tables), `dreams` (`dream`) and `library` (`lesson_done`). Tests: `test/integration/meditate_server_test.dart` (the module's endpoints through the server's own dispatch, `halkyone_base.<endpoint>`, and the sync engine's connector refusing calls without a session), `test/integration/sitting/person_deletion_test.dart`, the library's in `test/unit/library/` (the manifest, the files over HTTP) and `test/integration/library/` (the endpoint).
- **The app** is halkyone_app's `InstanceGate` and `SessionGate` with Meditate's frame (`meditateFrame`) and `ServerpodInstanceChecker(app: 'meditate')`; `MeditateDependencies` per instance, `MeditatePerson` per person (their sync, `SettingsRepository`, `SittingRepository`, `DreamRepository`, `LibraryRepository`, `LessonRepository`, and the device's `BellPlayer`, `GuidePlayer` and `ScreenAwake`); SimpleLogin's way back on `app.halkyon.meditate://` and the loopback port 43826. The person's app (`MeditatePersonApp`) washes in fern (`ScreenColor`, D3) and holds `PracticeCubit` (settings and sittings, read again on every change), `JournalCubit` (the dreams) and `LibraryCubit` (the instance's library, lessons done, recordings kept) for every screen. go_router (`buildMeditateRouter`, `MeditatePaths`): halkyone_ui's `NavigationFrame` (`MeditateShell`) with Today, Explore, Journal and Profile (D2, D14; a bar below medium windows, a rail with the icon from medium on), a practice's page in Explore's branch (`PracticeScreen`) with a library item's and course's (`/explore/library/:id`, `?course=` for a lesson, `/explore/courses/:id`), and over the tabs the sitting (`SittingScreen`, handed its `SittingPlan` as `extra`), a recording playing (`ListeningScreen`, its `ListeningPlan` as `extra`), the library's credits (`/library-credits`), a dream (`DreamScreen`, `/dream` new, `/dream/:id` kept) and Settings. **Today** (`TodayTab`, D9): greeting by the hour, from 4 to 12 o'clock a card to write down last night's dream while it has none (`dreamPromptDue`, D14), Sit now (5, 10, 20 and the timer's own length), the practice of the day (`practiceOfDay`), the week and the days in a row (`WeekCard`), favourites, recently sat. **Explore** (`ExploreTab`, D10): search folded without case or accents (`matchesSearch`), four goal tiles, kind chips, practice cards. **The Journal** (`JournalTab`, D14; `lib/src/feature/journal/`): dreams by month, the newest first (`dreamsByMonth`), searched over title, text and labels and filtered by marks (`findDreams`), *+* to write one; a dream's night is the date of the morning after it (`defaultNight`: today's date at any hour); its screen writes with the text first and focused, the rest folded, and reads a kept one with Edit and Delete; a microphone under the text dictates where the device recognises speech (D15: halkyone_app's `Dictation` on `MeditatePerson.speech`, `DeviceSpeechInput`: on Android the voice input app first where one is installed, D18, opened once a tap and its text put where the cursor stood, else Android's recogniser, on the device where it can, listening again after each pause until stopped or three silent listens; none on Linux, `NoSpeechInput` in tests that don't say), and Today's card offers *Speak it* (`/dream?speak=1`). The platforms declare the microphone as Learn's do (Android's `RECORD_AUDIO`, the recognition service's and `RECOGNIZE_SPEECH`'s queries, `VoiceInput.kt` answering `one.halky/voice_input` from `MainActivity`, iOS's and macOS's usage texts, macOS's `audio-input`), and the privacy page's points are `meditate_base.dart`'s. **Profile** (D2, D8): halkyone_app's `HalkyonProfile` with a *Practice* tab (minutes, sittings, days, the week, the journal's dreams this month and lucid ones once it holds one, every sitting with Delete). **The practices** are `practices` in `lib/src/feature/practice/model/practice.dart` (IDs never change once released), their words in the ARBs by ID (`practiceTexts`), their cues spread evenly over the length (`Practice.cueAt`). **A sitting** (`SittingCubit`, D6): its time from `clock.now()`, the ticks only showing it; the bell at the start (where on), between every interval and at the end; the screen kept on while it runs (where on); kept whole at its end, and ended early kept from a minute on where the person says so. **The figures** (`practiceFigures`, D8) on the device: a sitting's day by the wall clock it was sat by (`Sitting.offsetMinutes`), the streak up to today or yesterday, the week from the language's first day. **Settings** (D11): Sync, the operator's card, the device's appearance and language (halkyone_app's `DeviceLookCard` on `LookCubit`, kept in `<app support>/look.json`, `meditateLanguages`; ADR 0014's amendment of 4 Nov 2026), *Sitting* (bell, starting bell, bells between, screen on, the timer's length), *Practice* (the streak shown), *Journal* (Today's morning card, `dreamPrompt`), the account, What's new, About, the version. Texts in British English (the template) and German (du), `en_US` and `en_GB` empty. Widget tests start the whole app with halkyone_app's fakes (`test/app/meditate_app_test.dart`), the bells and screen recorded (`test/support/recording_device.dart`); a sitting on screen ticks every second, so tests pump seconds (`sit`) rather than `pumpAndSettle`; a test needing the hour (Today's morning card) starts the app inside `withClock` (`startAt`).
- **The operator's library** (D16; `lib/src/library/` on the server, `lib/src/feature/library/` in the app): a folder named by `MEDITATE_LIBRARY_DIR` with recordings, pictures and `library.yaml` (commented sample: the server's `library.example.yaml`), read by `readLibrary` (every check and refusal, with the reason), held in memory by `MeditateLibrary` and reread every ten minutes (`LibraryReloadFutureCall`, `startLibrary`); checked before deploying with `dart run bin/check_library.dart <folder>` in the server package. `LibraryEndpoint.catalogue` for signed-in people alone, with an access (`LibraryFileAccess`: the hour begun plus 12 hours, signed with a key made at start); the web server's `/library/<served name>?access=…` serves the files by ranges (`serveLibraryFile`), never by a path. In the app: Explore's section (none without a library), the item's page (recordings, Play, favourite as `library/<id>` in the settings, keep on the device, a sound under a silent sitting, its credit), the course's (lessons ticked), the player (a finished item is a sitting `library/<id>` and its lesson done), Settings' *Library credits*; the last catalogue in the person's folder (`library.json`), recordings kept in `library/` beside it. Tests: `test/feature/library/library_test.dart` and the app test's group, on `test/support/fake_library.dart` (made-up entries only: nothing of 7Mind's, whose structure alone D16 follows).
- **The bells** (D5) are `assets/bells/*.wav`, synthesised by `apps/meditate/tool/bells.py` (Python's standard library, run from `apps/meditate`; the same bytes every run) and played with `audioplayers` (`AudioplayersBellPlayer`). Only while the app runs: bells with the screen off are for later.
- **The icon** is Lucide's `sprout` from cobalt through pacific into fern (ADR 0031, D5, D7), drawn by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py meditate` from the root): `assets/icon/halkyon_meditate_icon.png`, Lucide's licence beside it, every platform's launcher icon. Its launch screen shows Halkyon's kingfisher in its colours instead (ADR 0031, D11: `assets/icon/halkyon_meditate_kingfisher.png`, `meditateKingfisher`), drawn by the same tool.
- **`serverpod start`** runs in `apps/meditate/halkyone_meditate_server` on ports of its own (8280 to 8282, PostgreSQL 8290), so it can run beside Learn's (8080…) and Train's (8180…); its tests need throwaway passwords in its git-ignored `config/passwords.yaml` (`database`, `serviceSecret`, `serverSideSessionKeyHashPepper`, `emailSignInPepper`) or from the environment, as CI gives them.
- **The deploy** (ADR 0038, step 7; ADR 0039, D14, 8 Nov 2026): the server's `Dockerfile` (as Todo's, with `bin/check_library` compiled beside the server) and `config/production.yaml` (its database `halkyon_meditate` as its own role; the public host from `.env`, `MEDITATE_DOMAIN`), `deploy/compose.meditate.yaml` (a member of the register) with `sidecars/meditate.caddy` (`/library/*` to the web server, the rest to the API server), the library in `deploy/compose.meditate-library.yaml` (`MEDITATE_LIBRARY`, read-only), `redeploy.sh` and `backup.sh` taking it in; `test/deploy/meditate_deploy_test.dart`; the steps in `deploy/sidecars/README.md`, *Meditate*. Not deployed yet: halkyon.li is its first instance.
- **The deploy** (ADR 0038, step 7; ADR 0039, D14, 8 Nov 2026): the server's `Dockerfile` (as Todo's, with `bin/check_library` compiled beside the server) and `config/production.yaml` (its database `halkyon_meditate` as its own role; the public host from `.env`, `MEDITATE_DOMAIN`), `deploy/compose.meditate.yaml` (a register of its own) with `sidecars/meditate.caddy` (`/library/*` to the web server, the rest to the API server), the library in `deploy/compose.meditate-library.yaml` (`MEDITATE_LIBRARY`, read-only), `redeploy.sh` and `backup.sh` taking it in; `test/deploy/meditate_deploy_test.dart`; the steps in `deploy/sidecars/README.md`, *Meditate*. Not deployed yet: halkyon.li is its first instance.
- **Not yet**: releases (`apps/meditate/release.dart`, its key, F-Droid listing, snap), and what ADR 0038 lists under *Later*.

View file

@ -24,9 +24,17 @@ Future<void> run(List<String> args) async {
identityProviderBuilders: identityProviderBuilders(pod),
);
// The calls between the instance's services, on the web server
// (ADR 0016); Caddy never forwards them.
// The calls between the instance's services, where it is a register with
// members or a member (ADR 0016), on the web server; Caddy never forwards
// them.
addInstanceRoutes(pod);
// Its own register's pages (Halkyon Days' ADR 0045, D7, D20), as
// Halkyon's server serves its own: where its SimpleLogin app sends a
// sign-in back, the privacy page (before the profile pages' route, whose
// one segment would take it) and the profile pages, `/@handle`.
addSimpleLoginCallbackRoute(pod);
addPrivacyPageRoute(pod);
addProfilePageRoute(pod);
// The operator's library's files, to devices holding an access the
// catalogue gave them (ADR 0038, D16).

File diff suppressed because one or more lines are too long

View file

@ -1,20 +1,21 @@
# Todo's server on learn.halkyon.eu's server, at todo.halkyon.eu (ADR 0033, step 5),
# behind the instance's Caddy (deploy/compose.todo.yaml): its API and its web server
# on the one domain, as Learn's and Train's; its database in the instance's PostgreSQL.
# Myrmex's server at myrmex.li (Halkyon Days' ADR 0045, D22; todo.halkyon.eu, then
# todo.halkyon.li before it), behind the instance's Caddy (deploy/compose.myrmex.yaml):
# its API and its web server on the one domain, as Halkyon's and Train's; its database
# in the instance's PostgreSQL.
# Configuration for the main API server.
apiServer:
port: 8080
publicHost: todo.halkyon.eu
publicHost: myrmex.li
publicPort: 443
publicScheme: https
# No insightsServer, as Learn's: without the block Serverpod doesn't start Insights.
# No insightsServer, as Halkyon's: without the block Serverpod doesn't start Insights.
# Configuration for the web server.
webServer:
port: 8082
publicHost: todo.halkyon.eu
publicHost: myrmex.li
publicPort: 443
publicScheme: https
@ -25,8 +26,8 @@ webServer:
database:
host: postgres
port: 5432
name: halkyon_todo
user: halkyon_todo
name: halkyon_myrmex
user: halkyon_myrmex
# On the instance's own network, never leaving the server.
requireSsl: false
#isUnixSocket: true # defaults to false

View file

@ -9,7 +9,7 @@ Future<void> run(List<String> args) async {
// The generated Serverpod is already connected with the generated code.
final pod = Serverpod(args);
// What every Halkyon server has (ADR 0014): sign-in, the account and its
// deletion, the pruning, the instance's basics, set up for Todo; a member
// deletion, the pruning, the instance's basics, set up for Myrmex; a member
// of the instance's register where the operator names one (ADR 0016).
HalkyonBase.configure(
pod,
@ -23,9 +23,17 @@ Future<void> run(List<String> args) async {
identityProviderBuilders: identityProviderBuilders(pod),
);
// The calls between the instance's services, on the web server
// (ADR 0016); Caddy never forwards them.
// The calls between the instance's services, where it is a register with
// members or a member (ADR 0016), on the web server; Caddy never forwards
// them.
addInstanceRoutes(pod);
// Its own register's pages (Halkyon Days' ADR 0045, D7, D20), as
// Halkyon's server serves its own: where its SimpleLogin app sends a
// sign-in back, the privacy page (before the profile pages' route, whose
// one segment would take it) and the profile pages, `/@handle`.
addSimpleLoginCallbackRoute(pod);
addPrivacyPageRoute(pod);
addProfilePageRoute(pod);
await pod.start();

View file

@ -3,7 +3,7 @@
import '../../tool/src/release/app_release.dart';
/// Myrmex's releases (ADR 0033, step 5): the APK and the snap, both
/// connecting to todo.halkyon.eu, published on code.halkyon.app, the APK in
/// connecting to myrmex.li, published on code.halkyon.app, the APK in
/// the project's F-Droid repository beside Learn and Train.
const myrmexRelease = AppRelease(
id: 'myrmex',
@ -12,7 +12,7 @@ const myrmexRelease = AppRelease(
directory: 'apps/myrmex',
flutterPackage: 'apps/myrmex/halkyone_myrmex_flutter',
artifactName: 'myrmex',
serverUrl: 'https://todo.halkyon.eu/',
serverUrl: 'https://myrmex.li/',
forgejo: 'https://code.halkyon.app',
repository: 'halkyon-days/halkyon',
// A PKCS #12 keystore of its own outside the repository (also in the

View file

@ -6,7 +6,7 @@
# share. By hand, the same: in such a clone, `snapcraft pack`.
#
# It builds what is committed (the source is the clone's HEAD, the tag), never a
# working tree with its build caches, and always for todo.halkyon.eu. Flutter is
# working tree with its build caches, and always for myrmex.li. Flutter is
# pinned like the local and CI one (FLUTTER_VERSION in .forgejo/workflows/ci.yaml),
# spelled out as in Learn's recipe, which says why.
name: myrmex
@ -28,7 +28,7 @@ description: |
Everything is sealed on your device with your account's keys before it
reaches the instance, which can't read it. You sign in with SimpleLogin, by
Proton, or with an email address, a password and an authenticator app. It
connects to todo.halkyon.eu, the project's instance, or to any other:
connects to myrmex.li, the project's instance, or to any other:
everything an instance needs is in the repository, to run your own. The app
is AGPL-3.0 and the server SSPL-1.0, so all of its code is public.
@ -118,7 +118,7 @@ parts:
craftctl set version="$(sed -n 's/^version: *\([^+]*\).*/\1/p' pubspec.yaml)"
flutter pub get --enforce-lockfile
flutter build linux --release \
--dart-define=SERVER_URL=https://todo.halkyon.eu/
--dart-define=SERVER_URL=https://myrmex.li/
mkdir -p "$CRAFT_PART_INSTALL/bin"
cp -r build/linux/x64/release/bundle/. "$CRAFT_PART_INSTALL/bin/"

File diff suppressed because one or more lines are too long

View file

@ -24,9 +24,17 @@ Future<void> run(List<String> args) async {
identityProviderBuilders: identityProviderBuilders(pod),
);
// The calls between the instance's services, on the web server
// (ADR 0016); Caddy never forwards them.
// The calls between the instance's services, where it is a register with
// members or a member (ADR 0016), on the web server; Caddy never forwards
// them.
addInstanceRoutes(pod);
// Its own register's pages (Halkyon Days' ADR 0045, D7, D20), as
// Halkyon's server serves its own: where its SimpleLogin app sends a
// sign-in back, the privacy page (before the profile pages' route, whose
// one segment would take it) and the profile pages, `/@handle`.
addSimpleLoginCallbackRoute(pod);
addPrivacyPageRoute(pod);
addProfilePageRoute(pod);
// The instance's own tiles' style and their check (ADR 0030, D50), which
// Caddy asks before each tile; no start where they couldn't be had.
checkMapKeySecret(pod);

View file

@ -3,54 +3,45 @@
# set up (e.g. rendered from Proton Pass on the deploying machine and copied over SSH,
# so the server itself never holds Proton credentials).
# Halkyon Learn's domain, as TRAIN_DOMAIN and TODO_DOMAIN are Train's and Todo's (named
# DOMAIN until 2 Nov 2026): `http://localhost` for the local rehearsal;
# `learn.halkyon.eu` on its server.
LEARN_DOMAIN=http://localhost
# Learn's server's public address, a host alone, where it isn't learn.halkyon.eu
# (`learn.halkyon.li`): the address its SimpleLogin callback, profile pages and privacy
# page are on. Unset: learn.halkyon.eu.
#LEARN_PUBLIC_HOST=learn.halkyon.li
# Halkyon's domain, as TRAIN_DOMAIN and MYRMEX_DOMAIN are Train's and Myrmex's (named
# DOMAIN until 2 Nov 2026, LEARN_DOMAIN until Halkyon Days' ADR 0045): `http://localhost`
# for the local rehearsal; `halkyon.li` on the instance.
HALKYON_DOMAIN=http://localhost
# Halkyon's server's public address, a host alone, where it isn't halkyon.li: the
# address its SimpleLogin callback, profile pages and privacy page are on. Unset:
# halkyon.li.
#HALKYON_PUBLIC_HOST=halkyon.li
# Every app's server holds its own register of accounts (Halkyon Days' ADR 0045, D7,
# D9; the instance's register of ADR 0016 shared them until then): its own people, ways
# in, handles and operators, which no other server asks about. Each register's settings
# below are Halkyon's, HALKYON_…; Myrmex's, Train's and Meditate's are the same under
# their own prefix (MYRMEX_SIGN_UP, TRAIN_SIMPLELOGIN_CLIENT_ID, …), each with its own
# secrets and SimpleLogin app; the mail server (EMAIL_SMTP_…) alone is shared.
# The domain people's handles are on (`raisondetredev@halkyon.eu`) and their profile
# pages (`https://halkyon.eu/@raisondetredev`, ADR 0011), when it isn't LEARN_DOMAIN's
# pages (`https://halkyon.eu/@raisondetredev`, ADR 0011), when it isn't HALKYON_DOMAIN's
# host: point its DNS (A and AAAA) at this server, and Caddy serves the profile pages
# there, nothing else. Unset: LEARN_DOMAIN's host. Decide before anyone follows a
# handle: it can never change once the fediverse knows it.
#ACCOUNT_DOMAIN=halkyon.eu
# The instance's other services (optional, ADR 0016), such as Halkyon Todo: Halkyon
# Learn holds the instance's register of accounts, so an address, a SimpleLogin account
# and a handle are one person in every service, and deleting the account deletes it in
# all. The register learns when someone signs in to which service (the privacy page says
# so).
# INSTANCE_SECRET is the secret the services share, carried by every call between them:
# the same value in each service's environment, never given to a device. Empty: no
# service calls another. Generate: openssl rand -hex 32
#INSTANCE_SECRET=
# The web servers of the other services as this server reaches them on the instance's
# own network (never through the internet), separated by commas or spaces
# (`http://todo-server:8082`): told when an account goes, so each deletes that person's
# data too, and told again hourly until each answers. Each of them sets, in its own
# environment, the same INSTANCE_SECRET and ACCOUNT_REGISTER_URL, this server's web
# server as it reaches it (`http://server:8082`). Learn never takes ACCOUNT_REGISTER_URL:
# it doesn't start with one, as it holds the register. Train's and Todo's compose files
# name their own servers (INSTANCE_MEMBER_TRAIN, INSTANCE_MEMBER_TODO): this is for others.
#INSTANCE_MEMBERS=
# there, nothing else. Unset: HALKYON_DOMAIN's host. Decide before anyone follows a
# handle: it can never change once the fediverse knows it. (Myrmex's, Train's and
# Meditate's servers take theirs, MYRMEX_ACCOUNT_DOMAIN, …, without a site of their
# own: their handles are on their domains.)
#HALKYON_ACCOUNT_DOMAIN=halkyon.eu
# The instance's assistants, which an operator grants to people (Halkyon Learn's
# ADR 0021): in a file of their own, deploy/assistants.env, which goes to the server
# alone (assistants.env.example says how). Without it, no assistant.
# The compose files of what runs, the instance's own first (compose.learn.yaml: Caddy,
# PostgreSQL, Learn's server with the register), then the sidecars switched on beside it
# The compose files of what runs, the instance's own first (compose.halkyon.yaml: Caddy,
# PostgreSQL, Halkyon's server), then the sidecars switched on beside it
# (sidecars/README.md): compose.jellyfin.yaml, a Jellyfin people watch and listen to
# learn from; compose.languagetool.yaml, spelling and grammar checked; compose.train.yaml,
# Halkyon Train's server with its BRouter; compose.todo.yaml, Halkyon Todo's server;
# compose.todo-search.yaml, Todo on compose.search.yaml's Photon, after both.
# Halkyon Train's server with its BRouter; compose.myrmex.yaml, Myrmex's server;
# compose.myrmex-search.yaml, Myrmex on compose.search.yaml's Photon, after both;
# compose.meditate.yaml, Halkyon Meditate's server.
# Always set: no file has Compose's default name (compose.yaml until 2 Nov 2026), so
# without it `docker compose` finds none (e.g. compose.learn.yaml:compose.jellyfin.yaml).
COMPOSE_FILE=compose.learn.yaml
# without it `docker compose` finds none (e.g. compose.halkyon.yaml:compose.jellyfin.yaml).
COMPOSE_FILE=compose.halkyon.yaml
# With the Jellyfin sidecar: the domain it answers at, which the instance then suggests
# to its people (the app's Jellyfin sign-in fills it in).
@ -58,10 +49,10 @@ COMPOSE_FILE=compose.learn.yaml
# With Halkyon Train beside the instance (compose.train.yaml, sidecars/README.md,
# Train): the domain its apps reach it at, its database's password, its own session
# pepper (never change it once people sign in), INSTANCE_SECRET above set, and the
# region of BRouter's segments, `west,south,east,north` in whole degrees, multiples of
# 5 (empty: Europe's, -25,35,40,70, about 3.3 GB). Generate each secret:
# openssl rand -hex 32. Its compose file names its server to Learn's register.
# pepper (never change it once people sign in), its register's settings (TRAIN_…, as
# Halkyon's below), and the region of BRouter's segments, `west,south,east,north` in
# whole degrees, multiples of 5 (empty: Europe's, -25,35,40,70, about 3.3 GB). Generate
# each secret: openssl rand -hex 32.
#TRAIN_DOMAIN=train.example.org
#TRAIN_POSTGRES_PASSWORD=
#TRAIN_SESSION_KEY_HASH_PEPPER=
@ -69,7 +60,7 @@ COMPOSE_FILE=compose.learn.yaml
# The Loop's firmware through the instance (Train's ADR 0030, D40): `off` to stop
# relaying it; empty: on.
#LOOP_FIRMWARE_RELAY=
# Workouts' weather (Train's ADR 0036) and Todo's forecasts (ADR 0033, D40) from
# Workouts' weather (Train's ADR 0036) and Myrmex's forecasts (ADR 0033, D40) from
# Open-Meteo: `off` to stop asking; empty: on, on Open-Meteo's free API, which is for
# non-commercial use alone. An instance with ads or subscriptions needs a paid plan:
# its key.
@ -97,36 +88,37 @@ COMPOSE_FILE=compose.learn.yaml
#PHOTON_HEAP=
#PHOTON_MEMORY=
# With Halkyon Todo beside the instance (compose.todo.yaml, sidecars/README.md, Todo):
# the domain its apps reach it at, its database's password and its own session pepper
# (never change it once people sign in), INSTANCE_SECRET above set. Generate each
# secret: openssl rand -hex 32. Its compose file names its server to Learn's register.
#TODO_DOMAIN=todo.example.org
#TODO_POSTGRES_PASSWORD=
#TODO_SESSION_KEY_HASH_PEPPER=
# With Myrmex beside the instance (compose.myrmex.yaml, sidecars/README.md, Myrmex): the
# domain its apps reach it at, its database's password, its own session pepper (never
# change it once people sign in) and its register's settings (MYRMEX_…, as Halkyon's
# below). Generate each secret: openssl rand -hex 32.
#MYRMEX_DOMAIN=myrmex.example.org
#MYRMEX_POSTGRES_PASSWORD=
#MYRMEX_SESSION_KEY_HASH_PEPPER=
# With Halkyon Meditate beside the instance (compose.meditate.yaml, sidecars/README.md,
# Meditate): as Todo's. Its library (compose.meditate-library.yaml, optional): the
# Meditate): as Myrmex's. Its library (compose.meditate-library.yaml, optional): the
# folder on the host with library.yaml, mounted read-only.
#MEDITATE_DOMAIN=meditate.example.org
#MEDITATE_POSTGRES_PASSWORD=
#MEDITATE_SESSION_KEY_HASH_PEPPER=
#MEDITATE_LIBRARY=/data/meditate-library
# The password of the instance's PostgreSQL superuser, halkyon_learn, which Learn's
# server signs in as, and backups and deploys dump as (Train's and Todo's servers have
# roles of their own), as TRAIN_POSTGRES_PASSWORD and TODO_POSTGRES_PASSWORD are their
# roles' (named POSTGRES_PASSWORD until 2 Nov 2026). Set once, when the database is
# The password of the instance's PostgreSQL superuser, halkyon_halkyon (halkyon_learn
# until Halkyon Days' ADR 0045), which Halkyon's server signs in as, and backups and
# deploys dump as (Train's, Myrmex's and Meditate's servers have roles of their own), as
# TRAIN_POSTGRES_PASSWORD and MYRMEX_POSTGRES_PASSWORD are their roles' (named
# POSTGRES_PASSWORD until 2 Nov 2026). Set once, when the database is
# made: PostgreSQL keeps the password it was made with, whatever this says later.
# Generate: openssl rand -base64 32
LEARN_POSTGRES_PASSWORD=
HALKYON_POSTGRES_PASSWORD=
# Learn's pepper for hashing session keys (server-side sessions), as Train's and Todo's
# are TRAIN_ and TODO_SESSION_KEY_HASH_PEPPER (named SESSION_KEY_HASH_PEPPER until
# 2 Nov 2026). NEVER change its value once sessions exist: everyone would be signed out
# of Learn, and anonymous accounts can't sign back in. Rotation only via
# Halkyon's pepper for hashing session keys (server-side sessions), as Train's and
# Myrmex's are TRAIN_ and MYRMEX_SESSION_KEY_HASH_PEPPER (named SESSION_KEY_HASH_PEPPER
# until 2 Nov 2026). NEVER change its value once sessions exist: everyone would be
# signed out of Halkyon. Rotation only via
# fallbackSessionKeyHashPeppers. Generate: openssl rand -base64 32
LEARN_SESSION_KEY_HASH_PEPPER=
HALKYON_SESSION_KEY_HASH_PEPPER=
# Email sign-in (optional, ADR 0010): people sign in with a code mailed to their address,
# of which the instance keeps only a keyed hash. Off unless the pepper and all of the SMTP
@ -134,61 +126,63 @@ LEARN_SESSION_KEY_HASH_PEPPER=
# The pepper: NEVER change it once anybody signs in with an address, as every address's
# hash would be lost, and with it those people's way in.
# Generate: openssl rand -hex 32
EMAIL_SIGN_IN_PEPPER=
HALKYON_EMAIL_SIGN_IN_PEPPER=
# The SMTP service codes go through, over TLS only (STARTTLS, or TLS directly on 465).
# Proton (a paid plan with an address on your own domain): smtp.protonmail.ch, 587, the
# address as the user, an SMTP token made for it in Proton Mail's settings (IMAP/SMTP) as
# the password, and the same address as EMAIL_FROM. Some hosts block outgoing 25 and 465;
# 587 gets through.
# 587 gets through. Every server's, the one setting the registers share.
EMAIL_SMTP_HOST=
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=
EMAIL_SMTP_PASSWORD=
# The address codes come from (shown as "Halkyon Learn"); its domain needs SPF, DKIM and
# DMARC records for the service, or mail lands in spam.
EMAIL_FROM=
# The address Halkyon's codes come from (shown as "Halkyon"); its domain needs SPF, DKIM
# and DMARC records for the service, or mail lands in spam.
HALKYON_EMAIL_FROM=
# Password sign-in (ADR 0017): an address, a password that never leaves the device, and
# a code from an authenticator app. Off unless all three keys and email sign-in are set
# up (resets go by mail); each key at least 32 characters, or the server doesn't start.
# They stay outside the database, so a copy of it alone gives none of this away:
# TOTP_KEY seals the authenticators' secrets: changing it makes every authenticator
# HALKYON_TOTP_KEY seals the authenticators' secrets: changing it makes every authenticator
# useless (people then reset theirs, 72 hours, by mail).
# RECOVERY_CODE_KEY keys the recovery codes' hashes: changing it voids every recovery
# HALKYON_RECOVERY_CODE_KEY keys the recovery codes' hashes: changing it voids every recovery
# code (people make new ones once signed in).
# SIGN_IN_DECOY_KEY makes the answers for addresses nobody has look like an account's:
# HALKYON_SIGN_IN_DECOY_KEY makes the answers for addresses nobody has look like an account's:
# changing it lets someone who watched those answers tell which addresses are unused.
# SimpleLogin needs it alone (below), for handles nobody has.
# Generate each: openssl rand -hex 32
TOTP_KEY=
RECOVERY_CODE_KEY=
SIGN_IN_DECOY_KEY=
HALKYON_TOTP_KEY=
HALKYON_RECOVERY_CODE_KEY=
HALKYON_SIGN_IN_DECOY_KEY=
# SimpleLogin (ADR 0020): people sign in with their SimpleLogin or Proton account, of
# which the instance keeps only SimpleLogin's ID for them (no address, no name). Off
# unless both are set: the client ID and secret of this instance's own SimpleLogin app
# (SimpleLogin → Settings → Developer → New website), registered with the callback
# /simplelogin-callback on the server's web address (on learn.halkyon.eu's server
# https://learn.halkyon.eu/simplelogin-callback), its forwarding page back to the apps.
# Takes SIGN_IN_DECOY_KEY (above), or the server doesn't start: a broken link is mended
# with the handle and the recovery phrase, a handle nobody has answered as an account's.
SIMPLELOGIN_CLIENT_ID=
SIMPLELOGIN_CLIENT_SECRET=
# unless both are set: the client ID and secret of the register's own SimpleLogin app
# (SimpleLogin → Settings → Developer → New website; one per register, as SimpleLogin
# gives a person another ID in each app), registered with the callback
# /simplelogin-callback on the server's web address (on halkyon.li
# https://halkyon.li/simplelogin-callback), its forwarding page back to the apps.
# Takes HALKYON_SIGN_IN_DECOY_KEY (above), or the server doesn't start: a broken link is
# mended with the handle and the recovery phrase, a handle nobody has answered as an
# account's.
HALKYON_SIMPLELOGIN_CLIENT_ID=
HALKYON_SIMPLELOGIN_CLIENT_SECRET=
# Another callback than this server's own page, as the app registered it (https only).
#SIMPLELOGIN_CALLBACK_URL=https://learn.example.org/simplelogin-callback
#HALKYON_SIMPLELOGIN_CALLBACK_URL=https://halkyon.example.org/simplelogin-callback
# The ways in the app offers, of those set up above: simplelogin, password, or both
# (separated by spaces or commas). Empty: every way set up. An unknown name stops the
# server as it starts.
#SIGN_IN_WAYS=simplelogin
# Who may make an account (Learn's ADR 0021): open (empty), approval (new accounts wait
#HALKYON_SIGN_IN_WAYS=simplelogin
# Who may make an account (Halkyon Learn's ADR 0021): open (empty), approval (new accounts wait
# until an operator approves them in the app, at most 10 at once, deleted unapproved
# after 14 days) or closed (nobody new). The first operator is made by SQL once signed
# up: docs/adr/0021-private-instance-and-assistant.md, D32. Anything else stops the
# server as it starts.
#SIGN_UP=approval
#HALKYON_SIGN_UP=approval
# Whether people may make agents in their Profile (ADR 0044, D45): accounts for programs,
# such as AI assistants, signing in with a token, Viewers of what they're invited to.
# on (empty) or off; anything else stops the server as it starts.
#AGENTS=off
#HALKYON_AGENTS=off
# Where the source of what this instance runs is, shown in the app's About dialog: both
# licences (AGPL for the app, SSPL for the server) require offering it to the people who
@ -228,7 +222,7 @@ BACKUP_LOCATION=
# this server keeps (and a fourth while a deploy writes its own): files take five times
# their size on the disk. One person may get more than the default; as the operator,
# in deploy/ (their id is their account's, "authUserId"):
# docker compose exec postgres psql -U halkyon_learn -d halkyon_learn -c "INSERT INTO
# docker compose exec postgres psql -U halkyon_halkyon -d halkyon_halkyon -c "INSERT INTO
# file_allowance (\"authUserId\", \"maxBytes\", note) VALUES ('<id>', 2147483648, 'asked by mail')"
FILE_LIMIT_IDENTIFIED_MB=
FILE_LIMIT_INSTANCE_MB=

View file

@ -4,9 +4,9 @@
# services (`/halkyon-base/…`, ADR 0016): they reach each other on its own network. Insights (8081) doesn't run in production;
# if it is ever re-enabled, reach it through an SSH tunnel, never through this file.
#
# {$LEARN_DOMAIN} is `learn.halkyon.eu` on its server, where Caddy obtains and renews a
# {$HALKYON_DOMAIN} is `halkyon.li` on its server, where Caddy obtains and renews a
# Let's Encrypt certificate by itself, and `http://localhost` for the local rehearsal.
# {$ACCOUNT_SITE} is the account domain handles are on where it isn't {$LEARN_DOMAIN}
# {$ACCOUNT_SITE} is the account domain handles are on where it isn't {$HALKYON_DOMAIN}
# (`halkyon.eu`), whose profile pages this instance serves; empty for none.
# Global options an instance's setting adds (compose.forwarded.yaml: behind another
# server's HAProxy, ADR 0039); none without.
@ -14,7 +14,7 @@
import global/*.caddy
}
{$LEARN_DOMAIN} {$ACCOUNT_SITE} {
{$HALKYON_DOMAIN} {$ACCOUNT_SITE} {
encode zstd gzip
# Profile pages, `/@handle`; asked for on the other host, they send the visitor to
@ -52,15 +52,15 @@
reverse_proxy server:8082
}
# Android's statement that Halkyon Learn, signed with the project's key
# (tool/release/android-certificate.sha256), may open learn.halkyon.eu's links
# Android's statement that Halkyon, signed with the project's key
# (tool/release/android-certificate.sha256), may open halkyon.li's links
# itself (App Links): built for ADR 0020's D9, whose correction brings the
# sign-in back through the app's scheme instead, and used since by the links to
# join a shared studyset (Halkyon's ADR 0044, D39). On learn.halkyon.eu alone, the one instance the
# join a shared studyset (Halkyon's ADR 0044, D39). On halkyon.li alone, the one instance the
# project's builds name: another operator's domain is no link of the project's
# app. Written here rather than by the server, which knows no app's key.
@assetlinks {
host learn.halkyon.eu
host halkyon.li
path /.well-known/assetlinks.json
}
handle @assetlinks {
@ -77,7 +77,7 @@
# Serverpod's streaming methods use WebSockets; reverse_proxy upgrades them as-is.
# Caddy puts the visitor's address last in X-Forwarded-For (what a visitor sends
# there it replaces), which the server's per-network limits count by, as
# TRUSTED_PROXIES names Caddy (compose.learn.yaml); the server never reads the Forwarded
# TRUSTED_PROXIES names Caddy (compose.halkyon.yaml); the server never reads the Forwarded
# header, which Caddy passes on as sent.
handle {
reverse_proxy server:8080

View file

@ -1,6 +1,6 @@
# The instance's assistants (Halkyon Learn's ADR 0021, points 4 and 8, D37, D38, D63):
# copy to deploy/assistants.env, restrict it (chmod 600), and fill in. It is git-ignored,
# goes to Halkyon Learn's server alone (compose.learn.yaml, `env_file`), never into an image,
# goes to Halkyon Learn's server alone (compose.halkyon.yaml, `env_file`), never into an image,
# the database or its backups; keep it with deploy/.env's secrets (in Proton Pass).
# Without the file, the instance offers no assistant, as before. A change takes a
# restart: docker compose up -d server. Anything the server can't use stops it as it

Some files were not shown because too many files have changed in this diff Show more