ADR-0032: Viewable ID scheme (TR-XXXXXXX)
Status: accepted Date: 2026-04-21 Deciders: команда проекта Accepted-by: Phase 1-a ingestion loop landed; canonical_products.viewable_id UNIQUE populated by CanonicalProvisioner + ViewableIDIssuer in production path (see commits b5933de, a672448, 2ac96c0).
Контекст
Публичный API (/v1/products) возвращает клиенту идентификатор продукта. Внутренний canonical_products.id — UUIDv7 (ADR-0004). UUID плохо копируется, плохо диктуется голосом, плохо печатается на бумаге (путаница 0/O, 1/I/L). Для B2B-контекста (менеджер по телефону, склад с распечатанной спецификой) нужен короткий, произносимый, устойчивый к опечаткам ID помимо UUID.
Решение
Ввести колонку canonical_products.viewable_id TEXT UNIQUE с форматом:
TR-XXXXXXX
TR-— фиксированный ASCII-префикс (от “Tracium”).XXXXXXX— 7 символов Crockford base32 (алфавит0-9A-HJKMNP-TV-Z, безI/L/O/U).- Регекс валидации:
^TR-[0-9A-HJKMNP-TV-Z]{7}$.
Генерация: crypto/rand → 5 raw bytes → base32 encode (custom alphabet, no padding) → first 7 chars → prefix. При DB-коллизии retry до 3 раз; исчерпание → ErrCollisionsExhausted (deg. UniqueChecker signal).
Пространство: 32⁷ ≈ 34·10⁹. При 10⁵ записей p(коллизии на запись) ≈ 1.5·10⁻⁵; 3 retry снижают до ~10⁻¹⁵.
Стабильность: viewable_id неизменяем после create. При canonical merge в Phase 2 оба ID сохраняются через canonical_aliases (alias_id → survivor_id); запрос по любому ID → survivor.
Последствия
Плюсы
- Короткий copy-paste friendly ID для клиентов.
- Crockford избавляет от глазных опечаток (0/O, 1/I/L).
- Неперебираемый (crypto/rand) — защищает от scraping.
- Реализация изолирована в
platform/viewableid; потребители зовут черезUniqueCheckerport без прямой связи с DB из generator’а.
Минусы
- Дополнительная колонка + UNIQUE индекс на
canonical_products(~8 байт + btree overhead). - Retry на DB-коллизии добавляет round-trip при редких (<10⁻⁵) collision events.
Нейтральные
- Для URL-friendly, но не SEO-friendly (нет слов). URL типа
/products/TR-8F2K3P2— нормально для B2B.
Рассмотренные альтернативы
A. Использовать UUID напрямую в URL
Отклонено: ADR-0004 делает canonical_products.id = UUIDv7, 36 символов, плохо диктуется.
B. Sequence + base36
Отклонено: TR-12345 легко перебрать — scraper пройдёт по каталогу.
C. HMAC от sequence
Отклонено: добавляет secret-management, rotation complexity; детерминированная обратимость не даёт продуктовой ценности.
D. nanoid / cuid
Отклонено: включает lowercase + ambiguous oO0, требует pre-process перед голосом/печатью.
Ссылки
- viewableid — импл.
- ADR-0004 postgres as primary store — UUIDv7 остаётся internal id.
- Phase 1-a design spec.
- Crockford base32 spec.
Outcomes (Phase 1-a)
platform/viewableidexports Generator withcrypto/rand+ retry 3× — coverage 92.6%, ErrCollisionsExhausted sentinel.canonical_products.viewable_id TEXT UNIQUE(migration 0009) + GIN-free plain btree index.canonical_aliases(alias_id PK → survivor_id)ready for Phase 2 merge flow; iter-1 untouched.- Public
GET /v1/products/{id_or_viewable}handler reserves the route but returns 404 in iter-1 — real lookup Phase 2.