What is written in a shared space after a member went takes a key they
never get (Halkyon's ADR 0044, D22). The instance marks the space as one
of its members goes, removed, leaving or deleted (`keyRollDue` on
`shared_space`, set in `removeMember`, through which each of them
passes); an invitation taken back never had a key and marks nothing, nor
does a new role. The Owner's or an Editor's device that next keeps its
keys up after a sync is told so (`sharedSpaceKeys.rollDue`, never true
for a Viewer, who seals no key) and makes a new key, sealed to those who
stay (D33). The first seal bringing a key ID the space never had clears
the mark in the same transaction (D34); members the rolling device
missed get it later as newcomers do. Earlier rows keep the key they had,
which whoever went had read already.
The migration, the same in the module and in each server including it
(Learn, Meditate, Todo, Train):
ALTER TABLE "shared_space"
ADD COLUMN "keyRollDue" boolean NOT NULL DEFAULT false;
It adds the column with false for every space there is, so no space is
due a roll from it; PostgreSQL adds a column with a constant default
without rewriting the table. The server before the switch reads and
writes the table naming its own columns, so it keeps working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0044, D29 to D32: Share opens one sheet, people first, then the
community; a Viewer's studyset screen is read-only; a shared studyset
is marked with people; safety codes are compared from a member's row,
members matched to their keys by handle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0044, D27 and D28.
- D27: sharing a studyset with a publication anywhere in its tree is
refused for now. Publishing reads a release from the person's
readable rows on the instance, and a shared studyset is sealed in
another space, so it could take no release; a later step has the
device hand over a release's content and re-points the publication.
- D28 (the agent's, to be confirmed): the copy is the archive's export
and import, so progress, tags, stars and the archive follow, each
item keeping its link to Explore and each card its origin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0044's D20 to D26, the developer's choices on 11 Nov 2026, each the
agent's recommendation: a shared space's rows trusted when any member
pinned signed them (D20); a shared space always sealed (D21); rolling
its key seals new writes alone (D22); a shared studyset at the top level
for every member (D23); writes reaching a shared space through the sync
engine, our fork routing them (D24); tags staying each member's own
(D25); and personal rows naming shared content by a plain ID, as the
engine links no row of one space to another's (D26). Step 3 is marked
built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Step 3 of ADR 0044 (D3, D18, D19). A shared space's data keys are
sealed on a device to each member's public keys; the instance keeps
what devices sealed and never opens it.
- halkyon_keys: the safety code of two accounts' public keys
(`safetyCode`), 60 digits, the same on both sides.
- halkyon_base: the register hands out accounts' public keys
(`public-keys-of`, local and remote); the table `shared_space_key`
and the endpoint `sharedSpaceKeys` (memberKeys, keysNeeded, seal,
mine). Only the Owner and Editors seal, the first key sealed to a
member stays, and a member's keys go with their membership. Every
host's migration creates the table.
- halkyon_app: `ContentCipher` seals a key to another account and
opens one sealed by another; `SharedKeys` shares a space's key with
its members, seals keys to newcomers and opens the person's own,
holding each member to the public keys `KeyPins` pinned on first
sight (`key_pins.json` in the person's folder), a change refused
until trusted anew.
Learn takes them into its `Privacy` in step 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0044, step 2 (D1, D2, D11, D13, and D15 to D17, decided today).
halkyon_base keeps the spaces several people share, for every app:
shared_space, its members with their roles (Owner, Editor, Viewer), and
invitations by handle waiting for someone with no account on the
service yet. The sharedSpace endpoint creates a space, lists its
members, invites by handle, changes roles, removes members, hands the
space on, leaves and deletes it.
Who may do what:
- Only people with an identity reach the endpoint.
- A space the caller isn't in can't be told from one that doesn't
exist.
- Members are named by their membership, never by their account's ID.
- Only the Owner manages the members, unless the space lets members
invite.
- The Owner's role passes only by handing the space on, and the Owner
leaves only naming who takes it on.
- An invitation counts as an answer about a handle, as it tells whether
one exists.
Every rule has a test asserting the refusal.
The module stays free of the sync engine: a host hands it a SpaceGrants,
and every call runs in the module's transaction, so the engine's grants
follow the roles or nothing changes. The Owner and an Editor write; a
Viewer reads. Learn hands over SyncSpaceGrants on OfflineSyncSpaces. Todo
hands over none yet, so its server refuses until its projects are
shared.
Deleting an account hands each space the person owns to the Editor who
joined first, else the Viewer who joined first. A space with no other
member goes. This runs before the host's own deletions. A person's first
sign-in on a service makes their waiting invitations memberships, in the
same transaction that makes them a person there.
Migrations: the module's and every host's (Learn, Todo, Train, Meditate)
create the three tables, their indexes and the two cascading foreign
keys to shared_space, and nothing else. The old server keeps working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
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>
The developer's decisions of 10 Nov 2026 on their evening's requests:
- ADR 0042: every app holds its sync back on a metered connection, as the
OS says, and asks in a quiet banner (Sync now, Without files, Not now);
an answer holds until the connection changes; Settings › Sync chooses
Ask, Without files or Everything.
- ADR 0043: Learn's file store moves into halkyon_base, room counted per
service; Todo gets sealed attachments in descriptions and comments, any
file up to 100 MB, fetched when shown.
- ADR 0044: shared spaces in halkyon_base for every app, Learn's studysets
first: Owner, Editor and Viewer, keys wrapped to members' key pairs,
invites by handle or email, links off at first, the material to publish
chosen item by item, and agents as accounts of their own whose changes
are suggestions a person accepts. Its open questions come before
building, first how rows move between spaces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>