Latch

Project

Packs — federated Latch

A proposal for connecting Latch instances into packs — one Handler training several pups, pack-mates who can see each other's clocks, pack training and pack wheels — without giving up the rule that every pup's data stays on their own box.

On this page

This is a design proposal, not a shipped feature

Nothing on this page exists yet. It is written down so the shape is agreed before the code is, and so the privacy rules survive contact with the fun parts. Discuss it on the repository's issue for M5.

The idea

Latch is solo by design: one instance, one pup. But the game gets a whole new dimension when instances can see and act on each other with consent:

None of that requires a central server, an account with anyone, or a pup's journal leaving their machine.

The rules that do not move

  1. Each pup's instance stays authoritative for its own clock. A Handler proposes; the pup's instance applies — as an event, in the pup's own log, with actor=pack:<handler-id>. The clock stays derived from the local log. Nothing remote can write a row.
  2. Every capability is a grant, chosen by the pup, revocable in one tap, default nothing. Pairing with a Handler grants nothing until the pup ticks boxes.
  3. Prose never federates by default. The line the API already draws — facts are shareable, prose is yours — is the federation line too. Reflection bodies and check-in scores travel only under an explicit share reflections with my Handler grant, negotiated as part of the kink, and revocable.
  4. The local emergency key always works. No pack state, no Handler, no lost connection can stand between a pup and unlocking their own game. A pack pause ("safeword") suspends every remote grant instantly and locally, without asking anyone.
  5. Nothing leaves the LAN unencrypted or unsigned, and instances never need to be on the public internet to pack up.

Roles and grants

Role Can, if granted
Pack-mate see my status (locked / frozen / free) · see my clock · see my streak and rank · see my stamped photos · send kudos
Handler everything a pack-mate can, plus: assign training · add / remove time (with a per-day cap the pup sets) · freeze / unfreeze · write my daily prompt and check-in question · set my Handler cadence · see my reflections (separate, explicit grant) · never unlock me early and never disable my emergency key

Grants are per peer. A pup can be in one pack with a Handler and another with only pack-mates. A Handler instance can hold many pups; v1 keeps one Handler per pack.

Caps matter: add time comes with a ceiling the pup set (say, four hours a day), enforced locally. A Handler who hits the cap gets a refusal, the same sentence the pup would see.

How instances find each other

No directory, no discovery, nothing public. Pairing is an invite:

  1. The Handler instance mints an invite — a short code or a QR (the collar tag): its identity key, its address, a one-time secret.
  2. The pup pastes or scans it, sees exactly who is asking, and ticks the grants.
  3. Both sides now hold each other's public key and a per-pair capability token. Revoking on either side tears it down; a revoked grant fails closed.

Each instance's identity is an Ed25519 keypair generated on first run and kept in the data volume — the same pattern as a mesh node's identity, and equally never regenerate it.

That key answers which box is this. It deliberately cannot answer which pup is this, because a pup outlives their server: rebuild the machine and the instance key is new, while the pup is not. Identity proposes the second, portable key that packs should actually know you by, so that moving machines re-pairs the box without costing you your standing.

Transport

The envelope is the contract; the pipe is pluggable. Every message is a signed JSON envelope (from, to, kind, payload, ts, id, signature), idempotent by id, replay-proof by timestamp window. Three pipes, in the order they would be built:

Pipe Fits Notes
Direct HTTPS two instances on one LAN or one private overlay (WireGuard, Tailscale, NetBird) POST /pack/inbox on the receiver; the Handler polls GET /pack/feed on each pup. Simplest, first.
Matrix room packs across the internet without exposing anything each instance is a bot in a private room; the room's membership is the pack, its invite is the pairing, E2EE is free. The Matrix bot is already on the roadmap.
LXMF / Reticulum off-grid, mesh, radio the same envelope over LXMF; slow, but a pack of two on a mountain is a fine thing.

An instance behind NAT with no overlay uses Matrix. Nobody has to open a port to the internet to join a pack.

What the Handler instance holds

Only what was granted, only as a cache: status, clock, streak, photos the pup's instance chose to send, and — under the reflection grant — reflection text. On revocation the pup's instance sends a purge and the Handler instance drops the cache; a Handler that ignored a purge would be a broken client, not a data breach on the pup's side, because the pup never sent more than the grant allowed.

Pack-mates hold even less: a status card each.

Pack play, concretely

What it is not

Phasing (if it goes ahead)

Phase What
F0 The pack module skeleton: identity keypair, invites, grants UI, the envelope, /pack/inbox. Nothing federates yet; the consent surface exists first.
F1 Read-only pack board over direct HTTPS. Status, clock, streak, kudos.
F2 Handler commands: training, time (capped), freeze, prompts. Every one an event with actor=pack:<id>. The pack pause.
F3 The Matrix pipe, reusing the Matrix bot. Packs across the internet.
F4 Pack play: pack training, pack wheel, roll call, pack streak.
F5 The LXMF pipe.

Open questions

Argue on the issue. The rules at the top are the ones that are not up for argument.


This page also ships inside the app, at /guide — so your own instance always serves the guide for the version you are running, with the internet unplugged. Get Latch · Something wrong here? Tell me.