halkyon/docs/adr/0044-shared-spaces-and-collaboration.md
Tizian 「ティツィアーン」 f745516f36
docs(adr): shared spaces, decided further, in six steps
ADR 0044 records the answers to its open questions: a shared studyset
passes on when its owner leaves or deletes their account (D11); an
agent's token lapses after 90 days unused (D12); an invitation by handle
waits for someone who never used the app (D13); and sharing copies the
studyset under new IDs into the shared space (D14), since the sync
engine's delete is soft and a row's ID can't be inserted into another
space. Its Steps section orders the work, starting with the community's
confirmation (D8, D9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 10:06:20 +01:00

6.9 KiB

ADR 0044: Shared spaces and collaboration, people and agents alike

  • Status: decided 10 Nov 2026 by the developer: D8 is their own request, D9 and D10 their choices over the agent's recommendations (all ticked at first; agents editing directly), D1 to D7 the agent's recommendations, which they chose; D11 to D13 the agent's recommendations, which they chose on 11 Nov 2026, D14 too. Not built yet; the steps below.
  • Builds on: ADR 0005 (publishing, reports and suggestions), ADR 0014 (the module), ADR 0016 (one account across an instance), ADR 0017 (key pairs), ADR 0019 (private studysets), ADR 0021 (operators and assistants).

Context

A studyset belongs to one person: their rows are in their personal space, a private one sealed with a data key that only their own devices unwrap. The developer asked (10 Nov 2026) for Vaia's sharing (Manage collaborators: invite by email, an invite link, the members and their roles; Collaboration settings: joining by link, collaborators inviting others; Share with the community: choosing what material goes public) and for collaboration with everyone, people and agents alike: invite @Paul or @Claude to a studyset, Claude Code on halkyon.li's host working as an agent account, as Copilot or Claude work on a GitHub repository.

The sync engine already has shared spaces (OfflineSyncSpaces: create, grant, revoke, members), with two roles, readOnly and readWrite, which limit writes alone: a member reads everything in the space.

Decisions

  • D1. Shared spaces live in halkyon_base, for every app: members, roles, invitations, links, settings and the wrapped keys. Learn's studysets use them first, Todo's projects later on the same tables.

  • D2. Roles: Owner, Editor, Viewer. The Owner can do everything, delete the studyset and hand it to another member; an Editor changes content; a Viewer reads and studies, with their own reviews in their own space. Editor and Owner are the engine's readWrite, a Viewer its readOnly; who is Owner is the module's own table, checked by its endpoints.

  • D3. A private studyset's key is wrapped to each member's public key (ADR 0017's key pairs) by a member's device that holds it, at its next sync after someone joined. The instance never sees the key; reading waits for that sync. A member removed gets no new content: the studyset rolls to a new key, as ADR 0019's rewrites.

  • D4. Invites by @handle or by email. An @handle adds a person on the instance at once (they see it in their Inbox and may leave). An email address gets a mail with a link to join, which works after signing up; the address is never shown to the other members.

  • D5. Joining by link is off at first; someone joining by it is a Viewer, whom the Owner can raise. Collaborators may invite others is off at first too.

  • D6. Agents are accounts of their own, marked as agents, each owned by a person who made it from their Profile; listed in the owner's Profile, deleted with them. An agent has its own key pair, kept where it runs, signs in with a token the owner made (never a password), and is invited like a person: @claude.

  • D7. An agent reaches the instance through a small CLI or MCP server run beside it (Claude Code on halkyon.li's host runs as its own user, without Docker): it signs in with its token, syncs the spaces it is a member of, opens the sealed rows with its own keys, and writes through the same endpoints an app uses.

  • D8. Share with the community (publishing to Explore, ADR 0005) gets a step confirming the material: every PDF, other file and note listed by kind.

  • D9. That step's choice: all unticked at first, with per kind tick all, and on any item everything but this and nothing but this. Cards and the learning path always go.

  • D10. An agent suggests; a person accepts. An agent is a Viewer in the engine's sense and writes nothing into the studyset; its changes go as suggestions, which a member with Editor's rights accepts or declines, each marked as by @claude. Learn's reports and suggestions (ADR 0005, stage 4: feedback_thread, the author's decisions, accepted suggestions applied by their item) are the start: they become suggestions within a shared space too, not only on published releases.

  • D11. A shared studyset passes on. Leaving, the Owner picks the next Owner first. Deleting their account, ownership passes by itself to the Editor who joined first, else to the Viewer who joined first; only with no other member does the studyset go with them.

  • D12. An agent's token lapses after 90 days unused; while used it has no fixed end, the owner revokes it any time, and their Profile shows when it was last used.

  • D13. An invitation by @handle waits for someone who has an account on the instance's register but has never used the app: they find it in their Inbox the first time they sign in to it, and the inviter sees them as invited until then.

  • D14. Sharing copies the studyset under new IDs into the shared space, rewrites what names its rows (reviews, tags, the learning path, files) and deletes the old ones, as ADR 0019's rewrites do; the fork stays as it is. Found 11 Nov 2026: the engine's delete is soft (insteadOfDelete: the domain row stays, hidden by a tombstone in its space's crdt_data_rows), and a synced table's primary key is the row's ID alone, so the same ID can't be inserted into another space; teaching the engine to move a row was the other way, set aside as a deeper change to its merge, kept on every rebase.

Steps

  1. Share with the community's confirmation (D8, D9), needing nothing else here.
  2. Shared spaces in halkyon_base (D1, D2, D11, D13): the module's tables (a shared space, its members and roles, its Owner), its endpoints with their denial tests, the engine's grants kept in step, the hand-on as an account is deleted.
  3. Keys (D3): a private studyset's key wrapped to each member's key pair.
  4. Learn shares a studyset (D14): the copy, Manage collaborators (invite by @handle, the members, their roles, leaving), each member's own reviews.
  5. Invitations by email and by link (D4, D5), the Inbox.
  6. Agents (D6, D7, D10, D12).

Open questions, before building

  • Moving rows between spaces. Decided: D14.
  • Whose reviews. A Viewer's or Editor's schedule for a shared card stays in their own space, keyed by the card's ID; the card's own review rows today assume one learner.
  • Handing on and leaving. Decided: D11. To build: the account's deletion hands the shared spaces it owns on before it deletes the person's own.
  • Members across services. Decided: D13. A member of a Learn studyset needs no Todo account, and the reverse.
  • The agent's token: made and revoked in the owner's Profile, scoped to the agent's account, never shown again after making; its lapse decided: D12.