Viewable ID

viewable_id is the public product identifier exposed alongside the internal UUIDv7 canonical_products.id. Format: TR-XXXXXXX where XXXXXXX is 7 characters from the Crockford base32 alphabet (0-9A-HJKMNP-TV-Z, letters I/L/O/U excluded to avoid visual collision).

Regex: ^TR-[0-9A-HJKMNP-TV-Z]{7}$.

Why not just the UUID?

  • Voice dictation: UUIDs mispell over the phone; TR-8F2K3P2 reads cleanly.
  • Paper catalogues: printed IDs often lose 0/O or 1/I/L glyph fidelity. Crockford alphabet dodges both.
  • URL ergonomics: short path component improves bookmarks + QR codes.

Lookup semantics

GET /v1/products/{id_or_viewable} accepts either the UUID or the viewable form. Handler dispatches on viewableid.ValidFormat to the appropriate repository method.

Stability

  • Assigned at canonical create via ViewableIDIssuer.Next.
  • Immutable after create.
  • Survives canonical merges: alias → survivor edge in canonical_aliases preserves both IDs; both resolve to the same surviving product.

Collision handling

platform/viewableid.Generator.Next uses crypto/rand and retries up to 3× on UniqueChecker hit. Space is 32⁷ ≈ 34·10⁹ — collision probability at 10⁵ products is ~1.5·10⁻⁵ per issuance; 3 retries drive it to ~10⁻¹⁵.

ErrCollisionsExhausted surfaces only when the UniqueChecker is degenerate (e.g. always returns “taken”). Production wiring reads uniqueness from canonical_products.viewable_id UNIQUE — if this error fires, investigate the check path before blaming randomness.

Implementation

  • backend/internal/platform/viewableid/ — Generator, UniqueChecker port, ErrCollisionsExhausted.
  • backend/internal/core/catalog/canonical/app.ViewableIDIssuer — consumer-specific wrapper plugging canonical_products.viewable_id UNIQUE as the check source.

References

  • ADR-0032 viewable-id scheme
  • spec docs/superpowers/specs/2026-04-20-ingestion-phase1-a-design.md §5.5