🐾 Handlr ?

Guide β€Ί Modules

Kennel Card

Your pup homepage β€” a Tag, your links, your toy box and your wishlist β€” built as a small static site you host anywhere and share by link or QR code. Each view shows only what you tick for it. This box never serves it.

πŸͺͺ Kennel Card makes you a homepage: who you are, where else to find you (Ko-fi, Throne, your socials), what you play with, and what you'd love to be given. You share it as a link or a QR code. It lives on Kennel Card in the menu.

Handlr builds the page. It never serves it: the optional kennel host or any static host you upload it to does, so sharing your page never opens your box to the internet.

Warning

Anyone with a link can see that view. Each link is long and random (128 bits), so nobody finds it by guessing, but links get forwarded and pages get screenshotted. A link is not a password. If you want a real login in front of a view, put it on the host (basic auth, your reverse proxy's login), not in Handlr. Think hard before you show your face: a face plus a distinctive toy, a tattoo or your room can be enough to find you.

Setting it up

  1. Your Tag: the name on the page, your pronouns, a few lines about you, a colour, and an avatar if you want one (picked from photos already on this box).
  2. Your Leash: the links people can follow β€” Ko-fi, Throne, a gift registry, the fediverse, Bluesky, Matrix, email, a website. https:// only (or an email address). Tracking tags such as utm_…, fbclid and gclid are stripped when you save; a referral code like ref= is kept.
  3. Views: make one for each group you share with β€” a Public one for your bio, a fuller Friends one. Each view has its own link, its own blocks, and its own ticks.
  4. Tick what each view shows, on the view's page: its blocks (Tag, Avatar, Leash, Toy box, Wishlist, Stats, Trophy case), which links, which toys and wishes, which of each one's photos, which stats and which badges β€” and whether to sign it.
  5. Preview it β€” Preview it as a stranger sees it opens exactly the page you'll publish.
  6. Publish β€” see what changed, then publish into the kennel host or download the site.

What a page can show

Nothing is ticked for you. A new view shows your Tag's name and nothing else.

You tick The page shows
a toy or piece of gear its name, its kind and its category
one of its photos that photo, re-encoded: no location, no camera data, no comment
show its specs (on the item's Kennel Card page) its sizes, material and other numbers and choices β€” never a text field like a brand, a colour or an engraving
show its price the price on the kit item
a shop link a button to the shop. Tick it's my referral link and the page says so beside it
⭐ a favourite a star, and it sorts first
a wish its name, kind, how much you want it (want most Β· would love Β· someday), and whatever of the above you added

What strangers read about an item is the blurb you write for them on its Kennel Card page (on your Kennel Card β†’ from the kit item). Your kit notes never go on a page, and neither does anything you logged about using it.

Retired toys get their own shelf, Retired β€” served well. When a wish becomes owned, it moves to the Toy box on the next publish.

Note

The wishlist is shareable here, and only here. Share cards and packs still never show a wish. The page can't take orders or reserve a gift β€” it's a static page. To stop two people buying you the same thing, add a gift registry such as Throne to your Leash. Never put a home address on the page.

The page itself

  • Browsable: filter chips (toys, gear, wishlist, retired), tap an item for its photos, specs and shop link, tap a photo to page through them, and switch sizes between inches and millimetres. Without JavaScript the page still works: each item opens in place.
  • An adult-content door before anything shows, remembered on that device.
  • Loads nothing from anywhere else. No font, no script, no image from another site: its own Content-Security-Policy says so, and every outbound link is a plain link the reader chooses to follow, with no referrer.
  • Hidden from search engines: a noindex on every page and a robots.txt that disallows everything.
  • One clock: the page says As of the day you built it. It's a snapshot, not live.
  • Can't be lined up: the same toy on two views has different ids and differently named photos, so two links can't be matched to each other, or to a pack card β€” unless you sign them, which puts your Pup ID on each.

Publishing

Nothing goes out until you tap Publish. The Publish page first lists, view by view, what changes for the people holding each link since you last published: + Rex (toy box), Rex: + 1 photo, Rex: price now shown, πŸ”— a new link, πŸ›‘ taken down. One tick changed is one line. Saving a view, a blurb or your Tag changes this box only.

Then either:

  • Publish into the kennel folder, if you run the kennel host: live at once.
  • Download the site, and unzip it at your static host's root, replacing what was there. The download counts as publishing: the next list of changes starts from it.

The site is every view, plus a β€œtaken down” page at every old link:

robots.txt
index.html             an empty page, so the root never lists the folders
k/<link>/index.html    one view
k/<link>/manifest.json the same view as data
k/<link>/qr.svg        its QR code (once you set where it will live)
k/<link>/a/…jpg        its photos
k/<link>/manifest.sig  its signature, if you signed it
k/<old link>/index.html  β€œThis card was taken down.”

Turn directory listing off on your host (the kennel host already does).

Set where it will live (https://kennel.example.org) and each view shows its full link and a QR code, on its page here and in the download.

Taking it down

  • πŸ›‘ Take it down (on a view's page) replaces that view with a page that says This card was taken down, at the same link. Your ticks are kept; Put it back up, then publish, and it's back.
  • πŸ›‘ Take every view down (on the Kennel Card page) does that to every view at once.
  • New link gives a view a new random link. The old one says taken down; the new one works from your next publish. Use it when a link went further than you meant.
  • Deleting a view leaves taken down at its link too.

With the kennel host, a take-down, a new link or a delete reaches the published page at once β€” it doesn't wait for Publish. With a downloaded site, it reaches your host when you next publish and upload.

The β€œtaken down” page is the same for every link: no name, no date, no colour. It says nothing about whose card it was.

Note

The safeword doesn't take your pages down. It pauses play and everything you granted; your Kennel Card is a page you chose to publish, and stays up. Take it down with the button beside it.

Stats

Tick the numbers a view shows: lifetime totals (time locked, longest lock, locks, training done, spins, photos…), your streak, level and rank, and weekly rates over the last four weeks. They're worked out as of the moment you publish, like everything else on the page.

  • A number that can't be measured yet is left off, never shown as 0 β€” a weekly rate needs at least two weeks of log.
  • Never on a Kennel Card: your timer's time to go or anything about a lock in progress (a page is a snapshot that stays up; a countdown on it is wrong a minute later, and a hidden timer stays hidden), what your reflections or check-ins say, a training title, your health, where you were, Mischief, or your own activities' numbers (Handlr can't tell a gym visit from a medication).

Trophy case

Badges, from the papers your Pup ID signed when you earned them β€” so a Trophy case needs a Pup ID. Only badges earned on this box, signed by the Pup ID it holds now.

A badge that states a number goes only on a view that already shows that kind of number β€” the same rule your pack's Handler is held to. Long Haul says how long you were locked, so it needs a time stat ticked on that view; First Handlr needs the locks count; a badge earned with a toy needs the Toy box on, and a badge about one toy needs that toy ticked. The view's page says what each badge is waiting for. A badge Handlr never shows anyone else β€” one counting something private, or whose rule it can't read β€” isn't offered at all.

If a Handler countersigned a badge, the page says what they signed β€” that this is dated inside their collar, not that they saw it happen β€” and whether they signed they're a person or an AI. It never names them: they're someone else, and nobody asked them. The page never calls a badge "verified", and never carries the papers themselves (they hold ids that would tie the page to your pack).

Signing a view

Tick πŸ” Sign it with my Pup ID on a view, and publishing adds a manifest.sig beside the page: every file of that view β€” the page, its data, its QR code and every photo β€” with its SHA-256 hash, signed by your Pup ID. The page shows your Pup ID's fingerprint (on your Tag, or at the bottom if the Tag is off).

Warning

A signature is a name. Two signed views carry the same Pup ID, so anyone holding both can tell they're yours β€” and match them with the cards you give your pack. Unsigned views can't be lined up. Sign only the views where proving it's you is worth that.

Your Pup ID has to be ready (made, unlocked, not moving) to publish a signed view. If it isn't, publishing stops and says which view β€” Handlr never publishes a view you ticked to sign without its signature.

Checking a signature

Anyone can check a signed view without Handlr, using tools/kennelcard/verify.py from Handlr's source code (it needs Python 3 and the cryptography package):

pip install cryptography
python3 verify.py https://kennel.example.org/k/<link>/

or point it at a saved copy of the view's folder. It checks that manifest.sig is signed by the did:key it names, that every file it lists has exactly that hash, and (for a folder) that no other file sits beside them. Change one byte of manifest.json β€” or the page, or a photo β€” and it says does NOT check, and which file.

What it proves: whoever holds that Pup ID's key published exactly these files. Whether that key is the pup you think it is, you decide by comparing the fingerprint it prints with one they gave you some other way β€” in person, in a pack, on their own profile. Don't use a verify.py that came in the same folder as the page it's checking.

The format, for anyone writing their own checker: manifest.sig is JSON β€” {"t": "latch.kennelcard.sig", "v": 1, "did", "signed_at", "files": {path: sha256 hex}, "sig"}. sig is base64url Ed25519 over latch/1/kennelcard + a newline + the object without sig as canonical JSON (keys sorted, no spaces, UTF-8). The did is a did:key (z6Mk…, multicodec 0xed01); refuse one that decodes to a small-order point.

A toy box share card

A view with its Toy box on offers a share card of it β€” story (9:16) or square β€” with the toys and gear ticked on that view, each with its first ticked photo, dated by the moment you make it. Up to eight on a story card, six on a square one. Never a wish (the wishlist stays on your Kennel Card only), never the retired shelf, and no photo metadata.

QR codes and a printable card

Once where it will live is set, each view's page offers its QR code as a PNG or SVG, and a printable card: business-card sized (3.5 Γ— 2 in at 300 dpi), with the name on your Tag (only if that view shows your Tag), your colour, and the QR code. Never a photo, and the link is only in the QR, never spelled out. For handing over at a munch or a con.

Sending it to your pack

A view can go to a pack you're in β€” to its Handler, to the pack-mates you show, or both β€” through the pack's den, beside your pack card. Unlike a link, it reaches only them.

  1. On /consent, grant that pack's Handler (or its mates) Your Kennel Card.
  2. Under 🐺 Your packs on the Kennel Card page, pick which view each one gets. What they get β†— shows exactly what will be sent, as of now.

What goes is the view cut down to what you've granted them on /consent:

On the view Goes only with
a toy Β· a piece of gear Toys you pick Β· Gear you pick
a wish the grant for its kind (a toy wish with Toys you pick)
any photo, the avatar too Photos you pick
a stat the grant for that kind of number: time locked needs The clock, locks Lock status, streak and level Streak and level, training Today's duties, photos Photos you pick
a badge everything it needs to be shown to them anywhere in Handlr
your Tag, your Leash Your Kennel Card alone

So pack-mates, who can never be given the clock or your duties, never get a time stat, a training count or a badge like Long Haul β€” whatever the view shows. The view's own ticks are still the item-by-item choice: no second tick per photo.

  • The safeword withdraws it. Your Handler sees paused in its place; your pack-mates' copies simply disappear, as if nothing were granted. It comes back when you resume. (Your public link pages stay up β€” a pause doesn't unpublish.)
  • A Handler gets it only while you wear their collar, like your pack card.
  • Revoke, untick or leave and the den is told to forget it, with its photos, on the next sync.
  • Photos travel at 512 px, re-encoded with no metadata, like your pack card's.
  • It can't be lined up with your public pages: its item ids and photo names are its own.
  • A den running an older Handlr doesn't carry Kennel Cards: it drops what your box sends it, unread. The Kennel Card page says so, and nothing reaches them through it until the den updates.

Your Handler's picks

Your Handler can star things or leave a short note on them for your Kennel Card β€” "my favourite on you", "get yourself this one". Two Handlers can:

  • Your AI Handler, when you grant it Pick from your Kennel Card on /consent. While that's on it's shown the names of all your owned toys and gear and every wish, so it can pick from them β€” on this box only, like everything your AI sees.
  • A pack's Handler, under the same grant in that pack's section. They pick only from what the Kennel Card you send them shows; anything else is refused.

Nothing they pick shows anywhere until you say yes. Picks wait under 🐾 Your Handler's picks on the Kennel Card page: Yes or No (a no deletes it). A yes shows it on the views where you turned on the 🐾 Handler's picks block, from your next publish β€” the publish page lists it. On the page it reads 🐾 my Handler's pick and 🐾 my Handler: "…" β€” your Handler is never named. Take back removes a yes.

A note is screened like any line your Handler writes: one sentence, no link, no training title, no other item's name, and nothing about time while your timer is hidden. Picks count against the grant's daily cap, stop while you're paused, and a new pick on the same thing waits for you again.

Reading other pups' cards

Paste another pup's Kennel Card link under πŸ“– Other pups' cards and Handlr keeps a copy you can open here, even if their page later goes away.

  • It reaches out once, on your tap. Handlr fetches that card's data (manifest.json), its signature if it has one, and its photos β€” and nothing else. It never refreshes on its own: tap Refresh to read it again. This is the module's one declared connection, listed on /settings.
  • Their page is never opened here. Handlr doesn't fetch their index.html at all. The copy is drawn by Handlr's own page from their data, with every word escaped, every link checked (https:// or an email address only, trackers stripped), and every photo re-encoded β€” pixels only, nothing from the original file. Anything that doesn't fit the shape of a Kennel Card is left out.
  • Signed cards are checked. If the card has a manifest.sig, Handlr checks it against the files it actually fetched and shows the Pup ID's fingerprint β€” worked out from the signature, never taken from what the page claims. A card whose signature doesn't check says so, loudly, and shows no fingerprint.
  • A new Pup ID is flagged. Handlr remembers which Pup ID signed a card the first time you read it. If a later read is signed by a different one β€” or isn't signed any more β€” the copy says so until you Trust the new Pup ID (only after you've asked them).
  • Forget removes the copy and its photos from this box.
  • On a hosted Handlr, a link to a private or local address (a home network, localhost) is refused.

Copies of other pups' cards are in your full backup, and never in a share export or pup.json.

The kennel host

An optional second container that serves your published site, and nothing else. Turn it on in .env beside docker-compose.yml:

HANDLR_KENNEL_DIR=/kennel
COMPOSE_PROFILES=kennel

then docker compose up -d --build. It listens on port 8771. Point your public reverse proxy (or tunnel) at that port β€” never at Handlr's.

  • It's a stock nginx serving only the folder Handlr publishes into, read-only. It isn't on Handlr's network, mounts nothing else, and has no route to the app or its database to forward a request to.
  • Every response carries the page's own Content-Security-Policy, Referrer-Policy: no-referrer, X-Robots-Tag: noindex and Cache-Control: no-cache (so a take-down reaches people who already opened the page). No directory listing, no server version.
  • Handlr only publishes into an empty folder or one it published into before. It never clears out a folder that has someone else's files in it.

If Handlr's own port is published on all interfaces, any container on the host can reach it, the kennel host included. Bind Handlr to 127.0.0.1 or firewall its port (see Install).

Questions

Is anything sent anywhere? Building and publishing open no connection: publishing writes into a folder on this machine, and whatever serves that folder is the only thing the internet talks to. The one thing that reaches out is reading another pup's card β€” only when you paste a link or tap Refresh, and /settings says so.

What's in my backup? Your views, ticks, links and blurbs are in the full backup, like everything else. The shareable export and pup.json carry none of it β€” a view's link belongs to this box.

This page is docs/kennelcard.md in the repo. Spotted a mistake? Tell us how to report it β†’