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

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

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.
  • 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.