Spool ids and QR links
Ratified 2026-11-05 — not implemented yet. Apps build on this contract. The points still open are listed at the end; settling one changes only what it names. Sections marked Proposed are not ratified yet: do not build on them before they are.
The rules every TigerSystem app follows:
- how a chipless spool (TigerData / TigerData+) gets its id;
- how a spool keeps its link when it gets a chip;
- how a spool’s id is written into the QR code of a printed label, and read back;
- how an app calls a spool with values (a weight, a note) through a link;
- (proposed) how a product gets its own link;
- (proposed) how a link answers in JSON.
1. Spool ids
Section titled “1. Spool ids”A spool is a document of the account’s inventory,
users/{uid}/inventory/{spoolId}. The document id is the spool id: one
account cannot hold two spools with the same id.
| Spool | Id | Example |
|---|---|---|
| Chip (TigerTag / TigerTag+) | the chip’s NFC UID, uppercase hex, 14 characters (an NTAG’s 7-byte UID) | 04AB12CD3E4F80 |
| Chipless, minted from now on | TigerData_ + epoch milliseconds (13 digits) + a random number 0000–9999 (always 4 digits) |
TigerData_17623456789014213 |
| Chipless, minted before this contract | TigerData_ + 10 random digits |
TigerData_4480517985 |
| Chipless, legacy prefix | CLOUD_ + 10 random digits |
CLOUD_4480517985 |
| Legacy decimal (old Android app) | the UID written in decimal, being migrated to hex | 1234567890123456 |
Ids already minted are never rewritten by this contract. Readers only test
the prefix (TigerData_ / CLOUD_ → chipless); none of them depends on the
number of digits. The id is an identifier, nothing else: never derive a
creation date from it. A 4-byte UID (a proprietary chip linked to a spool) is
never the name of an inventory document, so it never appears in a link.
Minting a chipless id
Section titled “Minting a chipless id”Every app that creates chipless spools (Tiger Studio today, the mobile app later) mints the id the same way:
- Compose
TigerData_<Date.now()><four random digits>. - Local check — if the id is already in the inventory the app holds (the
live listener’s copy, offline cache included) or in its
uidMigrationMap, compose another one. This works offline. - Write
- online: in a transaction that reads the document first and fails if it exists; on failure go back to step 1;
- offline: a plain write, queued by Firestore until the network returns. Steps 1 and 2 are the only guard.
A plain set() on an id that already exists overwrites the spool already
there, silently. That is what step 3 prevents.
The four random digits also make a new chipless id practically unique across all accounts (about one collision every 600 years at a million chipless spools a year), so a server can later resolve a link for someone other than the owner without changing any printed label.
2. Keeping the link when a spool gets a chip
Section titled “2. Keeping the link when a spool gets a chip”Programming a chip on a chipless spool moves the spool to the chip’s UID. In one batch:
- write the spool under the chip UID (and the twin UID, for a twin pair);
- delete the
TigerData_…document; - write the redirect
users/{uid}/uidMigrationMap/{old id}→{ hex_uid, migrated_at }.
The batch makes the three happen together or not at all. The redirect lives in
uidMigrationMap (the collection that already maps legacy decimal ids to hex),
never as a stub document in the inventory: everything that reads the inventory
counts one document as one spool.
3. QR links
Section titled “3. QR links”A printed spool label carries a QR code holding a link to the spool, and nothing else (no weight, no note — printed values go stale):
HTTPS://QR.TIGERTAG.IO/<id><id> is the spool id without its prefix:
| Spool | Link |
|---|---|
| Chip | HTTPS://QR.TIGERTAG.IO/04AB12CD3E4F80 |
| Chipless (new) | HTTPS://QR.TIGERTAG.IO/17623456789014213 |
| Chipless (before this contract) | HTTPS://QR.TIGERTAG.IO/4480517985 |
CLOUD_ |
no QR link — the label is printed without a QR code |
| Legacy decimal | no QR link — migrate the spool to hex first |
Encoding
Section titled “Encoding”- The whole link is uppercase. Only digits, uppercase letters and
$%*+-./:fit the QR alphanumeric mode, which keeps the code small; one lowercase letter or_switches it to byte mode. Schemes and host names are case-insensitive, and UIDs are uppercase hex already. - Segments, with the mode set explicitly (generators default to byte mode):
- chipless id (all digits):
HTTPS://QR.TIGERTAG.IO/in alphanumeric mode, then the digits in numeric mode; - UID: the whole link as one alphanumeric segment (a second segment’s header would cost a size).
- chipless id (all digits):
- Error correction M (15 %): labels get scratched and folded.
- Result: 25 × 25 modules for every spool. Draw each module on a whole number of printer dots, with the standard 4-module quiet zone.
Reading a link back
Section titled “Reading a link back”Readers parse the id from the link offline: an app scanning a label never
needs the website. They read the path case-insensitively, ignore a trailing
/, and resolve it by its shape:
| Path | Spool id |
|---|---|
| 17 digits | TigerData_<17 digits> |
| 10 digits | TigerData_<10 digits> |
| 14 hex characters | the UID, uppercased |
13 hex characters starting with 4 |
a truncated NTAG UID: 0 + the 13 characters (repair rule) |
P/ + 8 hex characters |
(proposed) a product — section 5 |
A .JSON ending asks for the answer in JSON — (proposed), section 6.
Apps write the short form above, but readers also accept the long form:
the TigerData prefix in any case, followed by _ or -, then the 17 or 10
digits — TigerData_17623456789014213, TigerData-17623456789014213,
TIGERDATA-17623456789014213, tigerdata_4480517985 all resolve to the same
spool. A link typed or shared by hand keeps working. The CLOUD prefix is not
accepted in either form.
A UID may be all digits, but it is 14 characters long, never 17 or 10, so the
length always decides. No legacy decimal id reaches a reader, since none is ever
printed. Any other shape is a label from a newer version: the reader says so
(“update the app”) instead of failing. Every other kind of thing gets its own
folder, a letter and a / (P/ for a product, later R/ for a rack), so it can
never clash with spool links already printed.
Resolving the spool
Section titled “Resolving the spool”-
Look the id up in the signed-in viewer’s own inventory.
-
Not there → look it up in the viewer’s
uidMigrationMapand follow the redirect. -
A twin pair has one label, with the main UID; either UID opens the spool.
-
A deleted spool (
deleted: true) opens as “deleted”, not as an error. -
(proposed) Anyone else (not signed in, or not allowed to read the spool) gets the spool’s product, never the spool: the server finds the spool by its id across accounts (an index on the spool id), reads its
id_productand answers with the product page of section 5. No product, no match, or the same id in two accounts with different products → no product is shown. The lookup runs only for those scans, so its cost follows them, not the number of accounts.
Privacy: knowing an id never shows a spool. A spool is shown only to someone the Firestore rules already let read it (the owner, an accepted friend, a public inventory). Ids contain a timestamp and can be guessed; that must never matter. The product of a spool is not private: it is printed on the label, and a TigerTag+ chip carries it in clear for any NFC phone. The weight, the note, the rack and the owner never leave.
4. Calling a spool with values
Section titled “4. Calling a spool with values”An app may build a link that opens a spool and proposes values, with ordinary query parameters (these links are never printed, so readability wins over QR size):
https://qr.tigertag.io/04AB12CD3E4F80?weight=640¬e=Opened%20in%20JuneParameters
Section titled “Parameters”| Parameter | Value | Limit |
|---|---|---|
weight |
net filament weight, in grams | whole number, from 0 to the spool’s capacity |
note |
the spool’s note (the message field) |
plain text, 28 bytes UTF-8 at most (the chip’s slot) |
Names are case-insensitive. An unknown parameter is ignored, never an error. A value out of its limit is refused. New parameters are added to this table, never invented by one app alone.
Who can apply them
Section titled “Who can apply them”| Situation | What happens |
|---|---|
| Not signed in | Values refused. The link offers to sign in, then opens the spool without them. |
| Signed in, not the owner (a friend’s spool included) | Values refused. The spool opens if the rules allow it. |
| Signed in, the owner | The spool opens with the values pre-filled, and one gesture saves them (“Update: 640 g”). |
A link never writes by itself: anyone can craft one and send it, and being signed in does not prove the person meant to apply it. An app that has to write without a confirmation (a scale updating the weight continuously, an integration) does not use links: it writes through the user’s account under the Firestore rules (integration guide).
5. Product links (proposed)
Section titled “5. Product links (proposed)”A product — a filament of the catalogue, whoever owns a spool of it — has its own link, for packaging, a shop shelf, a showroom or a printed catalogue:
HTTPS://QR.TIGERTAG.IO/P/<id_product><id_product>is written in hexadecimal, always 8 characters, uppercase, zero-padded: the 4 bytes as they sit on the chip, so a firmware or an NFC reader builds the link without converting. Example: product2855882328→HTTPS://QR.TIGERTAG.IO/P/AA394E58, 25 × 25 modules.- The decimal form stays where people see the number — the catalogue
(
tigersystem.io/<lang>/catalog/2855882328), the text printed under a QR code, a Code 128 barcode — and never goes into aqr.tigertag.iolink. P/00000000andP/FFFFFFFFmean “no product”: never printed, answered with404.- A spool label never repeats its product: the spool link already leads to it (section 3).
The scan page
Section titled “The scan page”P/… opens a page built for a phone just after a scan, not the catalogue page:
- the photo and the colour swatch, large; brand, material, colour name;
- the print settings as large tiles: nozzle, bed, drying (temperature and time), density, diameter;
- the documents (TDS, MSDS, RoHS, food contact) and the video;
- where to buy it;
- not signed in → “Start my inventory”; signed in → “Add to my inventory” (a TigerData+) and “Add to favourites”;
- “See it in the catalogue”, to the full page.
6. JSON answers (proposed)
Section titled “6. JSON answers (proposed)”A link ending in .JSON (any case) — or sent with Accept: application/json —
answers in JSON instead of a page. These are addresses for tools, never printed.
| Link | Not signed in | Signed in (Authorization: Bearer <Firebase ID token>) |
|---|---|---|
…/P/AA394E58.JSON |
the product record | the same |
…/04AB12CD3E4F80.JSON |
401 — no spool data |
the spool, if the rules let the account read it |
- Spool: a
.ttagfile holding that one record, the inventory document as Firestore stores it. It carriesid_product; a tool that wants the product calls…/P/<id_product>.JSONnext. - Product: the catalogue record as the product API returns it, in the
language of
?lang=(English by default). Cached on the server and allowed from any site. - A spool answer is never cached: it depends on who asks.
- A JSON request never writes: link values (
?weight=,?note=) are ignored there.
Open points
Section titled “Open points”- Uppercase scheme.
HTTPS://in uppercase is recognised as a link by the iPhone camera (tested 2026-11-05, chip and chipless links). Still to test: Google Lens, the Samsung camera and a basic scanner app. If one fails, the link goes lowercase (https://qr.tigertag.io/in byte mode + the id), which costs one size (29 × 29). - App links. iOS (
apple-app-site-association) and Android (intent filters) match paths case-sensitively by default: the mobile app must declare case-insensitive matching (iOS) and every form (Android). - Domain.
qr.tigertag.iois printed forever: renewtigertag.ioon a long term.
Related: Universal filament identity, Inventory & cloud sync, Developer overview