Skip to content

only-cliches/nano64

Repository files navigation

Nano64 — 64-bit Time-Sortable Identifiers for TypeScript

Nano64 is a lightweight library for generating time-sortable, globally unique IDs that provide similar practical guarantees to ULID or UUID while using half the storage.
This reduces index and I/O overhead while preserving cryptographic-grade randomness. Includes optional monotonic sequencing and AES-GCM encryption for timestamp privacy.

GitHub Repo stars NPM Version JSR Version npm package minimized gzipped size TypeScript License: MIT

Features

  • Time-sortable: IDs naturally order by creation time.
  • Compact: 8 bytes (16 hex characters).
  • Deterministic layout: [63‥20]=timestamp, [19‥0]=random.
  • Collision-resistant: ~1 % collision probability at 145 000 IDs/s.
  • Cross-database-safe: Big-endian bytes preserve order in SQLite, PostgreSQL, MySQL, and others.
  • AES-GCM encryption: Optionally hides the embedded timestamp.
  • Unsigned canonical form: Portable numeric representation 0‥2⁶⁴ − 1.
  • Typed and tested: 100 % TypeScript with full Vitest coverage.

Installation

npm install nano64

Usage

Basic ID generation

import { Nano64 } from "nano64";

const id = Nano64.generate();

console.log(id.toHex());        // 17-char uppercase hex TIMESTAMP-RANDOM
// 199C01B6659-5861C
console.log(id.toBytes());      // Uint8Array(8)
// [25,156,1,182,101,149,134,28]
console.log(id.getTimestamp()); // ms since epoch
// 1759864645209

Monotonic generation

Ensures strictly increasing values even when created within the same millisecond.

const a = Nano64.generateMonotonic();
const b = Nano64.generateMonotonic();

console.log(Nano64.compare(a, b)); // -1

AES-GCM encryption

Encrypt and decrypt IDs to hide the embedded timestamp from public view. Encrypted IDs can be safely shared to the internet without exposing any timestamp information of the source ID.

const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
const factory = Nano64.encryptedFactory(key);

// Generate and encrypt
const wrapped = await factory.generateEncrypted();
console.log(wrapped.id.toHex()) // Unencrypted ID
// 199C01B66F8-CB911
console.log(wrapped.toEncryptedHex()); // 72‑char hex payload
// 2D5CEBF218C569DDE077C4C1F247C708063BAA93B4285CD67D53327EA4C374A64395CFF0

// Decrypt later
const restored = await factory.fromEncryptedHex(wrapped.toEncryptedHex());
console.log(restored.id.value === wrapped.id.value); // true

Comparison with Other Identifiers

Property Nano64 ULID UUIDv4 Snowflake ID
Bits total 64 128 128 64
Encoded timestamp bits 44 48 0 41
Random / entropy bits 20 80 122 22 (per-node sequence)
Sortable by time ✅ Yes (lexicographic & numeric) ✅ Yes ❌ No ✅ Yes
Collision risk (1%) ~145 IDs/ms ~26M/ms Practically none None (central sequence)
Typical string length 16 hex chars 26 Crockford base32 36 hex+hyphens 18–20 decimal digits
Encodes creation time
Can hide timestamp ✅ via AES-GCM encryption ⚠️ Not built-in ✅ (no time field) ❌ Not by design
Database sort order ✅ Stable with big-endian BLOB ✅ (lexical) ❌ Random ✅ Numeric
Cryptographic strength 20-bit random, optional AES 80-bit random 122-bit random None (deterministic)
Dependencies None (crypto optional) None None Central service or worker ID
Target use Compact, sortable, optionally private IDs Human-readable sortable IDs Pure random identifiers Distributed service IDs

Nano64 keeps the chronological behavior of ULIDs but in 64 bits instead of 128, cutting key size by half without sacrificing sort order or safety.

Database Usage

Nano64 IDs are time-sortable, enabling index-only time-range queries without needing a separate timestamp column.

Time-Based Range Queries

Use Nano64.timeRangeToBytes(startMs, endMs) to obtain the lowest and highest possible Nano64 values for a given time window. These can be used directly in a SQL BETWEEN clause to select all rows created within that range.

Using Nano64 as the primary key is recommended, since the database’s native index makes these range queries extremely fast.

Storing Nano64 IDs in SQL

Store IDs as unsigned big-endian bytes using id.toBytes() and a byte-ordered column type.

DBMS Column Type Notes
SQLite BLOB(8) Lexicographic byte order matches unsigned big-endian.
PostgreSQL BYTEA(8) Works with primary key indexes.
MySQL 8+ BINARY(8) Use binary collation.
MariaDB BINARY(8) Same as MySQL.
SQL Server BINARY(8) Clustered index sorts by bytes.
Oracle RAW(8) Bytewise comparison.
CockroachDB BYTES(8) Bytewise ordering.
DuckDB BLOB(8) Bytewise ordering.

SQLite Example

import Database from "better-sqlite3";
import { Nano64 } from "nano64";

const db = new Database(":memory:");
db.exec("CREATE TABLE events (id BLOB PRIMARY KEY, message TEXT)");

// generate IDs
const id1 = Nano64.generate(Date.now() - 2000);
const id2 = Nano64.generate(Date.now() - 1000);
const id3 = Nano64.generate(Date.now());  

// insert records
const insert = db.prepare("INSERT INTO events (id, message) VALUES (?, ?)");
insert.run(id1.toBytes(), "Event from 2s ago");
insert.run(id2.toBytes(), "Event from 1s ago");
insert.run(id3.toBytes(), "Event from now");

// search for rows between now and 1.5 seconds ago
const tsEnd = Date.now();
const tsStart = tsEnd - 1500;

const [ start, end ] = Nano64.timeRangeToBytes(tsStart, tsEnd);

const query = db.prepare("SELECT * FROM events WHERE id BETWEEN ? AND ?");
const results = query.all(start, end);

// Will only get 2 rows
console.log(`Found ${results.length} events between ${new Date(tsStart).toISOString()} and ${new Date(tsEnd).toISOString()}`);

for (const row of results) {
  const found = Nano64.fromBytes(row.id);
  console.log(`- ${found.toHex()} @ ${found.toDate().toISOString()}${row.message}`);
}

You can also store IDs as integers in your database, which can increase performance but comes with some caveats. Database integer storage instructions.

API Summary

Nano64.generate(timestamp?, rng?)

Creates a new ID with optional timestamp and RNG.

Nano64.generateMonotonic(timestamp?, rng?)

Same as generate, but strictly increasing within the same millisecond.

Nano64.fromHex(hex) / fromBytes(bytes) / fromBigIntUnsigned(v)

Parse back into a Nano64.

id.toHex() / id.toBytes() / id.toDate() / id.getTimestamp()

Export utilities.

Nano64.compare(a,b) / id.equals(b)

Comparison helpers.

Nano64.encryptedFactory(key, clock?)

Returns an object with encrypt, generateEncrypted, fromEncryptedBytes, and fromEncryptedHex.

Design

Bits Field Purpose Range
44 Timestamp (ms) Chronological order 1970–2527
20 Random Collision avoidance 1 048 576 / ms

Collision characteristics:

  • Theoretical: ~1% collision probability at 145 IDs/millisecond
  • Real-world sustained rate (145k IDs/sec): <0.05% collision rate
  • High-speed burst (3.4M IDs/sec): ~0.18% collision rate
  • Concurrent generation (10.6M IDs/sec): ~0.58% collision rate

Reference: go-nano64 data

Tests

npm test

All tests are written in Vitest and cover:

  • Hex ↔ byte conversions
  • BigInt encoding
  • Timestamp extraction and monotonic logic
  • AES-GCM encryption / decryption integrity
  • Overflow and edge-case handling
  • BLOB Primary key and range queries with SQLite

Unofficial Ports

License

MIT License

Keywords

nano64, ulid, time-sortable, 64-bit id, bigint, aes-gcm, uid, uuid alternative,
distributed id, database key, monotonic id, sortable id, crypto id,
typescript, nodejs, browser, timestamp id