merkhain

Sharing: Personal sync and teams

In My Memory the personal pool is called Personal sync, adding a device is Connect another computer (with a pairing code), and a member enrollment is an invitation. This guide uses the CLI and protocol names (pool, device, member), which stay as they are in commands and logs.

Memories stay on this computer unless explicitly shared, through a project binding, publication or a save to a pool. A pool is an end-to-end encrypted log on one relay (Relay); its members' devices hold the pool keys. Two kinds:

  • personal — your own devices (device invite / device join);
  • team — owned by one user who invites other users (member invite / member join).

Shared records keep their author, device, available provenance and timestamps. They give others awareness of what was decided, tried or left open; they are not locks or assignments, and they are as fallible as any generated memory. Sync is asynchronous, so a teammate's newest work may arrive later.

Pools never bridge: a record pulled from one pool is never re-published to another. Every device needs an identity (merkhain identity init) and a running serve local to sync continuously (merkhain sync runs one pass by hand).

Sync my computers with Tailscale (no separate relay)

Sharing → Sync my computers with Tailscale sets up Personal sync entirely in the browser when your computers share a Tailscale network (tailnet). One computer hosts the relay for your own computers; nothing is reachable from outside your tailnet (Tailscale Funnel is never used), and you never handle an operator token.

Before you start, on every computer: install Tailscale, sign in to the same tailnet, and (once per tailnet) turn on MagicDNS and HTTPS certificates in the Tailscale admin console (DNS page). merkhain never installs Tailscale or signs in for you; it checks, explains what is missing, links to the right Tailscale page and offers Check again.

On the computer that is on most of the time, choose Set up sync on this computer:

  1. If this computer is not set up as you yet, Set up this computer as me creates your identity with its keys in the system credential store (macOS Keychain, Windows Credential Manager, Secret Service on Linux desktops). If you already use merkhain on another computer, host there instead or connect this one to it.
  2. Choose the projects to sync — none is preselected, and projects you create later are not synced until you add them on the Personal sync page. The list shows one page of projects at a time with a search box, the number selected and the chosen projects (removable); the selection is kept while you search or page. This step opens once Tailscale is installed and this computer is set up as you. Include existing history is off unless you turn it on; then what those projects already remember is published too, once.
  3. Start syncing runs the setup: a relay on 127.0.0.1:7782 (its own SQLite database personal-sync-relay.db in the data directory, migrated automatically like the instance database, see Databases, and an internal operator token in the config directory), a private Tailscale Serve mapping https://<this computer>.<tailnet>.ts.net:8443 → http://127.0.0.1:7782 (tailscale serve --bg --https=8443 http://127.0.0.1:7782), a reachability check over the tailnet, your Personal sync pool, the chosen projects and, if chosen, their history. Every step is recorded in personal-sync-host.json, so retries, a restart of serve local or a crash continue where they stopped without a second pool or a second publication.
  4. When it is done the page shows the sync address and Create pairing code, each with a copy button, the computers in Personal sync and the changes waiting to sync.

On each other computer, choose Connect to my other computer, enter the sync address and the pairing code (and a name for a computer not set up yet), Review, then Connect. A computer that is not set up yet gets an identity of your user, whose keys go to its credential store; it only becomes the computer's identity when the pairing completes, so a refused, expired or abandoned pairing leaves the computer as it was. Sending the same code again resumes an interrupted pairing with the same keys. The hosting computer approves automatically while merkhain runs there; both sides activate sync immediately — no restart, no new browser sign-in.

The host must be awake with serve local running for your computers to sync. While it is off, changes wait on each computer and sync when it is back. Stop hosting stops the relay and removes only the Serve mapping merkhain added; memories, computers, chosen projects and the relay database stay, and Resume hosting continues with them.

What setup refuses or stops for, with what to do:

  • Tailscale missing, stopped or signed out; HTTPS certificates off; the device or Serve awaiting a tailnet admin's approval (the link Tailscale prints is shown); on Linux, a user that may not change Serve settings (sudo tailscale set --operator=$USER).
  • Port 127.0.0.1:7782 in use by another program, or Tailscale Serve port 8443 already used for something else (merkhain never changes another service's mapping), or Funnel enabled on it.
  • Personal sync already set up through another relay, or a pairing code of another person on a computer that is already set up: identities are never merged, replaced or moved.
  • No usable system credential store (headless servers): use the terminal path below with --key-store file.

Only one computer hosts; there is no failover, no moving the host and no direct computer-to-computer sync. The terminal and an external relay (below) keep working as before and use the same pairing codes.

Personal pool and pairing a second device

With a relay you run yourself (an advanced alternative to the Tailscale setup above), on the first device (the relay operator gives you the operator token):

merkhain pool create --relay https://relay.example.com --operator-token-file relay-operator-token
merkhain pool list                                  # note the pool_id
merkhain pool bind <POOL_ID> --dir ~/src/app        # future memories of this project are published
merkhain publish --pool <POOL_ID> --dir ~/src/app --all   # optional: also share existing history
merkhain device invite --pool <POOL_ID>             # prints a single-use code, valid 24 h; stays online (default 15 min)

On the new device:

merkhain device join --relay https://relay.example.com    # paste the code at the no-echo prompt
merkhain recall search --dir ~/src/app "<terms>"          # its clone of the same remote maps to the same project
  • device join creates this device's identity for your user if it has none (--key-store file for headless hosts). The code is read from a prompt, an owner-only --code-file, or - (stdin) — never from argv.
  • Transfer the code over a channel you trust; whoever redeems it first becomes your device. The inviting device must be online to approve; with --wait 0 it returns at once and serve local (or merkhain device finalize) completes the handoff when it is back.
  • merkhain device list shows devices and enrollments; merkhain device cancel <ENROLLMENT_ID> withdraws a code. Once the new device was admitted to the pool, cancelling is refused: remove it with merkhain device remove (or merkhain member remove), which also rotates the keys.
  • Projects created on the first device after pairing are not mapped automatically: merkhain project alias <PROJECT_ID> <REMOTE_URL> on the other device, or search with --pool.

Connect another computer in the browser (My Memory)

Sharing → Connect another computer is the browser version of device invite. It is never an invitation: a pairing code only connects computers of the person who made it, and it never adds another person (that is Invite a teammate on a team page).

  1. On a computer that is already set up, choose Pair a computer from this one, then Personal sync or one team (a team or Personal sync page links here as Connect another of my computers / Connect another computer). The page says what the code gives before it exists: the other computer acts as you in that space only, with its own keys. It can read what that space has shared so far and later, and nothing else: not your other teams or Personal sync, and not memory on this computer that is not shared there. No universal sign-in.
  2. Create pairing code shows the code in a field (Copy code; if copying fails, select it in the field), its expiry, and the two ways to use it. There is no link. Done (forget the code) removes it from the page (it still works until it expires); Cancel pairing code stops it at the relay. The code is never stored, put in a URL or logged by the page, and it is removed at expiry.
  3. On a new computer (no identity yet, the usual case) run the command shown, merkhain device join --relay <relay>. It asks for the code at a no-echo prompt, creates the identity for your user, and connects only that space. Then restart merkhain serve local there, reopen My Memory and sign in again. With Personal sync hosted over Tailscale, use Connect to my other computer instead (above), which creates the identity in the browser. Do not run merkhain identity init there: it makes the computer a different person, which cannot be paired.
  4. A computer already set up as you (for example, already in Personal sync) can instead use I have a pairing code: relay and code, Review, then Connect this computer as you, which sends the code with kind: "device". A teammate invitation pasted here is refused before anything is sent, and so is a mistyped code.

The computer that made the code approves automatically while serve local runs there; nobody needs to click anything. If the code belongs to another person's computers, the page says so: nothing was connected and no access was given; that person needs to send you a teammate invitation instead. An expired, cancelled or already used code is refused with what to do next. If the relay does not confirm the request, the code stays in the page and Connect again sends the same request, which resumes it.

Team pool

Owner:

merkhain pool create --team --name acme-app --relay https://relay.example.com --operator-token-file relay-operator-token
merkhain pool bind <TEAM_POOL_ID> --dir ~/src/app
merkhain publish --pool <TEAM_POOL_ID> --dir ~/src/app --all     # optional: share history with the team
merkhain member invite --pool <TEAM_POOL_ID>                     # single-use 24 h code for one new user

New member (their own identity first):

merkhain identity init --name bob-laptop
merkhain member join --relay https://relay.example.com            # paste the code

The member receives every epoch key of that pool (full shared history) and the mapping of the projects published to it, so their clones of those repositories resolve to the team's projects and recall the shared history. Joining binds nothing: the member's own future lessons stay private until they bind a project themselves (merkhain pool bind <TEAM_POOL_ID> --dir <REPO>, or the team's Projects tab). Each project in the join output has "shared". Nothing of the owner's other pools or private memory is transferred. A member pairs their own extra devices into the team pool with device invite --pool <TEAM_POOL_ID>. merkhain member list --pool <TEAM_POOL_ID> shows users, roles and devices.

Removing a member or a lost device (owner device)

merkhain member remove <USER_ID> --pool <TEAM_POOL_ID>    # all of that user's devices
merkhain device remove <DEVICE_ID> [--pool <POOL_ID>]     # one device, e.g. a lost laptop, from every pool listing it

Removal revokes the devices at the relay immediately, cancels their pending enrollments, and rotates to a new key epoch wrapped only to the remaining devices; queued unsent records are re-encrypted under the new epoch. Exit code 75 means the relay was unreachable and the rotation is pending — run merkhain sync once it is back (uploads to that pool pause until then). Limits: anything the removed device already downloaded stays readable to it; owner transfer is not supported in v1; a device cannot remove itself.

The relay cannot prove that it is not hiding a newer membership, so a device uploads only while another owner device has signed the pool's membership within the last 72 hours (a device that is the pool's only owner device needs no one). Owner devices renew that signature about every 18 hours while they sync, with a checkpoint the relay keeps one of per owner device, so renewing does not grow the membership history (a relay that predates checkpoints gets one membership revision per owner device per renewal instead, until it is upgraded, and a proxy that refuses only the checkpoint route one about every 36 hours, until it is fixed). If no other owner device has synced for longer, uploads to the pool wait (membership_stale), pulls continue, queued records stay queued, and uploads resume after its next sync. This applies to every member of a team pool whose owner is offline for that long or runs a version that does not sign yet, so upgrade owner devices first; an owner device that is gone for good is removed from another owner device with merkhain device remove. A pool whose only owner device is lost cannot recover this way, because owner transfer is not supported.

What this does and does not bound. A relay that hides a removal can expose new records to the removed device for at most 72 hours after the last signature by an owner device that saw the removal or was not kept on the old membership. A single relay cannot be stopped from showing different devices different histories: if the removed device is itself an owner device whose key is still in use (a stolen laptop), or the relay also keeps another owner device on the old membership, that device keeps signing fresh evidence and the 72-hour limit does not apply, until a device sees the newer membership. Treat a stolen owner device together with an untrusted relay as a reason to move to a new pool.

Offline work

Capture, extraction and recall never depend on the relay. Offline devices queue their publications and converge without duplicates on reconnect; merkhain sync status shows per-pool health (ok, offline, waiting_for_key, not_member, …).

Reviewing sharing in My Memory

Sharing separates My computers from My teams. Each goal shows which computer acts and what its shared pool can access. Creation, invitations and pairing still require their own confirmation; a link alone does not authorize an action. The inviting computer completes enrollment automatically while merkhain runs there. “Connected” describes enrollment, not proof that every memory has arrived.

A team page has Overview, Projects, People, Invitations, Insights and Details sections. Use the arrow keys, Home and End on the section tabs; the selected section survives reload and browser Back. Devices are grouped under their people, identified by available computer names. Project choices stay selected while searching, paging or visiting another section. Search text and selections remain in page memory, outside the URL. Only confirmed future-sharing changes clear their selections; an uncertain request retains its project so the same idempotent action can be retried.

Binding a project controls future publication. It cannot establish whether history was published earlier. The UI therefore says earlier publication is unknown, unless this page holds a successful history-publication response. That response means history was queued, not delivered, and is forgotten on reload. Stopping future sharing does not withdraw memories already shared.

Personal host setup includes a scope review before Start syncing. Computer names are read-only after identity setup. Check again on the connect page performs a fresh readiness check; a failed check preserves the entered details. A stopped host still offers Resume hosting with its existing configuration. Managed Personal hosting does not expose the standalone Relay Administration console.

Handoff recovery explains how to reopen the original link or request a new one. Sensitive fragments are removed from the address bar and held in page memory only. Keep that page open during sign-in; a reload or another tab cannot recover its cleared secret. Cancellation that the relay has not confirmed is shown as uncertain: the code may still work until cancellation is confirmed.