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
secureGenerateinstead. uuidv7()orders IDs by millisecond only. Two from the same millisecond can sort either way; usecreateUUIDv7Generator()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.
/** 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:
uuidv7Timestampthrows for anything that is not a UUIDv7.isUUIDv7checks the format, the version and the variant, so with the guard in front, a junk route parameter becomesnullinstead 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 dateThe 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.
| Situation | What happens |
|---|---|
uuidv7Timestamp gets a string that is not a UUIDv7, such as a v4 | Throws MALFORMED. Guard with isUUIDv7 when the value came from a request. |
uuidv7Timestamp gets a value that is not a string | Throws INVALID_TYPE. |
isUUIDv4 or isUUIDv7 gets anything at all | Returns true or false, never throws. Uppercase hex passes. |
A timestamp is negative, an invalid Date, or later than the year 10,889 | Throws OUT_OF_RANGE. |
A timestamp is a string or anything else not a Date or number | Throws INVALID_TYPE. Parse it into a Date first. |
A generator's next throws | The 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 need | Use |
|---|---|
| A primary key, made anywhere in your code | uuidv7() |
| A primary key, every ID sorting after the one before | createUUIDv7Generator().next() |
| An ID that reveals nothing, not even when it was made | uuidv4() |
| A UUID without naming a version at the call site | secureUUID(), the same function as uuidv7 |
| The creation time of a v7 | uuidv7Timestamp(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
uuidv7Timestampcan't give you anything finer. - The same timestamp twice: two
next(date)calls with the samedatesort in call order only when both land in the same wall-clock millisecond. Either way, they stay unique. - Not
crypto.randomUUID():uuidv4draws 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.