Latch

Project

Identity โ€” the pup, not the box

A proposal for a portable pup identity โ€” a key that is you rather than your server, badges that can travel as signed papers, and the collar as a real object โ€” so that moving instances, joining a pack and leaving one are all things a pup does without losing what they have done.

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. It is the substrate Packs stands on, and it should be argued about first. Discuss it on the repository's issue for M7.

The idea

Today there is no row in Latch that is you. There is an instance, a log, and a game. The pup is implied by the box.

That works exactly as long as there is one box and one game. It stops working the moment you

So: a pup is a keypair with a name. The instance is a place a pup plays. Packs know the pup. Papers travel. The box is furniture.

The rules that do not move

  1. The pup key is the pup, and nobody else ever holds it. Generated on first run, carried in the identity export, never escrowed, never regenerated. Losing your instance must not lose your pup. Losing your pup key is losing your pup โ€” and the app says so at the moment the key is made, not at the moment it is gone.
  2. Badges stay derived; papers are a different object. A badge is a predicate over your log. A paper is a signed claim that the predicate was true. Importing papers never mints badges. A pup arriving on a fresh instance has papers and no badges, and that is the honest reading of the situation.
  3. A paper is only as strong as who signed it, and it says who. Self-signed papers are your own word, rendered as your own word. The app never implies more confidence than the signature carries.
  4. Identity never depends on a service. No account anywhere is required to be a pup. Handles attach to the key; the key is never derived from a handle. Losing a linked account costs you a display name and nothing else.
  5. The journal is not the identity. Moving pups takes the key, the profile and the papers. Prose travels only inside the full backup, which is a restore artefact and never a handover. This is the line Packs already draws, applied to leaving rather than to joining.

Two keypairs, not one

Packs already specifies an Ed25519 identity โ€” but it is the instance's, kept in the data volume and never regenerated. That key answers "which box is this". It cannot answer "which pup is this", because the whole point is that the pup outlives the box.

Instance key Pup key
Answers which box is this which pup is this
Born first run of a container first run, or imported
Lives the data volume, never leaves the identity export, travels
Signs transport envelopes papers, pairing, collars
If lost rebuild the box, re-pair it the pup is gone

Packs know a pup by the pup key, so moving machines re-pairs the box without dropping your grants or your standing. A pup is not a new pup because their NVMe died.

Badges and papers

This is the part that needs care, because the existing badge rule and the portability wish pull against each other.

A badge is derived: a predicate over the event log, with earned_at the first instant it became true. That is what makes badges retroactive, undo-safe and honest on a stamped photo. It is also what makes them unable to travel: carry the pup to an empty instance and every predicate is false.

So there are two objects, and conflating them is where this design would go wrong.

Badge Paper
Is a view over your log a signed claim about a log
Lives nowhere โ€” recomputed in the profile, exported
Travels only with the whole log yes, that is the point
Strength derivation testimony
Shown on the badge page the trophy case

A paper carries the badge id, not an image. An instance that knows that badge renders its art; one that does not shows the claim in plain words. Art is never part of the identity.

Do not build verification theatre

A self-hosted single-player game cannot stop its owner from editing their own database, and a signature you applied to your own claim does not change that. Pretending otherwise adds cryptography that buys nothing. A signature starts meaning something only when a second party makes it โ€” which is exactly what a collar is for.

Stray and collared

The weakness above is not a compromise. It is the culture, and it falls out of the signatures rather than being painted on.

Stray. No active Handler grant. Every paper self-signed, and presented as your own word. This is not a lesser state โ€” most pups are strays most of the time, and a stray's papers are perfectly good among people who take your word.

Your own Handler cannot witness for you

The local Handler โ€” rule-based or AI โ€” runs on your box, under your key. A countersignature from it is you signing twice. It must never be rendered as a witness, however much it feels like a second voice in the room.

Collared. An active Handler grant from another pup's instance. Papers issued during that window carry the Handler's countersignature, and are checkable by anyone who knows that Handler's key. The collar is the grant record; taking it off is revoking.

A pup's history is then a sequence of collars and strays with dates โ€” which is a life, not a leaderboard. A collar that ended is not a collar that never happened: the record keeps its ended_at and the papers issued under it stay countersigned forever. Nothing about leaving a pack should read as though you were never in it.

Naming collision, settled

stray used to be RANKS[0] too โ€” the level-one XP rank in core/game.py, alongside pup in training, good boy and four others. Using it here for "no active Handler grant" as well would make one word mean two unrelated things: a level you climb out of, and a relationship you are in or not. A pup can be a best boy and uncollared at the same time, and the header would have to render both. free is taken too โ€” it is a timer status.

This is the same trap settled once already for badge versus token, and it was cheaper to settle before it hit screens.

Settled 2026-09-20 by renaming the rank: RANKS[0] is now runt, freeing stray to carry the social state โ€” the culturally load-bearing meaning, and the one a pack actually reads. The rank strings are pure flavour (no database key, no API field, nothing joins on them), so the rename was a one-line edit with no migration.

Either way, one of the two moves before this ships.

Adoption

Packs already calls the pairing invite the collar tag. Adoption is that exchange with the ceremony left in:

  1. A Handler offers a collar tag.
  2. The pup sees exactly who is asking, and what is being asked for.
  3. The fun bits, all optional, all reversible: a name you are given, a colour, a pack tag on your profile, a first entry in the collar record.
  4. The grants are ticked separately and deliberately, because adoption grants nothing by itself. The ceremony is the ceremony; consent is a checklist. Keeping them apart is what stops the ritual doing the arguing.

Leaving is the same shape in reverse and should be as easy: the collar record gets an end date, grants fail closed immediately, papers stay, and the name and colour revert only if you ask them to. Some pups keep the name.

Handles

A pup key is unreadable. A handle is how people say your name.

The key is the identity; a Matrix ID, a fediverse handle or a Bluesky DID is a claim attached to it โ€” proven once by publishing a signed nonce from that account, stored with the proof, revocable in one tap, and entirely optional. The natural wire format for the key is a did:key: an Ed25519 public key written as a DID, with no registry and no server behind it.

Anchoring the identity to an account instead would be a mistake in three directions at once: losing the account would lose the pup, it would put a third party in the path of a game designed to work on a LAN or a mesh with no internet at all, and it would quietly turn a private thing into a public profile. Handles point at the pup. The pup never points at a handle.

Two exports, two jobs

Export and backup is one file today and does one job well. Identity needs a second, and keeping them separate is what keeps the privacy line legible.

bundle.zip โ€” backup pup.json โ€” the pup
Holds every table, every reflection, media key, profile, handles, collars, papers
For restoring a machine, moving with your whole history joining a pack, arriving somewhere new
Prose yes โ€” this is why it is never shared never
Badges after import re-derive in full, because the log came too shown as papers; local badges start empty

Restoring a backup on a new machine is the high-fidelity path and should stay the recommended one for moving. pup.json is for arriving โ€” the papers you hand over, not the diary you carry.

What the profile holds

Small, and mostly not secret:

Table Holds
profile the pup keypair, display name, colour, pronouns, avatar, created_at
handles kind, value, proof location, verified_at, revoked_at
collars handler pup key and name, pack name, granted_at, ended_at
papers badge id, scope, claim, earned_at, issued_at, issuer key, countersigner key, signature

The private half of the pup key is the only secret in the list, and it is the one thing the export has to protect.

Meta badges need no new tracking

Worth stating plainly, because it is the thing most likely to get over-built: the log never resets. sessions is a table of locks; events is one continuous append-only stream across every game you have ever played, and the lifetime figures on the progress page already read it without a session filter.

So a badge earned "across games" is not a different kind of object. It needs one thing the badge predicate does not have yet โ€” a scope:

Scope Reads Example
session one lock ten training tasks verified in a single lock
window a rolling period ten in one week
lifetime the whole log three hundred hours locked, ever

That is a column, not a subsystem. "Meta" is a tier in the trophy case, not a second engine.

Phasing (if it goes ahead)

Phase What Needs
I0 The profile exists locally: pup keypair, name, colour, the loud warning at key generation. Nothing federates; the pup exists before anything can ask who they are. โ€”
I1 Papers: issue signed claims for derived badges, a trophy case that shows them as claims, pup.json export. badges
I2 Import: a pup arriving on a fresh instance. Papers shown, local badges derive from whatever log is present, the two never confused. I1
I3 Handles: link and prove a Matrix, fediverse or Bluesky handle; revoke in one tap. I0
I4 Collars: the grant record as a first-class object with a start and an end, and countersigned papers. I2, Packs F2
I5 Adoption: the naming, the colour, the pack tag, the ceremony. I4

I0 through I3 are useful on a single instance with no pack anywhere, which is deliberate โ€” an identity that only pays off once you have friends is an identity nobody tries.

Open questions

Argue on the issue. The rules at the top are the ones that are not up for argument. The consent rules in The spirit of the game and in Packs outrank everything here.


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.