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:
- A pack. Several pups, each on their own instance, who can see each other's clocks, streaks and stamped photos on a shared pack board. Kudos between pups. A leaderboard, if the pack wants one.
- A Handler with a kennel. One person running a handler instance who trains several pups at once: assigns training to the pack, spins one wheel that lands on all of them, adds an hour to everyone who missed a reflection, writes the day's prompt for the whole kennel, and reads what each pup chose to share.
- Pack play. Roll call (a check-in window where every pup checks in or the pack pays), pack training (one task, everyone verifies), a pack wheel, a pack streak, shared milestones.
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
- 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. - 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.
- 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.
- 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.
- 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:
- The Handler instance mints an invite — a short code or a QR (the collar tag): its identity key, its address, a one-time secret.
- The pup pastes or scans it, sees exactly who is asking, and ticks the grants.
- 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
- Pack board. Every pup's ring side by side: status, remaining (or hidden), streak, the
latest stamped photo. Kudos = a
kudosevent on the receiver, +XP, a 🐾 on the board. - Pack training. The Handler assigns one task to N pups; each verifies on their own instance; the board shows who is done. A pack reward when everyone verifies.
- Pack wheel. The Handler spins once; every pup's instance receives the same proposal and applies it within its caps. Or roulette: one pup at random pays.
- Roll call. A check-in window. Pups who check in are good boys; the pack's streak survives only if everyone does.
- Handler-less packs. Pups with no Handler, mutual accountability only: the board, kudos, the pack streak. Probably the most common pack.
- A pup's own Handler still runs. The local rule-based (or AI) Handler keeps its rhythm; a remote Handler layers on top. Two Handlers disagreeing is a conversation the pup is in.
What it is not
- Not social. No public profiles, no timelines, no follow graph. A pack is a private room of people who chose each other.
- Not a keyholding service. The pup's instance is the keyholder; a remote Handler is a voice with borrowed, capped powers.
- Not hosted. Nobody runs the pack for you. Instances talk to instances.
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
- Should a Handler be able to see a hidden timer's remaining time? (Proposal: only under a separate grant. The pup hid it from themselves, not from their Handler.)
- Does a pack want a shared prompt library the Handler writes, or does each pup's library stay local with the Handler only able to add? (Proposal: add only.)
- How loud should a pup's instance be about remote actions — a notification per event, or a daily digest? (Proposal: per event, with a mute.)
- Who can see the leaderboard, and can a pup opt out of it while staying in the pack? (Proposal: opt-in per pup; a missing row reads as not playing, never as last.)
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.