prefixed nanoid identifiers
why
a bare token in a log says nothing about what record it addresses, and an ad hoc format drifts per caller until logs cannot be correlated or regex-searched. one maintained generator with a prefix makes every durable id self-describing and uniform, so copying an id through a url, cli, or editor round-trips unchanged, and the input schema validates one exact grammar instead of per-call normalization each consumer must guess.
rule
every generated durable record identifier has the exact form <prefix>_<token>:
prefix— the shortest exact domain/fact word for that record kind.token— from one maintained NanoID generator, lower-case alphanumeric alphabet. hyphens forbidden.
the id reads as one technical word in URLs, logs, SQL output, error reports, selection gestures, and regex search. no consumer derives, normalizes, splits, case-converts, or reconstructs it.
format contract
| part | contract |
|---|---|
| prefix | canonical lower snake-case domain/fact vocabulary, e.g. process_step, billing_charge |
| separator | exactly one underscore between prefix and token |
| token alphabet | 0123456789abcdefghijklmnopqrstuvwxyz; no hyphen, whitespace, punctuation, mixed case |
| token length | one reviewed fixed length for collision risk; same length for all generated IDs unless a boundary requires otherwise |
| generator | one named, tested ID builder using NanoID/custom alphabet; raw callers never assemble the format |
the underscore is structural yet editor-friendly: the full ID stays one
word-like token for selection and search. the token avoids -, which splits
double-click selection and breaks log/CLI parsing.
identity decision
the prefix describes kind, not storage location, provider, screen, or display copy. choose it from the same glossary vocabulary as table names; use the shortest exact compound only when one word would collide. a prefix is not an authorization scope and not a substitute for a foreign key or scoped unique constraint.
| candidate | decision | reason |
|---|---|---|
process_step_<token> | allowed | the durable fact needs both exact domain words |
step_<token> | allowed only when step is unique in project scope | shorter is correct only when it stays exact |
provider_step_<token> | reject | provider implementation is not the durable fact |
display-label-<token> | reject | labels are authored copy; hyphens break the format contract |
| bare NanoID | reject for durable internal identifiers | logs and diagnostics lose record kind |
ownership and validation
- the id builder owns token generation and format construction.
- a schema/parser validates a received id against the declared prefix and lower-case alphanumeric token grammar; after validation, pass the complete opaque id to server services, storage, and protected logs.
- a caller needing record kind knows it from its typed boundary; it must not parse the prefix back out.
- public routes and UI resolve a separate public key per identifiers and keys.
verification
- generate representative IDs for every record kind; confirm exact prefix, one separator, lower-case token alphabet, reviewed length.
- reject mixed-case, hyphenated, whitespace, wrong-prefix, and malformed IDs at the input schema boundary with a correction diagnostic.
- search source for ad hoc NanoID calls, string assembly,
split,replace, or case conversion around IDs; only the named builder/schema should remain. - copy an ID through the target URL, log, CLI, and editor; it stays one selectable/searchable token without escaping or rewriting.
- keep PK/FK columns and indexes exact; no parsed prefix/token duplicate unless a distinct approved query requires it.
good and bad
bad
const workflowRunId = `workflow-run-${nanoid()}`.toLowerCase();
hyphenated and assembled ad hoc: the next caller writes
workflowRun_${nanoid()}, a third uses uppercase — three incompatible spellings
of one id kind in the log stream. double-click selection breaks at the hyphen,
grep cannot correlate across systems, and no input schema can validate a grammar
never declared; every new caller adds another variant.
good
const workflowRunId = createWorkflowRunId();// workflow_run_k1m9x2d8q4
the named generator owns one lower-case, underscore-separated, selectable format: every id in logs, errors, urls, and cli transcripts is self-describing and structurally identical, correlation is a single grep, selection survives editor and terminal round-trips, the schema validates one declared grammar, and a new record kind is one reviewed generator call — the contract holds by construction.