Skip to content

moritzmyrz/sigilid

sigilid

A tiny, tree-shakeable ID toolkit for TypeScript apps.

CI npm version bundle size install size license

sigilid gives you a secure, URL-safe ID generator as a zero-dependency ESM-first package. The root import is intentionally minimal — extra utilities like prefixed IDs, typed IDs, and validation live in subpath exports so your bundler only pulls in what you actually use.

import { generateId } from "sigilid";

const id = generateId(); // "K7gkJ_q3vR2nL8xH5eM0w"

Features

  • Cryptographically secure — uses crypto.getRandomValues, not Math.random
  • URL-safe by default — 64-character alphabet: A-Z a-z 0-9 _ -
  • Tree-shakeable — subpath exports mean your bundle only includes what you import
  • Zero runtime dependencies — no third-party code in production output
  • Optional Node native fast pathsigilid/native for Node-only throughput tuning
  • ESM-only — works in modern Node, edge runtimes, and all major bundlers
  • Strong TypeScript support — strict types, branded ID types, precise inference
  • Predictable behavior — explicit errors on invalid input, no silent failures
  • One package, seven entrypointsinstall sigilid, then import only what you need
  • Companion native addon package — only needed when using sigilid/native

Bundle size

All sizes are brotli-compressed. Each subpath is a standalone module — importing one never pulls in the others.

Import Size
sigilid ~297 B
sigilid/non-secure ~214 B
sigilid/prefix ~385 B
sigilid/typed ~398 B
sigilid/validate ~360 B
sigilid/alphabet ~380 B

Zero runtime dependencies. Verified by size-limit on every PR.


Install

npm install sigilid
pnpm add sigilid
yarn add sigilid

Node 20+ required. Works in all modern runtimes that expose the Web Crypto API (globalThis.crypto).

Optional native path:

npm install sigilid @sigilid/native-addon

Use @sigilid/native-addon only if you plan to import sigilid/native.


Quick start

import { generateId } from "sigilid";

// Default: 21 URL-safe characters using crypto.getRandomValues
generateId(); // "K7gkJ_q3vR2nL8xH5eM0w"
generateId(12); // "aX4_p9Qr2mNs"

Why sigilid?

Most apps eventually need more than a plain random string. They need prefixed IDs to distinguish entity types in logs, branded TypeScript types to prevent mixing userId and postId, and validation helpers at API boundaries.

sigilid is a focused toolkit for exactly that. The root package is as lean as it gets. Everything optional is a subpath import.


When to use the root import vs subpath exports

If you need... Import from...
A secure random URL-safe ID sigilid
A non-crypto ID (tests, fixtures) sigilid/non-secure
Prefixed IDs like usr_abc123 sigilid/prefix
Branded TypeScript ID types sigilid/typed
Validation at API boundaries sigilid/validate
IDs from a custom character set sigilid/alphabet

The root import has no dependency on any of the subpath modules. Importing only sigilid will not pull in prefix, validation, or alphabet code.


API reference

sigilid — secure root

import { generateId, DEFAULT_ALPHABET } from "sigilid";

generateId(); // 21-character secure ID
generateId(12); // 12-character secure ID

console.log(DEFAULT_ALPHABET);
// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"

generateId throws a RangeError if length is outside the range 1–255 or is not an integer.


sigilid/non-secure — Math.random-based

import { generateNonSecureId } from "sigilid/non-secure";

generateNonSecureId(); // 21-character ID using Math.random
generateNonSecureId(8); // 8-character ID

Not suitable for tokens, secrets, or session identifiers. Use this only when you explicitly do not need cryptographic quality — for example, in test fixtures or non-sensitive local keys.


sigilid/native — optional Node-only fast path

import { generateDefault, generateId } from "sigilid/native";

generateDefault(); // 21-character secure ID
generateDefault(32); // 32-character secure ID

// Alias that mirrors the root naming
generateId(21);
  • Node-only entrypoint.
  • Requires the companion addon package: @sigilid/native-addon.
  • Uses secure randomness and the same default alphabet as sigilid.
  • Throws a clear error if the addon is missing or the runtime is unsupported.
  • Addon install tries prebuilt binaries first, then falls back to local node-gyp build.
  • Publishing note: sigilid and @sigilid/native-addon are versioned/published separately.

If you want the broadest compatibility (browser, edge, and Node) stick to the root sigilid import.


sigilid/prefix — prefixed IDs

import { generatePrefixedId, createPrefixedGenerator } from "sigilid/prefix";

// One-off prefixed ID
generatePrefixedId("usr"); // "usr_K7gkJ_q3vR2nL8xH5eM0w"
generatePrefixedId("doc", 10); // "doc_aX4p9Qr2mN"

// Factory for repeated use
const userId = createPrefixedGenerator("usr");
userId(); // "usr_K7gkJ_q3vR2nL8xH5eM0w"
userId(); // "usr_Xp9mN2qL5vR8nK3eJ7cHw"

Prefix rules:

  • Must start with a letter
  • Must contain only letters and digits
  • Separator is always _

Throws TypeError for invalid prefixes. Throws RangeError for invalid lengths.


sigilid/typed — branded TypeScript ID types

import { createTypedGenerator, castId } from "sigilid/typed";
import type { IdOf, Brand } from "sigilid/typed";

// Define typed generators for your entities
const userId = createTypedGenerator<"User">("usr");
const postId = createTypedGenerator<"Post">("post");

const uid = userId(); // IdOf<"User"> = "usr_K7gkJ_q3vR2nL8xH5eM0w"
const pid = postId(); // IdOf<"Post">

// TypeScript prevents mixing them up
function getUser(id: IdOf<"User">) {
  /* ... */
}
getUser(uid); // ✓
getUser(pid); // ✗ type error

// Cast an untyped string at a trust boundary
const fromDb = castId<"User">(row.user_id);

// Unprefixed typed ID
const tokenGen = createTypedGenerator<"Token">();
const token = tokenGen(); // IdOf<"Token">, no prefix

Brand<T, B> and IdOf<T> are pure type-level utilities — no runtime cost.


sigilid/validate — validation helpers

import { isValidId, assertValidId, parseId } from "sigilid/validate";
import type { ValidationOptions } from "sigilid/validate";

// Boolean check
isValidId("K7gkJ_q3vR2nL8xH5eM0w"); // true
isValidId("bad id!"); // false
isValidId("usr_K7gkJ_q3vR2nL8xH5eM0w", { prefix: "usr" }); // true
isValidId("abc123", { length: 6, alphabet: "abc123def456" }); // true

// Throws TypeError if invalid — good for API boundaries
assertValidId(req.params.id);
assertValidId(req.params.id, { prefix: "usr" });

// Returns the value if valid, throws if not — useful in pipelines
const id = parseId(rawInput);
const id = parseId(rawInput, { prefix: "usr", length: 21 });

ValidationOptions:

Option Type Description
length number Expected length of the ID (or ID portion after prefix)
prefix string Expected prefix; separator _ is assumed
alphabet string Characters the ID must be drawn from (defaults to DEFAULT_ALPHABET)

sigilid/alphabet — custom alphabets

import { createAlphabet, validateAlphabet } from "sigilid/alphabet";

// Validate first (optional — createAlphabet validates internally)
validateAlphabet("0123456789abcdef");

// Create a bound generator
const hex = createAlphabet("0123456789abcdef");
hex.generate(); // 21-character hex string
hex.generate(32); // 32-character hex string

// Binary IDs (contrived, but works)
const binary = createAlphabet("01");
binary.generate(16); // "1010011001110101"

createAlphabet throws immediately if:

  • the alphabet has fewer than 2 characters
  • the alphabet has more than 256 characters
  • the alphabet contains duplicate characters

generate(length?) uses rejection sampling to avoid modulo bias.


Built for real TypeScript apps

Branded types in practice

If you have multiple entity ID types in your codebase, the TypeScript compiler can silently allow you to pass a userId where a postId is expected — both are just string. Branded types close that gap.

import { createTypedGenerator } from "sigilid/typed";
import type { IdOf } from "sigilid/typed";

const newUserId = createTypedGenerator<"User">("usr");
const newPostId = createTypedGenerator<"Post">("post");

type UserId = IdOf<"User">;
type PostId = IdOf<"Post">;

// Your service functions now accept precise types
async function deletePost(postId: PostId) {
  /* ... */
}
async function getUser(userId: UserId) {
  /* ... */
}

const uid = newUserId();
const pid = newPostId();

deletePost(pid); // ✓
deletePost(uid); // ✗ Argument of type 'IdOf<"User">' is not assignable to 'IdOf<"Post">'

Validation at the edge

import { parseId } from "sigilid/validate";

// In an Express/Hono/Fastify handler
app.get("/users/:id", (req, res) => {
  const id = parseId(req.params.id, { prefix: "usr" });
  // id is a plain string, validated — throws before reaching your service
});

Tree-shaking

Because each subpath is a separate bundle with no cross-imports, bundlers like Vite, esbuild, and webpack can eliminate unused entrypoints entirely. An app that imports only generateId will not include any prefix, validation, or alphabet code.


Why not just use Nano ID?

Nano ID is excellent. If all you need is the smallest possible secure random string generator, it may still be the right call — it has a longer track record and an even smaller core.

sigilid is worth considering if:

  • You want prefixed IDs and typed IDs in the same package
  • You want validation helpers that know about your ID format
  • You want stricter TypeScript ergonomics out of the box
  • You want a single library that handles the full ID lifecycle

If you are already using Nano ID and are happy with it, there is no compelling reason to switch just for the root generateId function — the behavior is similar. The subpath ecosystem is where sigilid earns its place.


Runtime and environment notes

sigilid uses globalThis.crypto.getRandomValues, which is available in:

  • Node.js 20+ (stable, no flags required)
  • All modern browsers
  • Edge runtimes: Cloudflare Workers, Vercel Edge, Deno, Bun

If you are targeting an environment without Web Crypto, use sigilid/non-secure with the understanding that Math.random is not cryptographically safe.

sigilid/native is a separate Node-only path. It depends on the companion addon package and is not intended for browsers or edge runtimes.


Benchmarking

For local performance checks, run:

npm ci
npm run build
npm run bench

Native vs JS benchmark:

npm run build:native-addon
npm run bench:native

Build prebuilt binaries for publishing the addon:

npm run prebuild:native-addon

Package exports

Import Entry file Description
sigilid dist/index.js Secure root generator
sigilid/native dist/native.js Optional Node-only native fast path
sigilid/non-secure dist/non-secure.js Math.random-based generator
sigilid/prefix dist/prefix.js Prefixed ID helpers
sigilid/typed dist/typed.js Branded types and typed generators
sigilid/validate dist/validate.js Validation helpers
sigilid/alphabet dist/alphabet.js Custom alphabet factory

All exports are ESM (.js) with TypeScript declarations (.d.ts). Node.js 20+ required.


Contributing

Contributions are welcome. See CONTRIBUTING.md for setup instructions, coding standards, and PR expectations.

See ARCHITECTURE.md for an explanation of the design decisions and constraints contributors should keep in mind.


Release and versioning

sigilid uses Semantic Versioning. Breaking API changes will bump the major version. Releases are cut from GitHub — bump the version in package.json, tag the release, and the publish workflow handles the rest.


License

MIT — see LICENSE.