The app's side of stage 4 (ADR 0005, rule 11). The card's Report sheet offers "Send to the author too" on a card from a release; a copy's menu reports the studyset as a whole and lists what is reported and suggested on it, as Explore's summary does; the editors of cards, notes and steps suggest them to the author as the learner has them, never under "no derivatives". The Profile tab holds the inbox: the threads on the person's published studysets and those they wrote, the newest activity first, with a count on the tab. A thread shows a suggestion beside the author's own item and its replies; its writer and the author reply, and the author accepts (the content becomes their own edit, its files fetched from the writer's), declines, closes, hides or blocks. Writing needs an identity; reading what is public doesn't. ADR 0005 gets an addendum for stage 4. 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>
9.8 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.
Addendum: stage 3, Explore and learning from a release (2026-10-09)
- Explore finds by fit, never by popularity. It lists what is published by search, languages and kinds of content, the newest release first, a page at a time, the next only when asked for. Votes (ActivityPub likes) cost nothing to fake, so they count for nothing. Learning activity is harder to fake and is shown, never ranked: "10+ learn this here", counting only people of this instance (a number another instance sends could be anything), with an identity, who chose to be counted (opt-in), and who studied a copy on 3 or more days of the last 30; shown rounded, from 5 on, so that nobody can be told apart. Ranking by it would keep the first good studyset on top for good.
- The app makes and updates a copy; the server never writes into a learner's space. A copy is written through the learner's own repositories, as an import; each item names its origin (publication, item, release). A later release is offered on the copy with what it brings and taken at a tap: per side for cards, notes and steps (as rule 4 says), per field for studysets, paths and PDFs; withdrawn items stay, marked and studied on; what the learner deleted stays deleted.
- A copy may be published within its licence: never under "no derivatives", under a ShareAlike licence only under that licence; it says what it is based on. An author can take a studyset off Explore again: nobody can add it then, and copies get no updates; its releases stay.
Addendum: stage 4, reports and suggestions (2026-10-09)
- Reports and suggestions are public, gathered by the instance from each writer's own
space. A report or suggestion on a published studyset (or one of its cards, notes or
steps), its replies and its author's decisions are rows of their writers' own synced
spaces, so writing works offline; the instance gathers them when they are read, and
decides then what counts: threads from people with an identity and a public profile the
author hasn't blocked, replies from the writer and the author alone, decisions from the
author alone, no suggestion under "no derivatives". Everyone signed in reads them, each
with its writer's public profile, but for those the author hides (then only writer and
author see them). The author replies, closes, hides, accepts or declines a suggestion,
and blocks people from writing on their studysets. A suggestion is a card, note or step
as its writer has it; accepted, it becomes the author's own edit, its files fetched from
the writer's, and the next release credits the writer (
Co-authored-by).
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.