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 a path, 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 in p=. 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; 300 decimal is 6B in base58.
  • - — delimiter. - is not in the base58 alphabet and is not the path separator ., so the split is unambiguous. Splitting on the first (only) - yields prefix (left) and path (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 }. keyMaterial is 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:

  1. 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.
  2. 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:

  1. Keygen prints both the standard 2D QR (full URL, i + p) and the 1D Code128 (<prefix>-<path>) for the same tag.
  2. Scan the 2D QR → the full instance key + path register the object → the object becomes known.
  3. 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. path alone is enumerable and was rejected as a payload. With path + 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 via AVCaptureVideoDataOutput. 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.md when the format ships.
  • Engine + adaptive escalation — LOCKED. AVFoundation AVCaptureMetadataOutput is the default decode path (light; handles dark-on-light Code128 + QR; reads instantly). Because black-tape (inverted) is a supported requirement, the Vision/VNDetectBarcodes invert 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 payload instance_key[:N] + "-" + base58_path as a new barcode_payload column in the generated CSVs, alongside the existing shortcode / decimal_path / base58_path / url columns (the QR/print workflow stays unchanged; shortcode is already instance_key[:4], i.e. the prefix at N=4). Prefix length N is a parameter (≥ backend floor; default 12). For the validation phase it must be trivial to emit the test lengths — either a CLI/prompt option for N, or emit prefix8 / prefix12 / prefix16 columns so a sheet can be printed at each. Optionally render a Code128 image. Per the keygen-output note in CLAUDE.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 the ObjectInstance by path; verify scanned keyMaterial against 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=c in 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 .code128 to the AVFoundation metadataObjectTypes — the default decode path.
    • Add an adaptive inverted fallback for black/transparent-tape labels: an AVCaptureVideoDataOutput + Vision (VNDetectBarcodesRequest, Code128) over a CIColorInvert + 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. (Branch task/scanner-code128-probe is the reference implementation of both paths.)
  • 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 feeding ResolveIdentifierOperation.
  • Set transport t=c on 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=c analytics.
  • 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:

  1. Plings-API — path-keyed resolver + keygen + t=c.
  2. Plings-iOS — Code128 scanning (depends on the resolver accepting path/prefix).
  3. 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 resolutionDECIDED: 1080p (.hd1920x1080); 720p excluded, 4K optional boost. See findings.
  • Inverted-contrast (black-tape) supportDECIDED: 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.