NewSortable IDs are here
prefID
Menu

sortableId()

Time-ordered, prefixed IDs — a fixed-width timestamp plus a random tail, so IDs sort chronologically as plain strings. The idea behind ULID and UUIDv7, with prefID's prefix and type.

Usage

sortableId works just like id() — pass a prefix, get back a typed `${prefix}_${string}`. The difference is the body: it starts with an encoded timestamp, so newer IDs always sort after older ones.

import { sortableId, getTimestamp } from "prefid";

sortableId("evt"); // => "evt_00VQ5a1k0lBjgjfx6pwYy6WkY"
sortableId("evt"); // => "evt_00VQ5a1mgkWGzAvv93g1bC3yR"  ← later, sorts after

// Read back the millisecond timestamp baked into the id:
getTimestamp("evt_00VQ5a1k0lBjgjfx6pwYy6WkY"); // => 1721600000000
const { sortableId, getTimestamp } = require("prefid");

sortableId("evt"); // => "evt_00VQ5a1k0lBjgjfx6pwYy6WkY"
sortableId("evt"); // => "evt_00VQ5a1mgkWGzAvv93g1bC3yR"  ← later, sorts after

// Read back the millisecond timestamp baked into the id:
getTimestamp("evt_00VQ5a1k0lBjgjfx6pwYy6WkY"); // => 1721600000000

Why sortable IDs?

  • Database-friendly — time-ordered keys keep inserts local in B-tree indexes, avoiding the page fragmentation that random UUIDs cause.
  • Natural cursors — “everything after this ID” is a chronological range query, no separate timestamp column required.
  • Coordination-free — every process generates ordered IDs on its own; there is no central sequence or lock.

Sorting

Because the timestamp is fixed-width and encoded with an ascending alphabet, lexicographic order equals chronological order. No decoding is needed to sort:

ts
const ids = [sortableId("row"), sortableId("row"), sortableId("row")];

// A plain string sort is also a chronological sort — no parsing needed.
[...ids].sort(); // same order they were created in

Monotonic ordering

By default the generator is monotonic: IDs created within the same millisecond — or when the system clock steps backwards — are still strictly increasing. Instead of drawing a fresh random tail, prefID increments the previous one; if that tail is ever exhausted within a millisecond, it spills into the next. Pass monotonic: false for a stateless generator that is only ordered at millisecond granularity.

Reading the timestamp

Three helpers decode the millisecond timestamp baked into a sortable ID. Pass the same alphabet, separator, and timestampSize the ID was generated with (the defaults match sortableId).

  • getTimestamp(id) — returns the millisecond timestamp, or undefined when the value isn't a well-formed sortable ID. Reach for this when an invalid value is an expected, handled case — you branch on the result.
  • getTimestampOrThrow(id) — the strict variant: returns a number and throws a TypeError on a malformed value. Reach for this when a bad ID is a programmer error you want surfaced loudly instead of silently becoming undefined.
  • getDate(id) — a convenience that returns a Date (or undefined) instead of a raw number.
import { getTimestamp, getTimestampOrThrow, getDate } from "prefid";

const id = sortableId("evt");

// Safe: the ms timestamp, or undefined if the value is malformed.
getTimestamp(id);          // => 1721600000000
getTimestamp("not-an-id"); // => undefined

// Strict: same value, but throws a TypeError instead of undefined.
getTimestampOrThrow(id);   // => 1721600000000

// Convenience: decode straight to a Date (or undefined).
getDate(id);               // => Date(2024-07-21T…)
const { getTimestamp, getTimestampOrThrow, getDate } = require("prefid");

const id = sortableId("evt");

// Safe: the ms timestamp, or undefined if the value is malformed.
getTimestamp(id);          // => 1721600000000
getTimestamp("not-an-id"); // => undefined

// Strict: same value, but throws a TypeError instead of undefined.
getTimestampOrThrow(id);   // => 1721600000000

// Convenience: decode straight to a Date (or undefined).
getDate(id);               // => Date(2024-07-21T…)
ts
type DecodeOptions = {
  separator?: string;     // default: "_"
  alphabet?: string;      // default: base62
  timestampSize?: number; // default: 9
};

function getTimestamp(id: string, options?: DecodeOptions): number | undefined;
function getTimestampOrThrow(id: string, options?: DecodeOptions): number;
function getDate(id: string, options?: DecodeOptions): Date | undefined;

Configuring

Use createSortableId() to build a generator with fixed options, then reuse it:

import { createSortableId } from "prefid";

// A generator with your own settings:
const newId = createSortableId({
  randomSize: 20,   // more entropy in the random tail
  separator: "-",   // "evt-…"
});

newId("evt"); // => "evt-00VQ5a1k0lBjgjfx6pwYy6WkYq2mT"
const { createSortableId } = require("prefid");

// A generator with your own settings:
const newId = createSortableId({
  randomSize: 20,   // more entropy in the random tail
  separator: "-",   // "evt-…"
});

newId("evt"); // => "evt-00VQ5a1k0lBjgjfx6pwYy6WkYq2mT"

Signature

ts
function createSortableId(options?: {
  separator?: string;      // default: "_"
  alphabet?: string;       // default: base62 (must be ascending)
  randomSize?: number;     // default: 16
  timestampSize?: number;  // default: fits 2^48-1 ms (9 base62 chars)
  monotonic?: boolean;     // default: true
  now?: () => number;      // default: Date.now
}): <P extends string>(prefix: P) => `${P}_${string}`

// A ready-made generator with the defaults:
const sortableId: <P extends string>(prefix: P) => `${P}_${string}`

Options

OptionTypeDefaultDescription
separatorstring"_"Text between the prefix and the body.
alphabetstringbase62Characters for the timestamp and random tail. Must be in strictly ascending code-point order so sorting works.
randomSizenumber16Random characters after the timestamp (1–4096).
timestampSizenumber9Width of the encoded timestamp. Defaults to the smallest width that holds any time up to the year 10889.
monotonicbooleantrueGuarantee strictly increasing IDs within a process.
now() => numberDate.nowClock source, for testing or custom epochs.

Notes

  • A sortable alphabet must be in strictly ascending code-point order (the default base62 already is) — otherwise a string sort would not match time order, so createSortableId throws a RangeError.
  • The random tail uses the same cryptographic RNG as id(), so IDs are unguessable as well as ordered.
  • isId() and getPrefix() work on sortable IDs unchanged — the format is still prefix_body.
  • getTimestamp, getTimestampOrThrow, and getDate decode the leading timestamp field; they can't verify that a value was actually produced by sortableId (any ID with a long-enough body decodes to some time). Call them on values you already know are sortable — there is deliberately no isSortableId, because the format carries no marker that would make such a check reliable.
  • For case-insensitive, unambiguous ids (ULID-style), pass the exported BASE32_CROCKFORD alphabet — it omits I, L, O, U and is already ascending, so it stays sortable.