npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

modeid

v0.1.1

Published

Hybrid Adaptive Identifier — 128-bit IDs with profiles for db, shard, and opaque use cases

Readme

modeid

HAID (Hybrid Adaptive Identifier) — npm package modeid. Source repo: sagarchive/modeid.

CI

Hybrid Adaptive Identifier — 128-bit IDs with explicit profiles for row-store DBs, hash-partitioned stores, and public-facing opaque tokens.

  • Profile-baseddb, shard, opaque; pick by storage engine and threat model, not one global format
  • 128-bit — Same size as UUID/ULID; 26-character Crockford base32 strings (URL-safe, no hyphens on output)
  • Documented wire formatSPEC.md frozen at v0.1.0; conformance tests in-repo
  • Sortabledb and opaque (with key) approximate creation-time lex order; shard spreads partitions
  • Monotonic semantics — Shared generator core: same-ms counter, clock-rollback handling, drift guard (SPEC.md §6)
  • Zero runtime dependencies — TypeScript compiled to ESM; tree-shakable subpath exports
  • Cross-runtime — Node 18+, browsers, Workers, Deno, Bun (Web Crypto for opaque)

Status: v0.1.0 — wire format and encoding are frozen. Public API may change until v1.0. Not a drop-in replacement for RFC UUID or ULID bytes on the wire.

Quickstart

1. Install

npm install modeid

Local development (before publish):

npm install file:../modeid

2. Generate an ID

import { db } from 'modeid';

const id = db.generate();
id.toString();   // ⇨ '400sw0shksqbv0pgr1f4m0y31w' (26 chars)
id.timestamp();  // ⇨ 1715000000000
id.profile();    // ⇨ 'db'

3. Parse and validate

import { parse, validate } from 'modeid';

const back = parse(id.toString());
back.equals(id);              // ⇨ true
validate(id.toString());      // ⇨ true
validate('not-a-modeid');       // ⇨ false

When to use which profile

| Profile | Use instead of | Storage / exposure | |---------|----------------|-------------------| | db | UUIDv7, ULID (B-tree PK) | Postgres/MySQL uuid / binary(16) PK | | shard | UUIDv7 on Cassandra/Dynamo | Hash-partitioned writes; prefix spread | | opaque | UUIDv4 in public URLs | External IDs; hide creation time without key |

There is no single profile that replaces all UUID and ULID uses. See Migration.


API summary

| Export | Description | |--------|-------------| | db.generate() | Default row-store ID (sync) | | DbGenerator | Configurable db generator | | shard.generate() | Default sharded-store ID (sync) | | ShardGenerator | Keyed / generatorId shard generator | | OpaqueGenerator | Public opaque IDs (async) | | unwrapOpaque() | Recover time + counter with key | | parse(str) | Parse string → HaidId | | validate(str) | true if valid HAID string | | compare(a, b) | Lexicographic byte compare | | HaidId | Opaque token type |

Subpath imports (tree-shaking)

import { generate } from 'modeid/db';
import { ShardGenerator } from 'modeid/shard';
import { OpaqueGenerator, unwrapOpaque } from 'modeid/opaque';
import { parse, validate } from 'modeid';

API

HaidId

Returned by all generators and parse().

| Method | Returns | Notes | |--------|---------|-------| | toString() | string | 26-char lowercase Crockford; canonical form | | toBytes() | Uint8Array | 16 bytes; store in uuid / bytea columns | | profile() | 'db' \| 'shard' \| 'opaque' | From header byte | | timestamp() | number | Unix ms; throws on opaque (use unwrapOpaque) | | equals(other) | boolean | Constant-time-ish byte compare | | debug() | object | Unstable; debugging only |

parse(str)

| | | |-|-| | str | 26-char HAID string (hyphens allowed, stripped) | | returns | HaidId | | throws | Invalid length, charset, version, profile, or trailing bits |

import { parse } from 'modeid';

parse('400sw0shksqbv0pgr1f4m0y31w');

validate(str)

| | | |-|-| | str | Candidate string | | returns | boolean — never throws |

import { validate } from 'modeid';

validate('400sw0shksqbv0pgr1f4m0y31w'); // true

compare(a, b)

| | | |-|-| | returns | -1 \| 0 \| 1 — lexicographic on 16-byte form |

For db IDs, byte order ≈ creation-time order. For shard, use .timestamp() for time ordering.

import { compare, db } from 'modeid';

const a = db.generate();
const b = db.generate();
compare(a, b); // -1 | 0 | 1

db profile

Row-store primary keys. Layout: 48-bit unix_ms + 16-bit monotonic counter + 56-bit entropy. Header 0x20.

Module convenience

import { db } from 'modeid';

const id = db.generate();

DbGenerator

import { DbGenerator } from 'modeid';

const gen = new DbGenerator({
  timeSource: () => Date.now(),
  maxDriftMs: 3_600_000, // throw if clock drifts >1h (default)
});
const id = gen.generate();

| Option | Default | Description | |--------|---------|-------------| | timeSource | Date.now | Millisecond clock | | maxDriftMs | 3600000 | Max lastMs - now before throw |


shard profile

Hash-partitioned stores. SipHash-2-4 prefix (16 bits) + 48-bit time + 56-bit entropy. Header 0x21. No on-wire counter (entropy only within a ms).

import { shard, ShardGenerator } from 'modeid';

shard.generate();

const gen = new ShardGenerator({
  key: undefined,           // default: zero key (public prefix derivation)
  generatorId: myPodId,     // 8 bytes; default random per process
});
const id = gen.generate();
const prefix = gen.prefixFor(Date.now()); // range-scan helper

| Option | Default | Description | |--------|---------|-------------| | key | 16 zero bytes | SipHash key; set for keyed mode | | generatorId | random 8 bytes | Per-instance; spreads same-ms writes across shards |


opaque profile

Public-facing IDs. AES-128 single-block keystream XOR over encrypted time+counter; 56-bit nonce in plaintext. Header 0x22. Requires 16-byte key.

import { OpaqueGenerator, unwrapOpaque } from 'modeid';

const key = crypto.getRandomValues(new Uint8Array(16));
const gen = new OpaqueGenerator({ key });

const id = await gen.generate();
id.toString(); // looks random to outsiders

const { timestamp, counter } = await unwrapOpaque(id, key);

| | | |-|-| | generate() | Promise<HaidId> — async (Web Crypto) | | unwrapOpaque(id, key) | Promise<{ timestamp, counter }> |

Not provided: MAC/authenticity; treat IDs as opaque tokens and validate existence in your DB.


Encoding

  • Alphabet: Crockford base32 lowercase (0123456789abcdefghjkmnpqrstvwxyz)
  • Length: 26 characters for 128 bits
  • Parse: case-insensitive; I/L1, O0; hyphens ignored
  • Output: never emits hyphens

Database storage

Store id.toBytes() (16 bytes) in Postgres uuid, MySQL binary(16), or text via id.toString().

-- Postgres example
CREATE TABLE items (
  id uuid PRIMARY KEY DEFAULT NULL,
  ...
);
-- insert: pass 16-byte buffer from id.toBytes()

Migration from UUID / ULID

| From | To | Action | |------|-----|--------| | uuid.v7() / ULID for new PKs | db.generate() | New column or new tables only; re-encode is required | | uuid.v4() in URLs | opaque + key | Add key management; async generate | | Hot partitions with time-sorted UUIDs | shard | Use prefixFor() for range scans | | Existing UUID/ULID columns | — | No in-place wire upgrade; migrate with backfill |

Not supported: RFC v1/v3/v4/v5/v6, namespace UUIDs, hyphenated UUID canonical form, ULID byte compatibility.

Rough equivalence

  • ULID: 48-bit time + 80-bit random, monotonic via random increment → HAID db uses explicit 16-bit counter + 56-bit entropy and a profile header.
  • uuid v7: RFC layout and 36-char hex → HAID db is 26-char Crockford with different bit layout (SPEC.md).

Support

| Runtime | Support | |---------|---------| | Node.js | 18+ (CI: 20, 22 on Ubuntu + Windows) | | TypeScript | Types included; strict compatible | | Browsers | ESM + Web Crypto (opaque needs crypto.subtle) | | Deno / Bun / Workers | Expected to work; not all matrix-tested in CI | | React Native | Polyfill crypto.getRandomValues / subtle before import (same as uuid) |


Development

Branch naming: Conventional Branch 1.0.0 (main, feature/…, fix/…, release/v0.1.0, etc.).

npm ci
npm run typecheck
npm run build
npm test              # 54 tests
npm run test:smoke    # consumer smoke against dist/

Known issues

opaque generate() is async

Web Crypto AES has no sync API in portable code. Use await or .then().

Not interchangeable with UUID or ULID parsers

Strings and bytes differ. Do not feed HAID strings into uuid.parse() or ULID decoders.

package.json / monorepo CI

If this package lives in a monorepo, point the CI badge at your repo’s workflow path.


Specification & license