Releases in a git repository per published studyset (the `git` command), learning as a copy that follows (later releases offered, merged per side), handles on the account domain Mastodon's way (`name@halkyon.eu`, changeable until the first publish; a profile of display name, bio, links and avatar), reports and suggestions of our own on plain git rather than a forge's, carried by the sync engine, and ActivityPub through Fedify in a container of its own: federated from the start, built in six usable stages. Decided 2026-10-09; the alternatives and why they lost are in the ADR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
7.1 KiB
ADR 0005: The public tier: releases in git, a copy that follows, federation
- Status: accepted
- Date: 2026-10-09
Context
The Japanese studyset is meant to be the project's public example: other people, on this instance and on others, should be able to find it, learn from it, and help improve it. Until now content has two tiers (ADR 0001, ADR 0002): private, a person's own synced space, and shared, spaces granted to people of the same instance. Explore has stayed empty.
A direction for a third, public tier was recorded on 2026-09-27 (in the working notes, not decided): releases kept in git, published as one-way snapshots, announced over ActivityPub the way PkgFed proposes for package releases, with an index on halky.one, learners' instances mirroring what they learn from, and reports and suggestions as issues and pull requests behind a gateway. This ADR decides it, and the order it is built in.
Decision
- The public tier is federated from the start, and built in usable stages. Each
stage works on its own; none is built in a way the later ones would undo:
- public identity (handles, profiles);
- releases in git;
- Explore, and learning from a release (rule 4);
- reports and suggestions (rule 6);
- releases across instances over HTTPS;
- ActivityPub, through Fedify (rule 7).
- A person appears under a handle on the account domain, Mastodon's way:
raisondetredev@halkyon.eu. The account domain is the instance operator's setting, apart from the server's host (learn.halkyon.eu), as Mastodon'sLOCAL_DOMAINandWEB_DOMAIN; the account domain then answers WebFinger by pointing to the host. It can never change once anyone follows a handle on it. A handle can change until its person first publishes, and is fixed from then on, as followers and links refer to it; moving later goes the fediverse's way (Moveto another account). A public profile holds a display name, a short bio, links (shown as verified when the linked page links back withrel="me") and an avatar. Only people signed in with an identity provider (theidentifiedscope) can have one: anything involving other people needs it. - A release is a commit and a tag in a git repository of its own per published
studyset, a bare repository on a volume of the server, worked on through the
gitcommand, never a library linked into the server. The server builds a release from the publisher's synced rows (which it can read, ADR 0002): no device sends a snapshot. Its layout is a directory per studyset of the tree and a file per card, note, path, step and PDF, with their files, in a stable serialisation, so git's trees give every subtree a hash of its own (a learner of one part can tell whether it changed) and diffs mean something. Releases are numbered and immutable; a release can be withdrawn, never rewritten. Only studysets under a licence that allows passing them on can be published, as mirroring and copying are passing them on; the licence and its credits travel with every release. - Learning takes a copy that follows. The learner gets the release as their own studyset, editable and studied offline like any other; each card keeps a reference to the published card (its studyset's identity and its own, both stable across releases) and the release it came from. A later release is offered, never applied by itself, and merged per side as the dictionary's corrections are (ADR 0003): what only the release changed comes in, what the learner changed stays, a side both changed is a conflict the learner settles. New cards are offered, withdrawn ones marked; progress is never deleted by a release.
- Names are locations: a published studyset is
halkyon.eu/@raisondetredev/japanese; its identity is its UUID, the location only finds it (a move is announced). - Reports and suggestions are ours, on plain git, not a forge's. A report (an issue)
is a thread on a published studyset or one of its cards; a suggestion (a pull request)
is an edit made in the app's own editors. Both are written as rows in the writer's own
synced space, so they work offline and travel with the sync engine; the server routes
them to the author's inbox. Accepting a suggestion commits it to the repository with
its writer as
Co-authored-by, and the next release carries it. Across instances they travel later as ForgeFed's tickets and offered patches, through Fedify. - ActivityPub is spoken by Fedify, in a container of its own beside the server (TypeScript, MIT), behind Caddy's federation paths: WebFinger, actors, HTTP signatures (both kinds), delivery queues, its state in PostgreSQL. The Serverpod server records what to announce; Fedify announces it. A release is announced with its hash and where to fetch it, and travels over HTTPS (stage 5), never inside an activity; no core function depends on federation. halky.one may later index releases, never host them.
Consequences
- An instance's operator chooses an account domain once, and keeps WebFinger answering on it for as long as the instance lives.
- Instances hold git repositories on a volume, which the backups must cover; the server's
image needs the
gitcommand. - A learner's progress, notes and edits stay theirs whatever the author does: releases only ever offer.
- Reports, suggestions, the author's inbox and the review of a suggestion as cards are ours to build, in the app.
- A second runtime (Fedify on Node, Deno or Bun) joins the deployment at stage 6, with its own container.
Alternatives considered
- Publishing and learning on one instance first, federation designed later: reaches learners sooner, but risks a format and identities other instances can't share. Rejected for building in stages within the federated design instead.
- A plain copy (the old app's "Add to my library"): simplest, but an author's fix never reaches anyone who already learns. A read-only mirror with a personal layer of reviews and overlays: updates arrive by themselves, but cards can't be edited, only overlaid, and much more has to be built.
- The GitHub username as the public name: would mean storing it, which the instance avoids on purpose (only the numeric GitHub id is kept). No author shown: only the licence holder would name anyone.
- Releases as files by their hash, git later: less to run, but history, diffs and per-subtree hashes would have to be rebuilt by hand.
- libgit2 linked into the server (git2dart): no processes, but a native library in the server and its build.
- A Forgejo on each instance for issues and pull requests: mature issues, reviews and a web interface, but a second account system per instance (a Forgejo user per person, usernames and emails stored, deletion to keep in step), an experimental federation that would give a person a second identity in the fediverse, every action a live call (an outbox of its own for offline use), and the review of a suggestion as cards still ours to build in the app.
- Our own ActivityPub in Dart: one runtime, but every interoperability detail (signatures above all) ours to get right and keep right.