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"); // => 1721600000000const { 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"); // => 1721600000000Why 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:
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 inMonotonic 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
getTimestamp(id, options?) decodes the millisecond timestamp embedded in a sortable ID, or returns undefined if the value is not a well-formed sortable ID. Pass the same alphabet, separator, and timestampSize the ID was generated with (the defaults match sortableId).
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
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
| Option | Type | Default | Description |
|---|---|---|---|
| separator | string | "_" | Text between the prefix and the body. |
| alphabet | string | base62 | Characters for the timestamp and random tail. Must be in strictly ascending code-point order so sorting works. |
| randomSize | number | 16 | Random characters after the timestamp (1–4096). |
| timestampSize | number | 9 | Width of the encoded timestamp. Defaults to the smallest width that holds any time up to the year 10889. |
| monotonic | boolean | true | Guarantee strictly increasing IDs within a process. |
| now | () => number | Date.now | Clock source, for testing or custom epochs. |
Notes
- A sortable
alphabetmust be in strictly ascending code-point order (the default base62 already is) — otherwise a string sort would not match time order, socreateSortableIdthrows aRangeError. - The random tail uses the same cryptographic RNG as
id(), so IDs are unguessable as well as ordered. isId()andgetPrefix()work on sortable IDs unchanged — the format is stillprefix_body.- For case-insensitive, unambiguous ids (ULID-style), pass the exported
BASE32_CROCKFORDalphabet — it omitsI,L,O,Uand is already ascending, so it stays sortable.