Choose an ID from your requirements, not habit
There is no universally best identifier. Describe what the ID must do and get a recommendation with the constraints that could change the answer.
Recommended: CUID2 (default 24 characters)
Best fit for compact, distributed public IDs
CUID2 is compact, lowercase, URL-friendly, and can be generated by disconnected clients or multiple application nodes without coordination. Keep the default length unless you have quantified a different collision budget.
Why this fits
- ✓Your writers do not need a round trip to reserve an ID.
- ✓The lowercase Base36 representation stays readable in routes and logs.
- ✓The default length provides a very large collision space without UUID punctuation.
Trade-offs to accept
- △CUID2 is not an IETF identifier standard and may lack a native database type.
- △It is intentionally not time-sortable; query an explicit created_at column.
Suggested database shape
id VARCHAR(24) PRIMARY KEY
created_at TIMESTAMPTZ NOT NULL DEFAULT now()Generation starting point
import { createId } from "@paralleldrive/cuid2";
createId();Why the closest alternatives were rejected
Database bigint
Offline and distributed writers cannot reserve values from one coordinator.
UUIDv4
It is more standardized, but its canonical text form is longer and punctuated.
Before shipping
- 1Generate with the maintained @paralleldrive/cuid2 package.
- 2Keep 24 characters unless a documented collision analysis supports another length.
- 3Validate format at trust boundaries and enforce a unique or primary-key constraint.
- 4Treat IDs as public and enforce authorization separately.
How the recommendation is made
The advisor starts with the system boundary. A single primary database can coordinate a compact numeric key; offline clients and independent nodes cannot. It then separates public record identifiers from bearer secrets and checks whether ordering or standards compatibility is a hard requirement.
The result is deliberately conservative: it keeps timestamps in an explicit created_at column, keeps authorization separate from identifier opacity, and does not shorten random IDs without a collision analysis.
- 1. Separate credentials first. A bearer secret exits the identifier decision tree and needs random-token lifecycle controls.
- 2. Honor ordering next. If time ordering is required, select an explicitly ordered format rather than decoding incidental fields.
- 3. Check coordination. A central database can allocate a bigint; offline or distributed writers need independent generation.
- 4. Break the remaining tie. Prefer UUIDv4 for standard/native-type support or CUID2 for compact lowercase output.
Worked decisions
Multi-region order records
Inputs: public records, distributed writers, compact URLs. Result: CUID2 at the package default length, with a unique constraint and separatecreated_at. A bigint is rejected because writers cannot depend on one allocator.
Drafts created without a network
Inputs: public records, offline generation, standards compatibility. Result: UUIDv4. CUID2 remains viable when compact lowercase routes outweigh UUID interoperability; the product team must make that tie-break explicit.
Password-reset link
Inputs: login/reset/API token. Result: a dedicated 256-bit random token, stored as a hash with expiry and single-use semantics. CUID2 and UUID are rejected because an ID format does not supply credential lifecycle controls.
What the advisor does not decide
It does not benchmark your database, assess a library implementation, choose a sharding key, or prove a collision budget. Workload shape, database engine, retention, regulatory requirements, and failure recovery can overturn a generic recommendation. Treat the result as a documented starting hypothesis and test it against the real system.