
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
One library. Every ID format. Every runtime. One runtime dependency (@noble/hashes, CUID2 only).
uniku /uˈniːku/ — Maltese for "unique"
import { uuidv7 } from 'uniku/uuid/v7'
const id = uuidv7()
// => "018e5e5c-7c8a-7000-8000-000000000000"
// Time-ordered: IDs sort by creation time
const [first, second, third] = [uuidv7(), uuidv7(), uuidv7()]
console.log(first < second && second < third) // true
| uniku | uuid | typeid-js | nanoid | ulid | cuid2 | ksuid | bson | tsid-ts | |
|---|---|---|---|---|---|---|---|---|---|
| UUID v4 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| UUID v7 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| TypeID | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Nanoid | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| ULID | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| CUID2 | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| KSUID | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
| ObjectID | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
| TSID | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Tree-shakeable | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| ESM-only | ✅ | ✅¹ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| Edge/Workers | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ⚠️ | ⚠️ | ⚠️ |
| Byte ↔ String | ✅ | ✅ | ✅ | - | ⚠️² | - | ✅ | ✅ | ✅ |
Notes:
- Byte ↔ String conversion doesn't make sense for nanoid and cuid2, since they are string-native formats with no canonical binary representation.
- ¹
uuid@13is ESM-only; earlier versions support CommonJS.- ²
ulidonly provides timestamp encoding/decoding, not full binary serialization.
Uses globalThis.crypto (Web Crypto API) — no Node.js-specific APIs.
| Generator | uniku vs npm |
|---|---|
| ULID | 85× faster |
| CUID2 | 8× faster |
| KSUID | 1.5× faster |
| ObjectID | 1.1× faster |
| TSID | 1.7× faster |
| UUID v7 | 1.1× faster |
| Nanoid | ~comparable speed |
| Nanoid (10 chars) | npm is 1.1× faster |
| TypeID | 2.6× faster |
| UUID v4 | npm is 1.1× faster |
| Use Case | Recommended | Why |
|---|---|---|
| Database primary keys | UUID v7 or ULID | Time-ordered for index efficiency |
| API/domain identifiers | TypeID | UUID v7 with readable type prefixes like user_... |
| URL shorteners | Nanoid | Compact, URL-safe characters |
| Prevent enumeration | CUID2 | Non-sequential, secure |
| Maximum compatibility | UUID v4 | Universal standard |
| Distributed systems needing sortable, URL-safe IDs | ULID | Millisecond ordering + 80-bit entropy |
| Very high-volume distributed systems | KSUID | Time-ordered with 128-bit entropy |
MongoDB _id compatibility | ObjectID | Drop-in match for MongoDB's native document ID format |
| Native 64-bit sortable integer ID | TSID | Fits a database BIGINT primary key, no UUID-sized overhead |
UUID v4 — Use when you need maximum compatibility with existing systems.
UUID type)UUID v7 — Use for database primary keys when you need RFC compliance.
UUID typeULID — Use for database primary keys when you want URL-safe IDs.
TypeID — Use when API IDs should expose their entity type.
user_01h2xcejqtf2nbrexx3vqjhp41KSUID — Use when you need highest collision resistance.
ObjectID — Use when you need drop-in compatibility with MongoDB.
_id fields, systems already speaking the ObjectID wire formatTSID — Use when you need a native 64-bit sortable integer ID, e.g. a database BIGINT primary key.
bigint by default (the only uniku generator that isn't string-primary); toString/fromString convert to/from the 13-character canonical stringBIGINT primary keys, high-throughput distributed ID generation without central coordinationNanoid — Use for short, URL-friendly identifiers.
CUID2 — Use when ID unpredictability matters.
toBytes()Formats with binary representations (UUID, ULID, KSUID, ObjectID, TSID) support toBytes() and fromBytes() for efficient storage:
// Store as BINARY(16) instead of VARCHAR(36)
const bytes = uuidv7.toBytes(id) // 16 bytes
db.insert({ id: bytes })
// Retrieve and convert back
const id = uuidv7.fromBytes(row.id)
Benefits of binary storage:
When to use binary:
Exception — tsid: toBytes()/fromBytes() operate on bigint, not string, since bigint is TSID's primary type (see tsid in the API Reference below). Every other generator's toBytes()/fromBytes() operate on its primary string type.
When to keep strings:
| Format | String Length | Binary Size | Savings |
|---|---|---|---|
| UUID v4/v7 | 36 chars | 16 bytes | 56% |
| ULID | 26 chars | 16 bytes | 38% |
| TypeID | prefix + 27 chars | 16 bytes | prefix-dependent |
| KSUID | 27 chars | 20 bytes | 26% |
| ObjectID | 24 chars | 12 bytes | 50% |
| TSID | 13 chars* | 8 bytes | 38% |
| Nanoid | 21 chars | N/A | - |
| CUID2 | 24 chars | N/A | - |
Note: Nanoid and CUID2 don't have binary representations because they're string-native formats with no canonical byte encoding.
* TSID's primary type is
bigint, not a string; the 13-character figure is its canonicaltoString()form, shown here only for a size comparison against the other formats' native strings.
# pnpm (recommended)
pnpm add uniku
# bun
bun add uniku
# npm
npm install uniku
# yarn
yarn add uniku
# deno
deno install npm:uniku
| Import | Minified + gzipped |
|---|---|
uniku/uuid/v4 | ~1.1 KB |
uniku/uuid/v7 | ~1.4 KB |
uniku/ulid | ~1.8 KB |
uniku/typeid | ~1.6 KB |
uniku/cuid/v2 | ~992 B* |
uniku/cuid2 | ~1007 B* |
uniku/nanoid | ~1.1 KB |
uniku/ksuid | ~1.3 KB |
uniku/objectid | ~1.3 KB |
uniku/tsid | ~1.4 KB |
uniku/generators | ~98 B |
@noble/hashes; this table's entry-point size excludes that external dependency.import { uuidv4 } from 'uniku/uuid/v4'
const id = uuidv4()
// => "550e8400-e29b-41d4-a716-446655440000"
// Convert to/from bytes
const bytes = uuidv4.toBytes(id)
const str = uuidv4.fromBytes(bytes)
import { uuidv7 } from 'uniku/uuid/v7'
const id = uuidv7()
// => "018e5e5c-7c8a-7000-8000-000000000000"
// IDs are lexicographically sortable by creation time
const ids = [uuidv7(), uuidv7(), uuidv7()]
ids.sort() // Already in creation order
import { ulid } from 'uniku/ulid'
const id = ulid()
// => "01HW9T2W9W9YJ3JZ1H4P4M2T8Q"
// Convert to/from bytes
const bytes = ulid.toBytes(id)
const str = ulid.fromBytes(bytes)
import { typeid } from 'uniku/typeid'
const id = typeid('user')
// => "user_01h2xcejqtf2nbrexx3vqjhp41"
const uuid = typeid.toUuid(id)
const restored = typeid.fromUuid('user', uuid)
const canonical = typeid('')
// => "01h2xcejqtf2nbrexx3vqjhp41"
import { cuidv2 } from 'uniku/cuid/v2'
const id = cuidv2()
// => "pfh0haxfpzowht3oi213cqos"
// Custom length
const shortId = cuidv2({ length: 10 })
// => "tz4a98xxat"
// Validation (type guard)
if (cuidv2.isValid(maybeId)) {
console.log(maybeId) // TypeScript knows this is a string
}
uniku/cuid/v2is the canonical entry point. The olderuniku/cuid2import still works and re-exports the same generator, but is now deprecated — preferimport { cuidv2 } from 'uniku/cuid/v2'.
import { nanoid } from 'uniku/nanoid'
const id = nanoid()
// => "V1StGXR8_Z5jdHi6B-myT"
// Custom size
const shortId = nanoid(10)
// => "IRFa-VaY2b"
// Custom alphabet and size via options
const hexId = nanoid({ alphabet: '0123456789abcdef', size: 12 })
// => "4f90d13a42bc"
import { ksuid } from 'uniku/ksuid'
const id = ksuid()
// => "2QnJjKLvpSfpZqGiPPxVwWLMy2p"
// Convert to/from bytes
const bytes = ksuid.toBytes(id)
const str = ksuid.fromBytes(bytes)
// Extract timestamp (milliseconds)
const ts = ksuid.timestamp(id)
import { objectid } from 'uniku/objectid'
const id = objectid()
// => "667c3f2a1e2b3c4d5e6f7081"
// Convert to/from bytes
const bytes = objectid.toBytes(id)
const str = objectid.fromBytes(bytes)
// Extract timestamp (milliseconds)
const ts = objectid.timestamp(id)
import { tsid } from 'uniku/tsid'
// Generate a TSID bigint (the primary type, not a string)
const id = tsid()
// => 862301223059968074n
// Convert to/from the canonical 13-character string
const str = tsid.toString(id)
// => "0QXW2CK4XZM2A"
tsid.fromString(str) === id // true
// Extract timestamp (milliseconds, full precision)
const ts = tsid.timestamp(id)
// Convert to/from 8 bytes
const bytes = tsid.toBytes(id)
const restored = tsid.fromBytes(bytes)
uuid- import { v4 as uuidv4 } from 'uuid'
+ import { uuidv4 } from 'uniku/uuid/v4'
nanoid- import { nanoid } from 'nanoid'
+ import { nanoid } from 'uniku/nanoid'
ulid- import { ulid } from 'ulid'
+ import { ulid } from 'uniku/ulid'
@paralleldrive/cuid2- import { createId } from '@paralleldrive/cuid2'
+ import { cuid2 } from 'uniku/cuid2'
@owpz/ksuid- import { KSUID } from '@owpz/ksuid'
+ import { ksuid } from 'uniku/ksuid'
- const id = KSUID.random().toString()
+ const id = ksuid()
- const parsed = KSUID.parse(str)
- const timestamp = parsed.timestamp
+ const timestamp = ksuid.timestamp(str)
bson- import { ObjectId } from 'bson'
+ import { objectid } from 'uniku/objectid'
- const id = new ObjectId().toHexString()
+ const id = objectid()
- const timestamp = new ObjectId(str).getTimestamp().getTime()
+ const timestamp = objectid.timestamp(str)
uuidv4 (from uniku/uuid/v4)uuidv4(options?: UuidV4Options): string
uuidv4(options: UuidV4Options | undefined, buf: Uint8Array, offset?: number): Uint8Array
uuidv4.toBytes(id: string): Uint8Array
uuidv4.fromBytes(bytes: Uint8Array): string
uuidv4.isValid(id: unknown): id is string
uuidv4.NIL // "00000000-0000-0000-0000-000000000000"
uuidv4.MAX // "ffffffff-ffff-ffff-ffff-ffffffffffff"
uuidv7 (from uniku/uuid/v7)uuidv7(options?: UuidV7Options): string
uuidv7(options: UuidV7Options | undefined, buf: Uint8Array, offset?: number): Uint8Array
uuidv7.toBytes(id: string): Uint8Array
uuidv7.fromBytes(bytes: Uint8Array): string
uuidv7.timestamp(id: string): number
uuidv7.isValid(id: unknown): id is string
uuidv7.NIL // "00000000-0000-0000-0000-000000000000"
uuidv7.MAX // "ffffffff-ffff-ffff-ffff-ffffffffffff"
ulid (from uniku/ulid)ulid(options?: UlidOptions): string
ulid(options: UlidOptions | undefined, buf: Uint8Array, offset?: number): Uint8Array
ulid.toBytes(id: string): Uint8Array
ulid.fromBytes(bytes: Uint8Array): string
ulid.timestamp(id: string): number
ulid.isValid(id: unknown): id is string
ulid.NIL // "00000000000000000000000000"
ulid.MAX // "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"
typeid (from uniku/typeid)typeid(prefix: string, options?: TypeidOptions): string
typeid.toBytes(id: string): Uint8Array
typeid.fromBytes(prefix: string, bytes: Uint8Array): string
typeid.toUuid(id: string): string
typeid.fromUuid(prefix: string, uuid: string): string
typeid.timestamp(id: string): number
typeid.prefix(id: string): string
typeid.suffix(id: string): string
typeid.isValid(id: unknown): id is string
Use an empty prefix (typeid('')) to generate Jetify-compatible prefixless TypeIDs.
cuidv2 (from uniku/cuid/v2)cuidv2(options?: CuidV2Options): string
cuidv2.isValid(id: unknown): id is string
Also available (deprecated) as
cuid2fromuniku/cuid2— the same generator under the pre-versioned entry point.
nanoid (from uniku/nanoid)nanoid(): string
nanoid(size: number): string
nanoid(options: NanoidOptions): string
nanoid.isValid(id: unknown): id is string
// Constant
URL_ALPHABET // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"
ksuid (from uniku/ksuid)ksuid(options?: KsuidOptions): string
ksuid(options: KsuidOptions | undefined, buf: Uint8Array, offset?: number): Uint8Array
ksuid.toBytes(id: string): Uint8Array
ksuid.fromBytes(bytes: Uint8Array): string
ksuid.timestamp(id: string): number
ksuid.isValid(id: unknown): id is string
ksuid.NIL // "000000000000000000000000000"
ksuid.MAX // "aWgEPTl1tmebfsQzFP4bxwgy80V"
objectid (from uniku/objectid)objectid(options?: ObjectIdOptions): string
objectid(options: ObjectIdOptions | undefined, buf: Uint8Array, offset?: number): Uint8Array
objectid.toBytes(id: string): Uint8Array
objectid.fromBytes(bytes: Uint8Array): string
objectid.timestamp(id: string): number
objectid.isValid(id: unknown): id is string
objectid.NIL // "000000000000000000000000"
objectid.MAX // "ffffffffffffffffffffffff"
tsid (from uniku/tsid)tsid(options?: TsidOptions): bigint
tsid(options: TsidOptions | undefined, buf: Uint8Array, offset?: number): Uint8Array
tsid.toBytes(id: bigint): Uint8Array
tsid.fromBytes(bytes: Uint8Array): bigint
tsid.toString(id: bigint): string
tsid.fromString(str: string): bigint
tsid.timestamp(id: bigint, epoch?: number): number
tsid.isValid(id: unknown): id is bigint
tsid.NIL // 0n
tsid.MAX // 18446744073709551615n
Unlike every other generator listed here,
tsid()returnsbigintby default, andtoBytes/fromBytes/timestamp/isValidall operate on thatbigint.toString/fromStringare the boundary conversions to/from the 13-character canonical Crockford Base32 string.
For advanced usage, examples, and contributing guidelines, see the full documentation on GitHub.
The uniku@1.x stability contract defines the supported entry points, runtimes, and release gates.
Third-party libraries that inspired this project:
Other:
Hi, I'm Alberto Schiabel, aka @jkomyno. You can follow me on:
MIT — see LICENSE
FAQs
Minimal, tree-shakeable unique ID generators for every JavaScript runtime
The npm package uniku receives a total of 122,275 weekly downloads. As such, uniku popularity was classified as popular.
We found that uniku demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.