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
- move to a new machine, and every badge evaporates because they are derived from a log that did not come with you;
- join a pack, where "who is this" has to survive your server being rebuilt;
- want an achievement to mean anything outside your own SQLite file.
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
- 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.
- 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.
- 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.
- 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.
- 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:
- A Handler offers a collar tag.
- The pup sees exactly who is asking, and what is being asked for.
- 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.
- 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
- What happens to a paper when the event under it is undone later? Undo voids an event and every derived number recomputes, so the badge would vanish โ but the paper was true when it was signed. (Proposal: the paper stands, dated, and the local badge disappears. This is the same promise a stamped photo already makes โ a record of what was true at the shutter, not a claim about now. A paper that retracted itself would be a worse lie than one that is simply old.)
- Can a pup hold more than one identity? (Proposal: one key, one pup. A second persona is a second pup with its own export, and that is a feature โ the app should not be the thing that links them.)
- Is losing the pup key recoverable? (Proposal: no, loudly, at generation โ with an optional printable recovery sheet. No escrow, no "sign in to recover", not ever. The one place this app must not be helpful.)
- Do papers expire? (Proposal: never. They are dated; a reader can weigh an old one. Re-issuing is always allowed and keeps both.)
- Whose papers can a trophy case show? (Proposal: your own always; a pack-mate's only inside that pack and only what they chose to show. No browsing strangers.)
- Does a pup name themselves, or does a Handler? (Proposal: both, and the record keeps who chose. A name you were given is a different object from a name you picked, and the difference is most of the point.)
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.