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

entity-id

v1.1.3

Published

A DX-friendly entity ID toolkit: prefixed, self-describing IDs with branded TypeScript types, Zod schemas, a prefix registry and a matching Postgres generator. Isomorphic and ESM-only; ~3 KB until you import the schemas.

Readme

entity-id

Prefixed, timestamped entity identifiers with branded TypeScript types, a Zod validator and a matching PostgreSQL generator.

usr_a1b2c3d4e5f6g7h8.01jd8x2p4q
└─┬─┘ └──────┬──────┘ └────┬───┘
  │          │             └── ULID time segment  (10 chars, ms since epoch)
  │          └──────────────── ULID randomness    (16 chars, 80 bits)
  └─────────────────────────── entity-type prefix (2–16 chars)

An id says what it points at (usr_… is a user, ord_… is an order), is globally unique, and carries its own creation time. Because the type is branded, the compiler will not let a UserId reach a slot expecting an OrderId — and because the same contract is implemented in SQL, an id minted by Postgres validates in TypeScript and vice versa.

Runs unchanged in Node, Bun, Deno, edge workers and the browser.

Install

npm install entity-id

ulid and zod are regular dependencies — nothing else to install.

TypeScript 5.4 or newer. Verified by type-checking a consumer against 5.4, 5.5, 5.6, 5.9 and 7.0 with skipLibCheck off. The floor comes from zod, whose declarations use NoInfer (added in TS 5.4); this package's own branded types need 5.0+ for const type parameters. JavaScript consumers need no TypeScript at all.

The declarations already type-check cleanly under TypeScript 7.0, so an upgrade needs no change here.

Runtimes: Node 18+, Bun, Deno, edge workers and the browser. The main entry point is isomorphic; only entity-id/async requires a Node-style runtime, as it uses AsyncLocalStorage.

Quick start

import { createEntityId, parseEntityId, isEntityId } from 'entity-id';

const id = createEntityId('usr');
// 'usr_a1b2c3d4e5f6g7h8.01jd8x2p4q'

isEntityId(id); // true

parseEntityId(id);
// {
//   prefix: 'usr',
//   rand:   'a1b2c3d4e5f6g7h8',
//   ts:     '01jd8x2p4q',
//   ulid:   '01JD8X2P4QA1B2C3D4E5F6G7H8',
//   timeMs: 1731412345678,
// }

Examples

Seven runnable examples live in examples/, each a complete file you can execute:

| File | Shows | | --- | --- | | 01-basics.ts | minting, decoding, chronological sorting, migrating from ULIDs | | 02-registry.ts | a typed registry, per-kind guards, compile-time separation | | 03-sync-validation.ts | the three modes side by side, synchronously | | 04-async-validation.ts | async scopes, concurrency, an HTTP-style boundary | | 05-schemas.ts | Zod contracts and JSON Schema output | | 06-postgres.ts | table definitions and the migration | | 07-initialization.ts | where to set the mode, layering edges, anti-patterns |

npm run examples          # run them all
npm run examples -- 04    # run one

Why prefixed ids

A UUID in a log line, a URL or a support ticket is anonymous: you cannot tell what it identifies, and pasting one into the wrong query silently returns nothing. A prefixed id is self-describing, greppable, and — because the prefix is part of the value — verifiable at the boundary.

Compared to the alternatives:

| | UUIDv4 | ULID | entity-id | | --- | --- | --- | --- | | Globally unique | yes | yes | yes | | Carries creation time | no | yes | yes | | Says what it identifies | no | no | yes | | Distinct type per entity | no | no | yes | | Wrong-kind id caught at runtime | no | no | yes | | Database-native generator | yes | no | yes |

Registry: one place for every prefix

Declare each entity kind once. The registry validates the map eagerly — a malformed or duplicated prefix throws at startup rather than producing an un-routable id later — and derives a typed toolkit per kind.

import { defineEntityPrefixes, type EntityIdOf } from 'entity-id';

export const registry = defineEntityPrefixes({
  user: 'usr',
  order: 'ord',
  orderLine: 'orl',
} as const);

export type UserId = EntityIdOf<typeof registry.prefixes, 'user'>;
export type OrderId = EntityIdOf<typeof registry.prefixes, 'order'>;

const { user, order } = registry.ids;

const id = user.create();        // UserId
user.is(id);                     // true  — type guard
order.is(id);                    // false — a different kind
order.assert(id);                // throws: expected prefix "ord_"
registry.kindOf(id);             // 'user'

Duplicate prefixes are rejected with both claimants named, because an ambiguous prefix makes an id impossible to route back to one kind:

defineEntityPrefixes({ program: 'prg', progress: 'prg' });
// EntityIdError: Duplicate entity prefix "prg": claimed by both "program" and "progress".

The per-kind toolkit

| Member | Purpose | | --- | --- | | create(options?) | Mint a fresh branded id | | is(value) | Type guard narrowing to this kind | | assert(value) | Throwing parse at a boundary | | brand(value) | Unchecked cast, for trusted construction |

The toolkit carries no Zod. Schemas are opt-in through withSchemas, so an application that only mints and checks ids never bundles a validator library it does not call — see Module format and bundle cost.

import { withSchemas } from 'entity-id/schema';

const schemas = withSchemas(registry);

| Member | Purpose | | --- | --- | | schemas.user.schema | Strict Zod schema (full format) | | schemas.user.prefixSchema | Lenient schema (prefix only) | | schemas.user.jsonSchema(io?) | JSON Schema for a contract |

Branded types

The brand is what turns a naming convention into a compiler-enforced one. A plain string is not assignable to an EntityId, and two kinds are not assignable to each other:

declare function getUser(id: UserId): Promise<User>;

getUser(orderId);        // compile error: OrderId is not a UserId
getUser('usr_whatever'); // compile error: string is not a UserId
getUser(user.assert(x)); // fine — parsed at the boundary

The only ways to obtain a branded value are minting, parsing, asserting, or the explicit unsafeBrandEntityId escape hatch — so an unvalidated string cannot reach your domain by accident.

Validation with Zod

import { entityIdSchema, brandedEntityIdSchema } from 'entity-id';

entityIdSchema.parse(' usr_A1B2C3D4E5F6G7H8.01JD8X2P4Q ');
// 'usr_a1b2c3d4e5f6g7h8.01jd8x2p4q'  — trimmed, lowercased, branded

const userIdSchema = brandedEntityIdSchema<'user'>('usr');

const CreateOrder = z.object({
  userId: userIdSchema,
  total: z.number(),
});

The ULID segments are accepted in mixed case and normalized to canonical lowercase on the way through — in every mode, because normalization decides what a value IS rather than how strictly it is checked. The prefix itself is lowercase-only, so an upper-cased prefix is rejected rather than silently coerced.

JSON Schema

The schemas are built as Zod codecs rather than with .transform(), which matters in practice: a bare transform is unrepresentable in JSON Schema, so any contract embedding an id would fail to serialize. Here both sides render.

import { entityIdJsonSchema } from 'entity-id';

entityIdJsonSchema(withSchemas(registry).user.schema);
// { type: 'string', pattern: '^usr_[0-9a-hjkmnp-tv-z]{16}\\.[0-9a-hjkmnp-tv-z]{10}$' }

entityIdJsonSchema(entityIdSchema, 'input');  // permissive: accepts mixed case

Use 'input' for request payloads and 'output' for responses — an OpenAPI document or an MCP tool contract can embed either directly.

Validation modes

Validation is a trade-off, so the package lets you pick where you sit on it. The mode is global, set once at startup, and overridable per call.

import { setValidationMode, isEntityId } from 'entity-id';

setValidationMode('full');           // process-wide, at startup
isEntityId(value, { mode: 'fast' }); // just this call

| Mode | Checks | Cost | Use it for | | --- | --- | --- | --- | | fast | nothing | ~8 ns | data already known good — rows from a column with the CHECK, ids this process minted | | mixed (default) | the <prefix>_ head | ~28 ns | where business logic starts — catches a wrong-kind id in the wrong slot | | full | prefix, both segment lengths, the Crockford alphabet, overall shape | ~63 ns | trust boundaries — HTTP requests, webhooks, imports |

A mode is a profile, not merely a strictness level: it also selects the decoding path (fast and mixed split the string; full matches the regular expression). Future capabilities will be added as further fields of that profile, so the three names stay stable.

Where to set the mode

Set it once, in your application's entry point, before you serve traffic:

// server.ts
import { setValidationMode } from 'entity-id';

setValidationMode(process.env.NODE_ENV === 'production' ? 'mixed' : 'full');

Import order does not matter. The mode is read when a value is validated, not when a schema is built, so schemas and registries created at module load — before your bootstrap runs — still obey the mode you set later. You never have to police your imports.

A library should not call setValidationMode: the setting is process-wide, so a library changing it would silently alter the behaviour of the application that depends on it. Libraries should use a per-call { mode } instead.

In tests, reset it so a leaked mode cannot change the meaning of later assertions:

afterEach(() => resetValidationMode());

A typical layering is a cheap interior with strict edges:

| Where | Mode | How | | --- | --- | --- | | Application default | mixed | setValidationMode('mixed') at startup | | Public endpoint | full | bindValidationMode('full', handler) | | Trusted internal worker | fast | bindValidationMode('fast', worker) | | One-off strict check | full | isEntityId(v, { mode: 'full' }) |

See examples/07-initialization.ts for a runnable version of all of this.

Asynchronous code

withValidationMode is synchronous. Handing it an async function is a mistake — the callback returns a promise immediately, so the mode would be restored at the first await rather than at the end, and two concurrent requests would overwrite each other's setting. Rather than fail silently, it throws.

For asynchronous work, entity-id/async provides a scope backed by AsyncLocalStorage:

import { withValidationModeAsync, bindValidationMode } from 'entity-id/async';

await withValidationModeAsync('full', async () => {
  const body = await request.json();
  return userIdSchema.parse(body.userId); // strict, across every await
});

// Or pin a handler once:
const handler = bindValidationMode('full', async (req) => { /* ... */ });

Concurrent scopes are isolated: two requests served at the same time under different modes do not interfere. The module is a separate entry point because it imports node:async_hooks; the main entry point stays isomorphic and works in the browser.

Why there is no browser version

Not an oversight: the browser has no primitive for it. Carrying a value across await boundaries requires the runtime to propagate a context through every microtask and timer, and no engine exposes that today.

  • AsyncLocalStorage is a Node API (also in Bun and Deno). It is not part of the web platform.
  • AsyncContext, the TC39 proposal that would fix this, is at Stage 2 and ships in no browser — verified against Chrome 151, where typeof AsyncContext === 'undefined'.
  • Userland emulation means monkey-patching every async API: setTimeout, Promise, fetch, event listeners. That is what zone.js does, at roughly 1.7 MB unpacked, and it still misses anything it has not patched — far too much weight for a validation mode.

Without such a primitive the naive approach does not merely fail, it fails silently: a plain variable is restored the moment the callback suspends, so concurrent flows read whatever the last writer left behind. That is exactly why the synchronous withValidationMode throws on an async function rather than pretending to work.

In practice this rarely bites in a browser, where there is one user and little genuine concurrency:

  • setValidationMode at startup works fine, and
  • the per-call { mode } option works everywhere, needing no ambient context at all.

If a future browser ships AsyncContext — or you already run a scope provider of your own — you can wire it up yourself; the hook is public:

import { setAmbientModeResolver } from 'entity-id';

const modeVar = new AsyncContext.Variable();
setAmbientModeResolver(() => modeVar.get());

The core consults your provider first and falls back to the process-wide setting, exactly as entity-id/async does.

The per-call { mode } option is safest of all and needs no scope: it travels with the call, so no amount of interleaving can race it.

isEntityId(value, { mode: 'full' }); // immune to concurrency

What a mode does not change

Some guarantees are structural and hold in every mode:

  • createEntityId always mints a canonical id. There is nothing to validate when you are the one generating.
  • normalizeEntityId is always strict. It produces the canonical form, so it cannot trust an unvalidated input.
  • The registry always rejects a duplicate prefix, at startup. That check costs nothing at runtime and prevents an un-routable id, so tying it to a performance setting would be the wrong coupling.
  • kindOf always routes on a registered prefix; the mode only decides whether the remainder is inspected too.
  • strictEntityIdSchema ignores the active mode, for the one boundary that must stay strict inside an otherwise-fast application.

The honest caveat about fast

In fast mode assert() returns a branded value without checking it. The brand normally means "this was validated"; in fast that promise is transferred to you. Use it only where the data provenance already guarantees the format.

mixed is not a security boundary either. It checks the <prefix>_ head and nothing after it. Whatever follows is returned untouched — quotes, SQL fragments, a payload hundreds of characters long — so under the default mode both of these parse:

memoryId.schema.parse('mem_' + untrustedInput); // accepted: the head is correct
memoryId.schema.parse('oac_' + 'x'.repeat(200)); // accepted: no length bound

That is the intended trade — mixed catches the mistake that actually happens (a wrong-kind id in the wrong slot) at a fraction of the cost. It is not a sanitizer. Where a value crosses a trust boundary or reaches a database, ask for strictness explicitly: strictEntityIdSchema, { mode: 'full' } on a call, or your own schema built from the exported CROCKFORD_CANONICAL_CLASS, RAND_LENGTH and TS_LENGTH.

JSON Schema is unaffected

The emitted JSON Schema always advertises the complete canonical pattern, whatever the active mode. A mode is a local performance decision; a published contract must describe the format as it really is.

Performance

Measured on Node 26 / linux-x64 (npm run bench — numbers are hardware-specific; the ratios are the point):

| Operation | fast | mixed | full | | --- | --- | --- | --- | | isEntityId | 132 M/s | 36 M/s | 16 M/s | | assertEntityIdWithPrefix | 97 M/s | 13 M/s | 1.4 M/s | | parseEntityId | 7.6 M/s | 7.6 M/s | 2.0 M/s |

Generation runs at ~2.0 M ids/s (monotonic, the default), against 10.8 M/s for crypto.randomUUID() — the gap is the price of an embedded timestamp and a prefix.

Note on { monotonic: false }: that path runs at ~42 K ids/s, because the underlying ulid library re-seeds its CSPRNG on every call. The default monotonic path is unaffected. Avoid monotonic: false in a hot loop.

Verification

The package is checked as a published artifact, not only as a source tree — a green unit-test run cannot see a broken exports map, a missing file, or module state duplicated across entry points.

| Command | What it proves | | --- | --- | | npm run check | format, lint, types, type-level assertions, 253 unit tests | | npm run verify:package | the packed tarball works when installed, from ESM and CommonJS | | npm run verify:bundle | a consumer who never touches Zod does not bundle it | | npm run verify:types | the branded types reject what they must, from a consumer's seat | | npm run verify:browser | real behaviour in Chromium, including CSPRNG entropy | | npm run examples | every documented example still runs | | scripts/verify-sql-crosscheck.sh | SQL and TypeScript agree, against a live PostgreSQL | | npm run bench | throughput per mode, against ulid and randomUUID baselines |

All of them run in CI, plus a Node matrix (18/20/22/24) and a TypeScript matrix (5.4/5.5/5.9/7.0).

Module format and bundle cost

The package is ESM only. Every Node release still receiving security fixes implements require(ESM), so a CommonJS file can load it directly:

const { createEntityId } = require('entity-id'); // works on Node >=20.19

Node 18 cannot — it fails with ERR_REQUIRE_ESM — which is what engines records. Dropping the CommonJS build also removes the dual-package hazard, where a consumer could otherwise hold two copies of the module-level validation mode and have entity-id/async write to one while the validators read another.

Zod is a dependency, but you only bundle it if you use it. The schema and registry modules are emitted as their own chunks, so their Zod import does not travel with the main entry:

| What you import | Bundled (gzip) | Zod included | | --- | --- | --- | | createEntityId, isEntityId, parseEntityId | ~2.5 KB | no | | defineEntityPrefixes and the per-kind toolkit | ~3.1 KB | no | | entity-id/schemaentityIdSchema, withSchemas | ~88 KB | yes |

npm run verify:bundle measures this against the packed tarball and fails if the main entry ever regains a top-level Zod import.

The registry carries no Zod at all: defineEntityPrefixes gives you create, is, assert and brand, and schemas come from withSchemas in entity-id/schema. Keeping them in separate modules is what makes the separation real — a lazy getter on the toolkit would not have worked, because a static import pulls Zod into the module graph however the value is reached. Measured: the registry costs 3.1 KB gzip, against 89.1 KB when it built schemas itself.

Security

The package is a validator, so it is expected to meet hostile input. What it guarantees, and what it asks of you:

  • No ReDoS. Every quantifier in the id patterns is bounded, so matching is linear. Verified against 50 KB adversarial payloads — prefix floods, separator floods, near-miss bodies — all in single-digit milliseconds.
  • SQL helpers validate their inputs. entityIdColumnSql, entityIdCheckSql and entityIdDefaultSql build SQL by concatenation, so both the prefix and the column name must be plain lowercase identifiers. Anything else throws rather than reaching the statement.
  • No prototype pollution. A registry never writes to Object.prototype, inherited members (toString, constructor) are not treated as registered kinds, and prototype names are rejected as kind names.
  • Type-safe guards. isEntityId returns false for any non-string, including objects with a crafted toString.
  • CSPRNG randomness, from ulid — see the design trade-off below.
  • No runtime dependency has install scripts, and the dependency tree is two packages deep with nothing transitive.

Where the responsibility is yours: in fast mode nothing is validated, so assert() brands a value it never checked. Use it only for data whose provenance already guarantees the format. The default mixed mode is safe.

Adversarial cases are pinned in src/security.spec.ts, so a regression fails the build rather than shipping.

Design trade-offs

Three decisions are deliberate, and worth stating plainly rather than leaving to be discovered:

The id string is not chronologically sortable. The randomness segment comes before the timestamp, so ORDER BY id is not ORDER BY created_at. Formats like TypeID make the opposite choice and are K-sortable. The reason for this one: ids that begin with random bytes spread across a B-tree instead of all landing on its right-hand edge, which avoids index hot-spotting on insert.

The cost of that choice is bounded and measurable: chronological order needs either a created_at column or a functional index on the timestamp suffix, and both answer in well under a millisecond on 200 000 rows — see Ordering. What you trade away is the ability to get that order for free from the primary key alone.

The default validates, but not exhaustively. mixed checks the prefix and stops. Most id bugs in practice are a wrong-kind id reaching the wrong slot — not a corrupted ULID segment — and that is exactly what the prefix catches, at a fraction of the cost of full validation.

Randomness comes from ulid, not from hand-written crypto. A buffered CSPRNG pool of our own would make the monotonic: false path roughly 87× faster and remove a dependency. It is not worth it: generating randomness is security-sensitive, and a widely-used, audited implementation is preferable to in-house code on the one path where a subtle mistake would be both silent and serious. The speed of the default monotonic path was never in question.

Ordering

The randomness segment precedes the timestamp, so an id string is not chronologically sortable. That is a deliberate trade-off (see below), not a dead end — chronological order is available, and cheaply.

In application code, sort by the embedded time explicitly:

import { compareEntityIds } from 'entity-id';

ids.sort((a, b) => compareEntityIds(a, b, 'time'));

In PostgreSQL

Three options, measured on 200 000 rows (PostgreSQL 16, LIMIT 50):

| Approach | Latest-first query | Keyset page | Extra storage | | --- | --- | --- | --- | | created_at column + index | 0.08 ms | 0.08 ms | 4.4 MB | | Functional index on the timestamp suffix | 0.17 ms | 0.30 ms | 6.2 MB | | No index, decode and sort per query | 142 ms | — | none |

A created_at column is the recommendation — it is the fastest, the smallest, and it survives a change of id format.

A functional index is the cheap alternative when you would rather not add a column — to an existing table, say. It needs no schema change and stays in the same order of magnitude. The migration installs public.entity_id_ts_of, and the entity-id/sql entry point renders the statement:

import { entityIdTimeIndexSql, entityIdTimeOrderSql } from 'entity-id/sql';

entityIdTimeIndexSql('events');
// create index if not exists events_id_ts_idx
//   on events (public.entity_id_ts_of(id) desc)

entityIdTimeIndexSql('events', { concurrently: true, ifNotExists: false });
// create index concurrently events_id_ts_idx ...

`select * from events order by ${entityIdTimeOrderSql()} desc limit 50`;

The suffix is fixed-width Crockford Base32, so lexicographic order over it is chronological order — no decoding needed at query time. Measured write cost of keeping this index: about 9% on bulk insert (400 ms against 368 ms for 20 000 rows).

Sorting without an index is the one genuinely expensive option — a full scan and sort, ~1700× slower than either indexed path. Avoid it outside one-off maintenance queries.

PostgreSQL

The database side implements the same contract, so ids generated in either place validate in the other.

npx entity-id sql | psql "$DATABASE_URL"

or from code, using whatever migration runner you already have:

import { ENTITY_ID_SQL, entityIdColumnSql } from 'entity-id/sql';

await db.query(ENTITY_ID_SQL);
await db.query(`create table users (${entityIdColumnSql('usr')}, email text)`);

entityIdColumnSql('usr') expands to a column that both generates and enforces the contract:

id text primary key
  default public.entity_id_generate('usr')
  check (public.is_entity_id_with_prefix(id, 'usr'))

The migration installs entity_id_generate(prefix), is_entity_id(value) and is_entity_id_with_prefix(value, prefix). It needs no extension — randomness comes from core gen_random_uuid() (PostgreSQL 13+) — and every function pins an empty search_path.

The equivalence is enforced, not assumed: src/sql.spec.ts asserts the two definitions share one pattern, and scripts/verify-sql-crosscheck.sh applies the migration to a throwaway PostgreSQL container, feeds database ids through the TypeScript validator, feeds TypeScript ids through the SQL CHECK, and asserts the constraint rejects malformed, wrong-prefix and wrong-alphabet values.

Choosing a prefix

Hand-pick a conventional prefix (userusr, orderord), or derive one deterministically:

import { derivePrefixFromSlug, ensureUniquePrefix } from 'entity-id';

derivePrefixFromSlug('project'); // 'prj'
derivePrefixFromSlug('program'); // 'prg'

const used = new Set(['prj']);
ensureUniquePrefix('prj', 'projection', used); // 'prjr'

A prefix is lowercase, starts with a letter, and is 2–16 alphanumeric characters.

CLI

npx entity-id new usr              # mint one id
npx entity-id new usr --count 5    # mint five
npx entity-id inspect "usr_…"      # decode to JSON
npx entity-id check "usr_…" usr    # exit 0 when valid, 1 when not
npx entity-id derive project       # → prj
npx entity-id sql                  # print the migration

API reference

CreatingcreateEntityId(prefix, options?), fromUlid(prefix, ulid), unsafeBrandEntityId(value)

ValidatingisEntityId, isEntityIdWithPrefix, assertEntityId, assertEntityIdWithPrefix, hasWellFormedPrefix, entityIdSchema, strictEntityIdSchema, entityIdWithPrefixSchema, brandedEntityIdSchema, prefixGatedEntityIdSchema, toEntityId

ModessetValidationMode, getValidationMode, resetValidationMode, withValidationMode (synchronous), getModeProfile, resolveModeProfile, and the types ValidationMode, ValidationDepth, ValidationOptions, ModeProfile

Async modes (entity-id/async) — withValidationModeAsync, bindValidationMode, hasAsyncModeScope

InspectingparseEntityId, safeParseEntityId, normalizeEntityId, entityIdPrefix, entityIdToTimeMs, entityIdToDate, entityIdToIso, entityIdToTuple, toUlid, compareEntityIds

PrefixesnormalizePrefix, isValidPrefix, derivePrefixFromSlug, ensureUniquePrefix, PREFIX_PATTERN, PREFIX_RE

RegistrydefineEntityPrefixes, and the types EntityIdRegistry, EntityIdToolkit, EntityIdOf, EntityPrefixMap

SQL (entity-id/sql) — ENTITY_ID_SQL, entityIdColumnSql, entityIdDefaultSql, entityIdCheckSql

TypesEntityId, BrandedEntityId, ParsedEntityId, EntityIdParts, CreateEntityIdOptions, EntityIdError

Every validating function takes an optional final { mode } argument that overrides the active mode for that one call.

Every error thrown is an EntityIdError carrying the offending value.

Notes on the format

  • Uniqueness — 80 bits of randomness per millisecond, from the platform CSPRNG.
  • Monotonic by default — ids minted in the same millisecond in one process are strictly increasing; pass { monotonic: false } to honour an explicit past timeMs.
  • Length — 31 characters for a 3-character prefix, against 36 for a UUID.
  • URL- and shell-safe — lowercase Crockford Base32 excludes I, L, O and U, so ids resist transcription errors and never form accidental words.

License

MIT © Kiwagu