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
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
- For advanced usage and real-world use cases, see API reference and Use cases.
- To see NeXID in action in a web environment, visit the library's github page.
Features
- Lexicographically sortable: natural sorting in databases, binary searches, and indexes
- Time-ordered: built-in chronological ordering (timestamp is the first component)
- Compact: 20 characters vs 36 for UUIDs (44% smaller)
- URL-safe: alphanumeric only (0-9 and a-v), no special characters to escape
- Universal: works in Node.js, browsers, Deno, and edge runtimes
- Fast: generates 10+ million IDs per second
- Secure: uses platform-specific cryptographic random number generation
- Adaptive: runtime environment detection with appropriate optimizations
- Type-safe: branded types for compile-time safety
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:
- Node.js/Deno: OS host UUID (
/etc/machine-idon Linux,IOPlatformUUIDon macOS, registryMachineGuidon Windows), hashed with SHA-256 - Browsers: localStorage-persisted random UUID via
crypto.randomUUID(), with deterministic fingerprint fallback (navigator, screen, timezone), hashed with MurmurHash3 - Edge: Adaptive generation based on available platform features
Values remain stable across restarts on the same machine.
Process ID (2 bytes)
16-bit process identifier:
- Node.js:
process.pidmasked to 16-bit - Deno:
Deno.pidmasked to 16-bit - Browsers: Cryptographic random 16-bit value via
crypto.getRandomValues()
Counter (3 bytes)
24-bit atomic counter for sub-second uniqueness:
- Thread-safe via
SharedArrayBuffer+Atomics(with WebAssembly andArrayBufferfallbacks) - Re-seeded with a fresh 24-bit CSPRNG value on each new second
- 16,777,216 unique IDs per second per process
- Automatic wrapping with 24-bit mask
Encoding
Base32-hex (0-9, a-v) encoding yields 20-character strings:
- Direct byte-to-character mapping with no padding
- Lexicographically preserves binary order
- Implemented with lookup tables for performance
Runtime adaptability
The implementation detects its environment and applies appropriate strategies:
- Server (Node.js, Deno): hardware identifiers, process IDs, native cryptography, SHA-256
- Browser: localStorage persistence, fingerprinting fallback, Web Crypto API, MurmurHash3
- Edge/Serverless: adapts to constrained environments with fallback mechanisms
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:
- Index efficiency: B-tree indices perform optimally with ordered keys
- Range queries: time-based queries function as simple index scans
- Storage: 44% size reduction translates to storage savings at scale
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
- No coordination: no central ID service required
- Horizontal scaling: services generate IDs independently without conflicts
- Failure isolation: no dependency on external services
- Global uniqueness: maintains uniqueness across geographic distribution
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:
- An attacker doesn't need your generator, they can enumerate candidates independently at any speed they want
- The search space is 2^128 regardless of how fast you can generate IDs
- Collision resistance is a function of bit-length (birthday bound), not generation throughput
- There's no "entropy-hiding" to break, the output is the random value
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
- High-scale e-commerce: time-ordering with independent generation enables tracking without coordination.
- Multi-region data synchronization: for content replication with eventual consistency, machine identifiers and timestamps simplify conflict resolution.
- Real-time analytics: high-performance generation with chronological sorting eliminates separate sequencing.
- Distributed file systems: lexicographical sorting optimizes indexes while machine IDs enable sharding.
- Progressive Web Apps: client-side generation works offline while maintaining global uniqueness.
- Time-series data management: XIDs function as both identifiers and time indices, reducing schema complexity.
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
- Original XID specification by Olivier Poitrey
- Inspired by MongoDB's ObjectID and Twitter's Snowflake
Good reads
- Great and comprehensive overview of unique ID generation systems from bool.dev blog