toolshed/docs/design-in-progress/items-labels.md
2026-08-20 05:19:53 +02:00

131 lines
8.6 KiB
Markdown

# Item Handles & Physical Labels (Design in Progress)
Status: not implemented. This document collects the problem, goals, and open design questions for
giving inventory items stable identifiers and physical (scannable) labels. Nothing here is
settled.
## Problem
As described in federation.md's "Unique Handles" section, an item today is identified only by a
local id scoped to its owner, it isn't given an explicit, portable handle the way a tag, property,
or category is. That's fine as long as the only thing ever addressing an item is the owning user's
own signed API traffic. It stops being fine the moment something *outside* that loop needs to
refer to the item:
- A friend who borrowed a physical tool has no way to look it up other than finding it in the
owner's shared inventory list by eye.
There's nothing you could put on a sticker.
- If items ever need to be referenced from outside their owner's own requests (a group's shared
view, a lending record, a printed label), there's currently no stable identifier to reference
that's meaningful outside the owner's own account.
- A local database id isn't something we'd want to expose or rely on externally: it's an
implementation detail of one backend's storage, not a handle with the same guarantees
(uniqueness, meaning, longevity) the rest of the federation model gives every other kind of
entity.
Put simply: every other kind of thing in Toolshed (users, tags, properties, categories) has a
handle that means something outside of one database. Items don't, and physical labeling is the
clearest case where that gap actually matters.
## Goals
- Give an item a handle that's meaningful and resolvable outside its owner's own account, without
requiring items to become shared/reusable entities the way tags are (an item is still owned by
exactly one person; see the "Items" subsection of federation.md for why that keeps things
simple).
- Support a physical label (QR code, barcode, or similar) that can be printed and stuck on a real
object, such that scanning it gets you to the right item on the right backend.
- Make the label survive the normal life of a physical object: it gets lent out, comes back,
maybe changes which storage location it lives in, all without needing a new label printed.
## Non-goals (for now)
- Turning items into shareable/reusable entities across owners (that's what tags/categories are
for; see [tags.md](tags.md)). An item handle identifies *this specific person's specific thing*,
not a class of thing.
- Solving inventory tracking/auditing (check-in/check-out logs) as a whole system; that can build
on top of a stable item handle once one exists, but isn't the same problem.
## Open design questions
**What does the handle look like?**
The natural extension of the existing scheme is owner handle + local id, that's enough to be
globally unique (no two users share a handle, and ids are already unique within one user's
inventory) without inventing a new namespace. Worth deciding whether the id should be the existing
internal database id (simple, but leaks a little implementation detail and a rough count of
someone's inventory) or a separate opaque id generated for exactly this purpose (see the
unguessability question below).
Two distinct formats are needed, because "an item handle" is used in two different situations:
- *A compact handle, for use where context already makes clear it's a Toolshed item.* Inside the
app, in exports, in logs, anywhere the reader already knows they're looking at Toolshed data,
the handle doesn't need to spell that out or be openable on its own. This can be as short as
`user@domain.tld:id`, the item's owner handle with `:id` appended, mirroring how a tag/category
handle already appends `:name` after its origin (see federation.md's Unique Handles section).
No new delimiter concept, just the same pattern applied to items.
- *A self-contained URL, for use with no context at all.* A physical label, a link shared outside
the app, has to work without the reader already knowing what it is or which server it belongs
to, so it needs to open directly to the right frontend, resolve the right backend, and land on
the right item. That means it has to encode the same information (owner handle + item id) as a
full URL, e.g. `https://toolshed.webdomain.tld/i/alice@example.com/42`, note the owner's handle
can be embedded in a path segment as-is (`@` doesn't need escaping in a URL path), which keeps it
one segment shorter than splitting the handle back into `domain/user`, and means the handle is
visible unmodified inside the link rather than reassembled from separate parts. If the owner is
ever a group rather than a user (see groups.md), its handle carries a leading `#`, which does need
the `+`-for-`#` substitution described in [handles-and-shortids.md](../handles-and-shortids.md)'s
Handle syntax section before it can sit in a path segment, e.g.
`https://toolshed.webdomain.tld/i/+climbing@example.com/42`. The frontend host
in this URL (`toolshed.webdomain.tld`) doesn't have to be, and generally won't be, the backend
authoritative for `example.com`, any frontend can resolve any handle (see federation.md's Servers
subsection), so this is just whichever frontend happens to be handling the link, not part of the
item's identity.
Both formats should stay as short as the encoded information allows, this matters most for the URL
form, since it's the one that ends up in a QR code or printed label where physical size is a real
constraint (see the labels goal above).
**How does this fit with the frontend's existing routes?**
There's already a `/inventory/shared/:user/:id` route (`InventoryDetailForeign`), but today
`:user` is just a bare username with no domain, i.e. it only works for a friend on the viewer's own
domain, and the view itself doesn't yet do anything domain-aware with that param. A resolvable
global handle needs the full `user@domain.tld` and a lookup step this route doesn't have yet. Two
ways to reconcile that: extend the existing route to take a full handle in the `:user` segment
(`/inventory/shared/alice@example.com/42`, no new route shape needed, just a richer meaning for the
param it already has), or treat the short `/i/...` URL as a dedicated, minimal entry point whose
only job is to resolve a handle and then hand off into whatever the richer in-app view ends up
being. The two aren't mutually exclusive: the short form is what needs to be small enough to print,
the in-app route doesn't have the same constraint and can stay more descriptive.
**What does scanning a label actually do?**
Probably: the label encodes a URL or handle-like string; scanning it opens the frontend, which
resolves the owner's domain the same way it resolves any other handle (see federation.md), and
lands on that item. This reuses the discovery mechanism that already exists for logging in as a
handle, rather than inventing a second one.
**Does resolving a label require authorization?**
An item's availability policy already controls who can see it (owner-only if private, friends if
shared, etc.). A label should presumably respect the same policy rather than being a backdoor that
makes a private item visible to literally anyone who finds the physical object and scans its code.
That means resolving a label isn't a free public lookup, it goes through the same friend/signature
checks as everything else, which has UX implications (an anonymous finder of a lost tool can't
necessarily see who it belongs to).
**Does the label need to be opaque/unguessable?**
If item ids are small sequential integers, a label built from a guessable id lets anyone enumerate
a user's items by scanning or guessing nearby numbers, even if each individual lookup is
authorization-checked. Probably wants some amount of unguessability even before authorization is
considered, as a defense-in-depth measure.
**What survives item changes?**
Storage location, availability policy, name, and description can all change over the life of an
object without it becoming a "different" item. The label should point at the handle, not at any of
that mutable data, so none of those changes require a new label. The one thing that probably *does*
need a decision is deletion: does a handle ever get reused, or is it retired for good once an item
is deleted (retiring seems safer, avoids an old label resolving to an unrelated new item later)?
**Relationship to lending/borrowing.**
A scannable label is the obvious hook for a future "mark as borrowed / returned" flow. Not solving
that now, but the handle scheme chosen here should be able to carry that later without a redesign,
i.e. it should be able to identify the item independent of who currently physically has it.