1D Code128 Identifier for Long Round Objects — Design
1D Code128 Identifier for Long Round Objects — Design
Created: Thu 18 Jun 2026 12:13:37 CEST Updated: Sun 21 Jun 2026 09:50:03 CEST - Locked capture resolution (1080p) and dual-polarity decode via adaptive escalation, from the completed probe matrix Document Version: 1.2 - Resolution + polarity locked Security Classification: Internal Technical Documentation Target Audience: Backend Developer, iOS Developer, Keygen/Tooling Author: Paul Wisén
Goal
Add a 1D Code128 barcode as a physical identifier format for long, round objects
— fishing rods, cable sections — where a 2D QR code does not fit. The barcode encodes a
compact, non-URL payload <prefix>-<path> that resolves to the same ObjectInstance as the
existing QR/NFC/RFID identifiers, through a unified resolution model.
This is a new encoding of the existing identifier, not a new identity scheme. No new cryptography is introduced; the HD-wallet identification layer is untouched.
Why this format (problem framing)
The real constraint on a round object is curvature, not surface area. Only a narrow strip down the middle faces the camera flat; everything toward the sides curves away and distorts.
- A 2D code (QR / DataMatrix) needs two flat dimensions. On a cylinder it must shrink to fit the narrow flat strip → too small to carry the ~80-char scan URL → unreadable.
- We do have abundant flat length along the object’s long axis. A 1D linear barcode oriented along that axis lives entirely in the flat strip: the reading direction follows the axis (undistorted), and bar height (the curved, circumferential direction) carries no data — it is only aiming margin. This matches the geometry exactly.
iPhone reads Code128 natively and quickly via Vision / AVFoundation, on-device.
Trade-off acknowledged: Code128 has no error correction — unlike QR (Reed-Solomon, tolerant of ~7–30 % damage), a single damaged or misprinted module kills the whole symbol. For objects that get scuffed outdoors this is a real robustness cost. 1D wins on the round-object geometry; QR wins on damage tolerance — they are complementary, not interchangeable (see “Scanner-probe findings”).
Scope
In scope
- The wire format of the 1D payload:
<prefix>-<path>(base58, hyphen-delimited). - Keygen: emit the 1D payload string (and optionally a Code128 image) per tag, alongside the existing QR/URL output. Configurable prefix length.
- iOS scanner: decode Code128, parse
<prefix>-<path>, resolve via the existing identifier-resolution path. - Backend: a path-keyed resolver (resolve by
path, verify by variable-length key material) with an enforced minimum prefix length. This is the unification described below. - Dual-print enrollment: print 2D + 1D together; the 2D registers the object, the 1D is affixed to the object.
- Documentation updates and per-repo ROADMAP entries.
Out of scope
- Master-key-in-system derivation of unissued tags (the “future mode” below) — design context only; no key material is loaded into the system yet.
- Solana / on-chain commerce.
- Any change to the HD-wallet derivation, key handling, or genuineness algorithm itself.
- Migrating the existing QR resolver onto the unified path (a de-risked fast-follow, see “Resolution model”).
- NFC / RFID for round objects.
The payload format
<prefix>-<path>
prefix— the first N base58 characters of the instance key. Variable length. It acts as a MAC over the path: an attacker can read and guess apath, but cannot compute the matching prefix without the master key (ed25519 / SLIP-0010 derivation is hardened-only — there is no public child derivation to enumerate with).path— the HD-wallet derivation path in base58 wire form, dot-separated segments (e.g.4.2.3.2.2). This is the same value the QR URL carries inp=. The decimal form in keygen CSVs is human-readable display only; on the wire the path is base58 per segment (confirmed:Plings-Gateway/lib/validators.ts:59— “Path segments are base58-encoded numbers (per the minting tool), NOT decimal”). Small numbers (1–57) look identical in both bases;300decimal is6Bin base58.-— delimiter.-is not in the base58 alphabet and is not the path separator., so the split is unambiguous. Splitting on the first (only)-yieldsprefix(left) andpath(right). The delimiter is what makes the prefix length self-describing — no length field or version marker is needed in the string.
The 1D payload is deliberately not a URL. On a 1D barcode the stock iOS Camera does not reliably offer to follow a link (that behaviour is effectively a QR feature), so a 1D scan is app-scan territory regardless. A bare token is shorter and cleaner. The flat QR (on cables via cable-tie/flag) continues to serve the stock-camera / URL path. The two formats complement each other.
ShortCode alongside (human reference). The 4-char shortCode (= first 4 chars of the
instance key, e.g. CCue) is printed/etched legibly next to the barcode as a human glance
and manual-entry backup. It is always a sub-prefix of the barcode’s prefix, so it is
consistent at any chosen prefix length. The shortCode is not the resolver (it is not
unique).
Examples / test vectors
Dev keys (deterministic test data — public, safe to discard):
| Instance key (full) | path | shortCode |
|---|---|---|
CCuejfEAmFGKaTqhjgXjxQepiY9CZyEB463wtA4izNb9 |
4.2.3.2.2 |
CCue |
DEPQwvq461U4yVXG8MxzvR6LpcuUMeB5o84rGAb1Bfra |
4.2.3.2.3 |
DEPQ |
1D payloads at several prefix lengths (path 4.2.3.2.2 / 4.2.3.2.3):
| Prefix len | Example payload | Total chars | ~Barcode length* |
|---|---|---|---|
| 8 | CCuejfEA-4.2.3.2.2 |
18 | ~50 mm |
| 12 | CCuejfEAmFGK-4.2.3.2.2 |
22 | ~61 mm |
| 16 | CCuejfEAmFGKaTqh-4.2.3.2.2 |
26 | ~73 mm |
| full | CCuejfEAmFGKaTqhjgXjxQepiY9CZyEB463wtA4izNb9-4.2.3.2.2 |
54 | ~130 mm |
* Code128 ≈ 11·chars + 35 modules; reference point: 44-char payload ≈ 110 mm on the test
printer → ~0.21 mm/module. Longer/deeper paths increase length. These are estimates to be
confirmed by print tests.
Prefix length — variable, with a backend floor
Because path carries identity and prefix is only a MAC, prefix length is a strength
knob the tag issuer (manufacturer) chooses per object value vs. label length — not a fixed
constant.
| Prefix | Bits (base58 ≈ 5.86 b/char) | Role |
|---|---|---|
| 8 | ~47 | Absolute floor; below the 64-bit MAC guideline |
| 12 | ~70 | Recommended default; clears the 64-bit MAC floor |
| 16 | ~94 | Margin; only meaningful in the path-less fallback |
| full (44) | ~256 | Strongest; preserves full cryptographic genuineness |
Backend owns the floor. Length is attacker-visible in the string, so a forger must not be able to submit a 2-char prefix and guess cheaply. The backend rejects any prefix below the floor (proposed: 8 absolute minimum, ≥12 recommended, full for high-value goods). The issuer chooses anything at or above the floor. The floor may be tightened per manufacturer/batch later — that lives in the issuance policy, not in the string.
Resolution model (unification)
The string formats stay deliberately different — QR is a URL, 1D is <prefix>-<path>.
What is unified is everything after parsing. Both forms carry the same two canonical
values, just wrapped differently:
URL "…?t=q&i=<full>&p=<path>" ─┐
├─► { path, keyMaterial } ──► one resolver + verifier
1D "<prefix>-<path>" ─┘
- Two thin, format-specific parsers (URL query parser; split-on-hyphen) each normalize to the
canonical tuple
{ path, keyMaterial }.keyMaterialis the full instance key (QR) or the prefix (1D) — the same axis at different lengths. - Resolve the object by
path(the common denominator, globally unique, present in every form). - Verify by comparing the scanned
keyMaterial(length L) against the resolved object’s stored instance key truncated to L. Match → verified-to-strength-L.
Why path is the resolution key: the instance key is derived from the path, so i and
p are 1:1; resolving by either yields the same object, and path is the one value every
form carries. This generalizes cleanly to future formats (each is just “path + verifier of
length L”).
QR migration is a fast-follow, not part of this work. Today the QR resolver keys on i.
Re-pointing it at the path-keyed resolver is backward-compatible (the URL has both i and
p) but touches working code and requires path to be indexed/unique in Postgres. To
de-risk: build the path-keyed resolver for 1D now; migrate QR onto it as a separate PR.
This spec describes the unified target and marks the QR migration as its own step.
Two operating modes
The instance-level MAC can only be cryptographically verified by re-deriving the full key from the path, which is hardened HD derivation → requires the master key (or a delegated key in the HD chain). The system has no key material loaded today. Hence two modes:
- Known-tag lookup — available now. The object already exists in the DB (it was enrolled,
see below). Resolve by
path; the stored instance key disambiguates and verifies the scanned prefix (a tamper / consistency check). No master key needed. - Unissued-tag derivation — future. With key material loaded (HSM), the system re-derives
the full key from a scanned
path, checks the prefix MAC, and creates the object on first scan. Requires keys in the system; slots into the same resolver as an additional branch when the object lookup misses.
In mode 1 the prefix’s anti-forgery strength is secondary (enrollment is gated by the full key — see below); it becomes the primary defence in mode 2.
Dual-print enrollment (the bootstrap bridge)
This is how the 1D format works today, before any key material is in the system:
- Keygen prints both the standard 2D QR (full URL,
i+p) and the 1D Code128 (<prefix>-<path>) for the same tag. - Scan the 2D QR → the full instance key + path register the object → the object becomes known.
- Affix the 1D barcode to the round object. In the field it resolves via known-tag lookup (mode 1); the master key is never needed in the field.
Security property: object creation is gated by the full instance key delivered in the
2D code. A guessed path on the 1D side resolves to nothing (no object was enrolled for it),
so no forged object can be created by enumerating paths. The derivation mode (2) is a pure
future upgrade, not a blocker.
The 2D used for enrollment is the existing QR — no new enrollment artifact. The keygen change is to also emit the paired 1D payload and lay them out together for printing.
Transport tag (t)
Add a transport value t=c (Code128) for analytics, alongside q/n/r/b. This keeps
the “routing never branches on transport — t is analytics only” invariant.
Nuance: the 1D payload carries no t (it is not a URL). The scanner sets t=c itself
when it has decoded a Code128, and passes it to the GraphQL API — unlike the QR URL, which
embeds t=q in the string. Native apps call the GraphQL API directly, so 1D scans bypass
the Gateway entirely; the Gateway’s TRANSPORTS list (Plings-Gateway/lib/validators.ts)
does not strictly need c, but the API’s accepted-transport set must include it. Add c
to the Gateway list as well for documentation consistency.
Verification trust model fit
Per docs/core-systems/plings_identifier.md and docs/use-cases/verification-trust-model.md:
instance-level verification is not possible offline (hardened derivation needs private
keys); only class-pointer verification is offline (manufacturer-signed SHA256). The 1D
prefix is instance-level — so its cryptographic verification (mode 2) requires the Plings
API with key access. This is a safe operation (verification only, pre-use), not a
trust-boundary violation. In mode 1 there is no cryptographic claim — only a DB
consistency check.
Security analysis
- Enumeration resistance.
pathalone is enumerable and was rejected as a payload. Withpath + prefix, guessing a path is easy but pairing it with the correct prefix requires the master key (hardened ed25519 derivation has no public-child derivation). The prefix is an unforgeable MAC over the path. - Forgery cost. For a chosen path there is exactly one valid prefix; a blind guess
succeeds with probability
1/58^N. At N=12 that is~1/10^21— infeasible offline, and online attempts are rate-limited by the firewall. The backend floor (≥8) prevents short-prefix cheapening. - Creation gate (mode 1). Object creation requires the full instance key from the 2D enrollment scan, so path enumeration on the 1D side cannot mint objects.
- Info leak. A neighbouring path on a label reveals manufacturer/batch structure — the
same information the existing QR URL already exposes in plaintext
p=. No regression.
Scanner-probe findings (2026-06-19)
Empirical results from an on-device scanner probe (branch task/scanner-code128-probe: Code128
enabled on the real AVFoundation stack, a runtime resolution toggle, a 6-code throughput
stopwatch, and an inverted-contrast Vision path). Tagged LOCKED (settled) or PENDING
(needs further on-device tests before final numbers move into the canonical docs).
- Capture resolution — LOCKED: 1080p. The completed probe matrix (720p / 1080p / 4K ×
normal & inverted) settles it. 720p is excluded — slow on normal (~3 s/code) and collapses
with the invert path on (25 s). 1080p reads every case including the hardest (inverted,
6 mm, minimum module width) at ~1.1–1.4 s/code, is as fast as 4K and more consistent (4K
showed a ~14 s outlier), and is lighter on battery/thermal. 4K gives no consistent speed gain —
keep it only as an optional future “boost”. Production: raise
.hd1280x720→.hd1920x1080. This also helps small-QR scanning (less reliance on the zoom feature). - Contrast / polarity — LOCKED: both supported. Standard 1D decoders read dark-on-light
only; white/transparent-on-black (black tape) needs the pixels inverted first
(
CIColorInvert+ contrast boost) and decoded with Vision viaAVCaptureVideoDataOutput. A clean reprint read 6/6 at ≥1080p (the earlier stubborn 1/6 was a localized print defect), so black tape is viable — ~1.6 s/code, ≥1080p, clean print required (Code128 has no error correction, so a print flaw is fatal — see below). Both polarities are supported in one scanner via adaptive escalation (next bullet): the metadata path reads dark-on-light instantly and is unaffected by the invert path running alongside (it just fails silently on a normal label) — confirmed by running the invert path over normal labels. Quiet zones still required either way. A light label band (matching the existing GMP/silver band) remains the most robust choice on dark objects; black/transparent tape is now a supported aesthetic alternative. - Error correction — LOCKED. Code128 has none; one damaged/misprinted module fails the whole
symbol (often invisible to the eye — the likely cause of the stubborn 1/6). QR has
Reed-Solomon (~7–30 % tolerance). A genuine durability argument for scuff-prone objects;
factor into the format choice per object. → propagate to
plings-identifier-overview.mdwhen the format ships. - Engine + adaptive escalation — LOCKED. AVFoundation
AVCaptureMetadataOutputis the default decode path (light; handles dark-on-light Code128 + QR; reads instantly). Because black-tape (inverted) is a supported requirement, the Vision/VNDetectBarcodesinvert path is included — but as an adaptive fallback, not always-on: run metadata only, and enable the Vision-invert path only after ~1–2 s with no decode. This gives both-polarity support without paying Vision’s battery/thermal cost on every scan (a cost a short 6-code test does not surface). At 720p the invert path tanked throughput (25 s) — another reason the floor is 1080p. - NFC alternative — REFERENCE (pending object material). NFC sidesteps all optical issues
(no contrast/print/quiet-zone, colour-agnostic, can hide under a plain label) and is already a
supported transport (
t=n). But readability scales with antenna area, not length, so a thin (~12 mm) strip is a small coil → short, fussy range and tap-the-exact-spot interaction. Conductive (carbon-fibre) objects detune NFC → need a ferrite “on-metal” tag (bulkier); non-conductive (fibreglass/plastic) is fine. Viable as a hybrid (NFC inside + printed shortCode/QR outside), with the Gateway handling both transports identically. - Engraving / direct-part-marking (DPM) — REFERENCE (not yet tested). Engraving will be a common marking method and is the hardest optical case — two problems stack. (a) Polarity: on dark material engraved marks usually appear lighter → inverted again. (b) Worse, the contrast is not pigment but topography/specularity: rough (matte, diffuse) engraved areas vs smooth (specular) background. On polished metal the apparent contrast — and even the polarity — varies with lighting/viewing angle and can flip across a single symbol (glare blows out modules). Decoders assume uniform illumination and one consistent polarity; specular metal breaks both. The AVFoundation metadata path will struggle; the Vision invert path does not rescue it (the problem is variable/angle-dependent reflection, not a fixed inversion). Real levers are lighting + capture technique (DPM uses diffuse/dark-field light a phone can’t provide; on-axis torch often worsens glare on gloss) and code choice. Design implications: (1) engraving is DPM territory where the industry standard is Data Matrix (2D, error-corrected), not 1D Code128 — but 2D needs flat area, conflicting with round-object curvature, so engraved round metal is doubly hard; (2) prefer a marking process that yields consistent dark-on-light (e.g. laser annealing → dark oxide on bright steel) over ablation (light-on-dark) or purely topographic marks; (3) prefer matte over polished where the code sits; (4) reliable metal DPM likely needs a DPM-tuned decoder beyond Apple Vision — a heavier engine decision warranting its own probe; (5) pragmatic fallback: a light printed label band may still win even on metal, or a flat facet for a Data Matrix. Treat engraved metal as an advanced marking case, not the casual phone-scan path. Broader principle: the reader cannot assume one polarity or one symbology — maintain a marking-method × symbology × reader-mode matrix as marking modalities grow.
Per-repo change lists
Plings-API (main)
- Keygen (
scripts/plings_keygen.py+ core): for each tag, derive and emit the 1D payloadinstance_key[:N] + "-" + base58_pathas a newbarcode_payloadcolumn in the generated CSVs, alongside the existingshortcode/decimal_path/base58_path/urlcolumns (the QR/print workflow stays unchanged;shortcodeis alreadyinstance_key[:4], i.e. the prefix at N=4). Prefix lengthNis a parameter (≥ backend floor; default 12). For the validation phase it must be trivial to emit the test lengths — either a CLI/prompt option forN, or emitprefix8/prefix12/prefix16columns so a sheet can be printed at each. Optionally render a Code128 image. Per the keygen-output note inCLAUDE.md, run the generator from the main checkout so the CSV/history outputs persist (a worktree would discard them); the code change still goes via a worktree/PR as usual. - Resolver: add a path-keyed resolution path — input
{ path, keyMaterial }; resolve theObjectInstancebypath; verify scannedkeyMaterialagainst the stored instance key truncated to its length; enforce the minimum prefix length. (Known-tag / mode 1.) - DB: likely no new columns. Confirm the HD path is stored, indexed, and unique on
object_instances(or the path registry); add an index if missing. No schema migration if the index already exists. - Transport enum: accept
t=cin scan logging / analytics.
Plings-iOS (main)
- ScannerSurface / PlingsKit — capture & decode pipeline (decided):
- Raise the session preset
.hd1280x720→.hd1920x1080(helps thin 1D and small QR). - Add
.code128to the AVFoundationmetadataObjectTypes— the default decode path. - Add an adaptive inverted fallback for black/transparent-tape labels: an
AVCaptureVideoDataOutput+ Vision (VNDetectBarcodesRequest, Code128) over aCIColorInvert+ contrast-boosted frame, enabled only after ~1–2 s of no decode, and throttled (~7/s). Off by default so normal scans pay no Vision cost. (Branchtask/scanner-code128-probeis the reference implementation of both paths.)
- Raise the session preset
- Parse
<prefix>-<path>: split on the first-→keyMaterial,path. Build the canonical resolve request (path + prefix). This is a new parse branch distinct from the URL parser feedingResolveIdentifierOperation. - Set transport
t=con the resulting scan/resolve call. - The downstream resolve/scan-logging flow is otherwise unchanged.
Plings-Web (dev)
- Documentation updates (see below) + this spec.
- Keygen-menu UI: surface the prefix-length choice / 1D output if the menu drives keygen.
Documentation updates (ship with the code)
| Document | Update |
|---|---|
docs/core-systems/s-plings-io/url-structure.md |
New section: the 1D Code128 payload <prefix>-<path>, that it is not a URL, base58 path, and how it normalizes to the same i/p semantics. |
docs/core-systems/s-plings-io/README.md |
t-table + transport-agnostic note: mention the 1D barcode and t=c. |
docs/core-systems/plings-identifier-overview.md |
New “Physical Identifier Type”: Code128 for long round objects, with the dual-print enrollment note. |
docs/use-cases/verification-trust-model.md |
Add the two operating modes + dual-print enrollment. |
ROADMAP entries
- Plings-API/ROADMAP.md — Next/Backlog: keygen emits 1D
<prefix>-<path>payload (+ optional Code128 image); new path-keyed resolver with min-prefix floor;t=canalytics. - Plings-iOS/ROADMAP.md — Next/Backlog: ScannerSurface reads Code128 + parses
<prefix>-<path>;t=c. - Plings-Web/ROADMAP.md — Next/Backlog: identifier docs updates + this spec; keygen-menu prefix-length option.
Safe merge order
Backend first, UI last, so the deployed app never calls a route that is not live yet:
- Plings-API — path-keyed resolver + keygen +
t=c. - Plings-iOS — Code128 scanning (depends on the resolver accepting path/prefix).
- Plings-Web — docs + spec (can lag without breaking functionality).
Open decisions (defer to implementation, none block this spec)
- Final backend floor value (proposed 8 min / 12 recommended).
- Whether keygen renders the Code128 image or only emits the payload string for an external label tool.
- Exact resolver API shape (extend
resolveIdentifier, or a sibling field) — an implementation-plan decision. - Confirm the path index/uniqueness already exists before assuming no migration.
- Capture resolution — DECIDED: 1080p (
.hd1920x1080); 720p excluded, 4K optional boost. See findings. - Inverted-contrast (black-tape) support — DECIDED: both polarities supported via adaptive escalation (metadata default → Vision-invert fallback after ~1–2 s). See findings.
- Label/tape height (6 mm vs 12 mm) — both read at 1080p (6 mm white-on-black and 12 mm black-on-white each worked); 6 mm confirmed viable for compactness. A clean 6-vs-12 mm ergonomics comparison is still un-isolated (height + polarity varied together) — optional follow-up, not blocking.
- Module width — minimum width read at ≥1080p; choose a comfortable margin above minimum in the keygen/print spec rather than running at the edge.