2026-09-30 15:13:34 +02:00
|
|
|
# Halkyon Learn
|
|
|
|
|
|
|
|
|
|
A spaced-repetition learning app: studysets of flashcards with rich text, studied offline
|
|
|
|
|
on every device and synced between a person's devices. Anyone can run an instance.
|
|
|
|
|
|
|
|
|
|
- **Offline first.** Each device keeps its own database and syncs in the background
|
|
|
|
|
(`serverpod_offline_sync`). Edits made on two devices while offline are never lost:
|
|
|
|
|
card content is stored as append-only revisions, and a side changed differently on two
|
|
|
|
|
devices is shown as a conflict until the person picks a version
|
|
|
|
|
([ADR 0001](docs/adr/0001-offline-sync.md), [ADR 0003](docs/adr/0003-card-revisions.md)).
|
|
|
|
|
- **No sign-up needed.** The app starts with an anonymous account; signing in with GitHub
|
|
|
|
|
later keeps everything. A GitHub sign-in stores only the numeric GitHub ID.
|
|
|
|
|
- **No end-to-end encryption.** Content is readable by the operator of the instance, as on
|
|
|
|
|
Mastodon; whoever trusts no operator can run their own
|
|
|
|
|
([ADR 0002](docs/adr/0002-no-end-to-end-encryption.md)).
|
|
|
|
|
|
|
|
|
|
Targets: Linux, Windows, macOS, Android and iOS. There is no web build.
|
|
|
|
|
|
|
|
|
|
## Packages
|
|
|
|
|
|
|
|
|
|
| Package | What it is | Licence |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `halkyon_learn_server` | The [Serverpod](https://serverpod.dev) server: endpoints, models, migrations | SSPL-1.0 |
|
|
|
|
|
| `halkyon_learn_client` | The client, generated from the server's models | AGPL-3.0-or-later |
|
|
|
|
|
| `halkyon_learn_flutter` | The app (`app.halkyon.learn`) | AGPL-3.0-or-later |
|
|
|
|
|
|
|
|
|
|
## Running it locally
|
|
|
|
|
|
|
|
|
|
You need Flutter (Dart v3.13 or later) and the Serverpod CLI v4.0.3
|
|
|
|
|
(`dart install serverpod_cli@4.0.3`). Linux builds also need `libwebkit2gtk-4.1-dev` and
|
2026-09-30 21:49:04 +02:00
|
|
|
`libsecret-1-dev`. The rich-text editor comes from our fork of AppFlowy Editor
|
|
|
|
|
([project-halkyon/appflowy-editor](https://code.halkyon.app/project-halkyon/appflowy-editor)),
|
|
|
|
|
which `flutter pub get` fetches by itself.
|
2026-09-30 15:13:34 +02:00
|
|
|
|
|
|
|
|
The server's secrets live in `halkyon_learn_server/config/passwords.yaml`, which is never
|
|
|
|
|
committed. In a fresh clone, create it with a `development` and a `test` section, each
|
|
|
|
|
holding `database`, `serverSideSessionKeyHashPepper` and `serviceSecret` (for example from
|
|
|
|
|
`openssl rand -base64 32`). GitHub sign-in is optional: add `githubClientId` and
|
|
|
|
|
`githubClientSecret` of your own GitHub OAuth app to `development`, registered with the
|
|
|
|
|
callback URLs `app.halkyon.learn://github-callback` and `http://127.0.0.1/github-callback`.
|
|
|
|
|
|
|
|
|
|
Then start the server, its embedded database and the app:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
cd halkyon_learn_server
|
|
|
|
|
serverpod --no-analytics start
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`serverpod start` applies pending migrations and reloads on changes. The app talks to the
|
|
|
|
|
server named by `--dart-define=SERVER_URL=…`, or else to the one in
|
|
|
|
|
`halkyon_learn_flutter/assets/config.json` (`http://localhost:8080`). Without
|
|
|
|
|
`--no-analytics`, the Serverpod CLI reports usage to Serverpod.
|
|
|
|
|
|
|
|
|
|
## Tests
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
tool/check.sh
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
runs the format check, the analyzer with the strict lints of `leancode_lint`, the server's
|
|
|
|
|
tests (against an embedded PostgreSQL, no Docker needed) and the app's tests. To run it
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
before every push: `ln -s ../../tool/check.sh .git/hooks/pre-push`. CI runs it on every
|
|
|
|
|
push too (`.forgejo/workflows/ci.yaml`).
|
2026-09-30 15:13:34 +02:00
|
|
|
|
|
|
|
|
The sync tests need a running server (`serverpod start`); run them in
|
|
|
|
|
`halkyon_learn_flutter` with `flutter test -t requires-server`.
|
|
|
|
|
|
|
|
|
|
## Running an instance
|
|
|
|
|
|
|
|
|
|
`deploy/` holds a Docker Compose setup: Caddy (HTTPS), the server and PostgreSQL. Copy
|
|
|
|
|
`deploy/.env.example` to `deploy/.env` and fill it in; it explains every value. The
|
|
|
|
|
licences require offering the source of what the instance runs to the people who use it.
|
|
|
|
|
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
learn.halkyon.eu itself deploys from `main` once CI is green: `deploy/redeploy.sh`, run by
|
|
|
|
|
the CI runner described in `deploy/runner/`, deploys only commits signed by a key in
|
2026-10-03 16:45:40 +02:00
|
|
|
`deploy/allowed-signers`. It backs up its database every hour, off-site and encrypted;
|
|
|
|
|
[`deploy/backups.md`](deploy/backups.md) says how to set that up and how to restore.
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
|
2026-09-30 15:13:34 +02:00
|
|
|
## More
|
|
|
|
|
|
|
|
|
|
- [`AGENTS.md`](AGENTS.md): how the code is organised and the rules it follows.
|
|
|
|
|
- [`docs/adr/`](docs/adr/): the architecture decisions and their reasons.
|
|
|
|
|
- [`CONTRIBUTING.md`](CONTRIBUTING.md): licences of contributions and the sign-off.
|
|
|
|
|
- [`LICENSE.md`](LICENSE.md): which licence covers what.
|