GitHub - thomastheyoung/nexid: Fast, time-sortable unique identifiers for the JavaScript ecosystem. Port of the original Go library github/rs/xid. (original) (raw)

NeXID - Fast, lexicographically sortable unique IDs

npm version TypeScript License: MIT

A TypeScript implementation of globally unique identifiers that are lexicographically sortable, following the XID specification, originally inspired by Mongo Object ID algorithm. NeXID provides a high-performance solution for generating and working with XIDs across JavaScript runtimes.

Tip

Features

Installation

npm install nexid yarn add nexid pnpm add nexid

Requires Node.js 20 or >= 22.

Quick start

import NeXID from 'nexid';

// Universal entry point — async (auto-detects environment) const nexid = await NeXID.init();

// Generate an XID object const id = nexid.newId(); id.toString(); // "cv37img5tppgl4002kb0"

// High-throughput string-only generation (~30% faster) const idString = nexid.fastId();

You can also resolve the environment separately, then init synchronously:

import { resolveEnvironment } from 'nexid';

const { init } = await resolveEnvironment(); const nexid = init();

Platform-specific entry points skip detection entirely and are synchronous:

import NeXID from 'nexid/deno'; // Deno import NeXID from 'nexid/node'; // Node.js import NeXID from 'nexid/web'; // Browser

// No await needed — init is synchronous const nexid = NeXID.init();

API

init(options?)

Creates an XID generator. Returns Generator.API.

const nexid = NeXID.init({ machineId: 'my-service-01', // Override auto-detected machine ID processId: 42, // Override auto-detected process ID (0–65535) randomBytes: myCSPRNG, // Custom (size: number) => Uint8Array allowInsecure: false, // Allow non-cryptographic fallbacks (default: false) filterOffensiveWords: true, // Reject IDs containing offensive words offensiveWords: ['myterm'], // Additional words to block });

Option Type Default Description
machineId string Auto-detected Custom machine identifier string (hashed before use)
processId number Auto-detected Custom process ID, masked to 16-bit
randomBytes (size: number) => Uint8Array Auto-detected Custom CSPRNG implementation
allowInsecure boolean false When false, throws if CSPRNG cannot be resolved
filterOffensiveWords boolean false Reject IDs containing offensive word substrings
offensiveWords string[] [] Additional words to block alongside the built-in list
maxFilterAttempts number 10 Max attempts to find a clean ID when filtering is enabled

Generator API

Returned by init().

nexid.newId(); // Generate XID object (current time) nexid.newId(new Date()); // Generate XID object with custom timestamp nexid.fastId(); // Generate XID string directly (faster)

nexid.machineId; // Hashed machine ID bytes (hex string) nexid.processId; // Process ID used by this instance nexid.degraded; // true if using insecure fallbacks

XID class

Immutable value object representing a 12-byte globally unique identifier.

Factory methods

import { XID } from 'nexid';

XID.fromBytes(bytes); // Create from 12-byte Uint8Array XID.fromString(str); // Parse from 20-character string XID.nilID(); // Create a nil (all-zero) ID

Instance properties

id.bytes; // Readonly XIDBytes (12-byte Uint8Array) id.time; // Date extracted from timestamp component id.machineId; // Uint8Array (3-byte machine ID, copy-on-read) id.processId; // number (16-bit process ID) id.counter; // number (24-bit counter value)

Instance methods

id.toString(); // 20-character base32-hex string id.toJSON(); // Same as toString() — JSON.stringify friendly id.isNil(); // true if all bytes are zero id.equals(other); // true if identical bytes id.compare(other); // -1, 0, or 1 (lexicographic)

Helper functions

Standalone utility functions for working with XIDs. These are used internally by the XID class and available as a deep import:

// Internal module — not part of the public package exports import { helpers } from 'nexid/core/helpers';

helpers.compare(a, b); // Lexicographic XID comparison helpers.equals(a, b); // XID equality check helpers.isNil(id); // Check if XID is nil helpers.sortIds(ids); // Sort XID array chronologically helpers.compareBytes(a, b); // Lexicographic byte array comparison

Prefer the equivalent XID instance methods (id.compare(), id.equals(), id.isNil()) for typical usage.

Offensive word filter

Opt-in filtering rejects generated IDs that contain offensive substrings, retrying with a new counter value.

import NeXID, { BLOCKED_WORDS } from 'nexid/node';

// Use the built-in blocklist (57 curated offensive words) const nexid = NeXID.init({ filterOffensiveWords: true });

// Extend the built-in blocklist with custom terms const nexid2 = NeXID.init({ filterOffensiveWords: true, offensiveWords: ['mycompany', 'badterm'], });

BLOCKED_WORDS is exported from all entry points for inspection.

Exported types

import type { XIDBytes, XIDGenerator, XIDString } from 'nexid';

// XIDBytes -- branded 12-byte Uint8Array // XIDString -- branded 20-character string // XIDGenerator -- alias for Generator.API

Architecture

XID structure

Each XID consists of 12 bytes (96 bits), encoded as 20 characters:

  ┌───────────────────────────────────────────────────────────────────────────┐
  │                         Binary structure (12 bytes)                       │
  ├────────────────────────┬──────────────────┬────────────┬──────────────────┤
  │        Timestamp       │    Machine ID    │ Process ID │      Counter     │
  │        (4 bytes)       │     (3 bytes)    │  (2 bytes) │     (3 bytes)    │
  └────────────────────────┴──────────────────┴────────────┴──────────────────┘

Timestamp (4 bytes)

32-bit unsigned integer representing seconds since Unix epoch. Positioned first in the byte sequence to enable lexicographical sorting by time.

Tradeoff: second-level precision instead of milliseconds allows for 136 years of timestamp space within 4 bytes.

Machine ID (3 bytes)

24-bit machine identifier derived from platform-specific sources, then hashed:

Values remain stable across restarts on the same machine.

Process ID (2 bytes)

16-bit process identifier:

Counter (3 bytes)

24-bit atomic counter for sub-second uniqueness:

Encoding

Base32-hex (0-9, a-v) encoding yields 20-character strings:

Runtime adaptability

The implementation detects its environment and applies appropriate strategies:

Detected runtimes: Node.js, Browser, Web Worker, Service Worker, Deno, Bun, React Native, Electron (main + renderer), Edge Runtime.

System impact

Database operations

Lexicographical sortability enables database optimizations:

Example range query:

-- Retrieving time-ordered data without timestamp columns SELECT * FROM events WHERE id >= 'cv37ijlxxxxxxxxxxxxxxx' -- Start timestamp AND id <= 'cv37mogxxxxxxxxxxxxxxx' -- End timestamp

Distributed systems

Performance

NeXID delivers high performance on par with or exceeding Node's native randomUUID:

Implementation IDs/Second Time sortable Collision resistance URL-safe Coordination-free Compact
hyperid 53,243,635
NeXID.fastId() 9,910,237
node randomUUID 8,933,319
uuid v4 8,734,995
nanoid 6,438,064
uuid v7 3,174,575
uuid v1 2,950,065
ksuid 66,934
ulid 48,760
cuid2 6,611

Benchmarks on Node.js v22 on Apple Silicon. Results may vary by environment.

Note on speed and security

For password hashing, slowness is intentional: attackers must brute-force a small input space (human-chosen passwords), so making each attempt expensive is the defense (that's why bcrypt/argon2 exist).

For unique IDs, security comes from entropy (randomness). If an ID has 128 bits of cryptographic randomness:

Note on SubtleCrypto() vs. MurmurHash3-32

The machine ID hash compresses an identifier like a hostname or browser fingerprint into 3 bytes with uniform distribution. With only 24 bits of output (16.7M possible values), the cryptographic guarantees of SHA-256 are lost to truncation, and the input itself is not a secret that needs protecting. MurmurHash3-32 achieves near-ideal avalanche properties, meaning small input changes spread evenly across the output space, which is exactly what matters for minimizing collisions in this 3-byte component. It also runs synchronously, which allowed us to remove the async initialization step that SubtleCrypto.digest() required from every consumer of the library.

Comparison with alternative solutions

Different identifier systems offer distinct advantages:

System Strengths Best for
NeXID Time-ordered (sec), URL-safe, distributed Distributed systems needing time-ordered IDs
UUID v1 Time-based (100ns), uses MAC address Systems requiring ns precision with hardware ties
UUID v4 Pure randomness, standardized, widely adopted Systems prioritizing collision resistance
UUID v7 Time-ordered (ms), index locality, sortable Systems prioritizing time-based sorting
ULID Time-ordered (ms), URL-safe (Base32), monotonic Apps needing sortable IDs with ms precision
nanoid Compact, URL-safe, high performance URL shorteners, high-volume generation
KSUID Time-ordered (sec), URL-safe (Base62), entropy Systems needing sortable IDs with sec precision
cuid2 Collision-resistant, horizontal scaling, secure Security-focused apps needing unpredictable IDs
Snowflake Time-ordered (ms), includes worker/DC IDs Large-scale coordinated distributed infrastructure

UUID v4 remains ideal for pure randomness, nanoid excels when string size is critical, cuid2 prioritizes security over performance, and Snowflake IDs work well for controlled infrastructure.

Real-world applications

CLI

NeXID ships a CLI for quick ID generation:

npx nexid # generate a single XID

Development

npm install npm test # runs vitest npm run build # compile library npm run bundle # build standalone bundles (required before benchmark) npm run benchmark

Credits

Good reads

License

MIT License