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

partcontract
prefixcanonical lower snake-case domain/fact vocabulary, e.g. process_step, billing_charge
separatorexactly one underscore between prefix and token
token alphabet0123456789abcdefghijklmnopqrstuvwxyz; no hyphen, whitespace, punctuation, mixed case
token lengthone reviewed fixed length for collision risk; same length for all generated IDs unless a boundary requires otherwise
generatorone 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.

candidatedecisionreason
process_step_<token>allowedthe durable fact needs both exact domain words
step_<token>allowed only when step is unique in project scopeshorter is correct only when it stays exact
provider_step_<token>rejectprovider implementation is not the durable fact
display-label-<token>rejectlabels are authored copy; hyphens break the format contract
bare NanoIDreject for durable internal identifierslogs 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

  1. generate representative IDs for every record kind; confirm exact prefix, one separator, lower-case token alphabet, reviewed length.
  2. reject mixed-case, hyphenated, whitespace, wrong-prefix, and malformed IDs at the input schema boundary with a correction diagnostic.
  3. search source for ad hoc NanoID calls, string assembly, split, replace, or case conversion around IDs; only the named builder/schema should remain.
  4. copy an ID through the target URL, log, CLI, and editor; it stays one selectable/searchable token without escaping or rewriting.
  5. 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.

search pages

go to any page