A tiny, tree-shakeable ID toolkit for TypeScript apps.
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"- Cryptographically secure — uses
crypto.getRandomValues, notMath.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 path —
sigilid/nativefor 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 entrypoints —
install sigilid, then import only what you need - Companion native addon package — only needed when using
sigilid/native
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.
npm install sigilidpnpm add sigilidyarn add sigilidNode 20+ required. Works in all modern runtimes that expose the Web Crypto API (globalThis.crypto).
Optional native path:
npm install sigilid @sigilid/native-addonUse @sigilid/native-addon only if you plan to import sigilid/native.
import { generateId } from "sigilid";
// Default: 21 URL-safe characters using crypto.getRandomValues
generateId(); // "K7gkJ_q3vR2nL8xH5eM0w"
generateId(12); // "aX4_p9Qr2mNs"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.
| 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.
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.
import { generateNonSecureId } from "sigilid/non-secure";
generateNonSecureId(); // 21-character ID using Math.random
generateNonSecureId(8); // 8-character IDNot 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.
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-gypbuild. - Publishing note:
sigilidand@sigilid/native-addonare versioned/published separately.
If you want the broadest compatibility (browser, edge, and Node) stick to the root
sigilid import.
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.
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 prefixBrand<T, B> and IdOf<T> are pure type-level utilities — no runtime cost.
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) |
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.
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">'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
});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.
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.
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.
For local performance checks, run:
npm ci
npm run build
npm run benchNative vs JS benchmark:
npm run build:native-addon
npm run bench:nativeBuild prebuilt binaries for publishing the addon:
npm run prebuild:native-addon| 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.
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.
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.
MIT — see LICENSE.