Create identifiers

Give every order, user or event a unique ID that sorts by creation time, or one that reveals nothing at all.

A UUID is a 36-character name for a record that any machine can make on its own, without asking a database for the next number. A v4 carries 122 random bits and a v7 74 next to its timestamp, so a collision is not a practical concern. A v7 starts with the millisecond it was made, so sorting the strings sorts the records by age, and your database indexes stay compact. A v4 is random from end to end. Use v7 for primary keys, and v4 where the creation time is nobody's business.

#Before you start

  • It runs on every runtime listed in Installation, and every call is synchronous.
  • An identifier names a record; it doesn't grant access. A v7 tells anyone who reads it when the record was made, and both versions end up in logs, URLs and analytics. For a link that unlocks something, generate a token with secureGenerate instead.
  • uuidv7() orders IDs by millisecond only. Two from the same millisecond can sort either way; use createUUIDv7Generator() when every ID must sort after the previous one.
  • Ordering holds within one process. Two servers each keep their own order. Every generated ID still carries 62 random bits, so a collision between them is not a practical concern.

#Mint and read IDs

This module makes IDs for orders and users, and reads an order's creation time back out of its ID.

record-ids.ts
/** One generator per process: its ids sort in the order they were made. */
const orderIds = createUUIDv7Generator();

export function newOrderId(): string {
  return orderIds.next();
}

/** User ids appear in profile URLs; a v4 keeps the sign-up time out of them. */
export function newUserId(): string {
  return uuidv4();
}

/** When an order was created, read from its id; `null` for anything not a v7. */
export function orderCreatedAt(id: unknown): Date | null {
  return isUUIDv7(id) ? new Date(uuidv7Timestamp(id)) : null;
}
  • One generator for the process: orderIds.next() never hands out an ID that sorts before the last one, even through bursts of thousands a millisecond or the system clock stepping back.
  • v4 for public user IDs: anyone can read a v7's creation time, so a profile URL with one in it tells the world when the account signed up.
  • Guard before reading: uuidv7Timestamp throws for anything that is not a UUIDv7. isUUIDv7 checks the format, the version and the variant, so with the guard in front, a junk route parameter becomes null instead of an error.

#Embed a time of your choice

When you import old records, you want their IDs to carry the time each one happened. Pass a Date or Unix milliseconds to stamp that time instead of now:

uuidv7(new Date("2020-01-01T00:00:00Z")); // "016f5e66-e800-7…"
orderIds.next(importedOrder.createdAt); // sorts among orders of that date

The generator keeps counting from the real clock behind the scenes, so its IDs stay unique. Backfilled ones sort by the time they carry, not by the moment you made them.

#When it fails

Only uuidv7Timestamp and a bad timestamp throw, and the guards never do. Errors shows how to turn a code into a response.

SituationWhat happens
uuidv7Timestamp gets a string that is not a UUIDv7, such as a v4Throws MALFORMED. Guard with isUUIDv7 when the value came from a request.
uuidv7Timestamp gets a value that is not a stringThrows INVALID_TYPE.
isUUIDv4 or isUUIDv7 gets anything at allReturns true or false, never throws. Uppercase hex passes.
A timestamp is negative, an invalid Date, or later than the year 10,889Throws OUT_OF_RANGE.
A timestamp is a string or anything else not a Date or numberThrows INVALID_TYPE. Parse it into a Date first.
A generator's next throwsThe generator's state is unchanged; the next call behaves as if the failed one never happened.

#Which function to use

Whatever you need, one call covers it:

You needUse
A primary key, made anywhere in your codeuuidv7()
A primary key, every ID sorting after the one beforecreateUUIDv7Generator().next()
An ID that reveals nothing, not even when it was madeuuidv4()
A UUID without naming a version at the call sitesecureUUID(), the same function as uuidv7
The creation time of a v7uuidv7Timestamp(id), in Unix milliseconds

#Going further

A few details for when precision, backfills or other runtimes matter:

  • Precision: the time inside a v7 is in whole milliseconds. Fractions of a number are rounded down, so uuidv7Timestamp can't give you anything finer.
  • The same timestamp twice: two next(date) calls with the same date sort in call order only when both land in the same wall-clock millisecond. Either way, they stay unique.
  • Not crypto.randomUUID(): uuidv4 draws from the same random source but sets the version and variant bits itself, so it behaves the same in every runtime.
  • Standard: both versions follow RFC 9562. The generator uses its counter method, which spends 12 of the 74 random bits on keeping IDs in order.