{"id":51170165,"url":"https://github.com/smonn/ids","last_synced_at":"2026-06-27T00:01:02.980Z","repository":{"id":361730340,"uuid":"1255535249","full_name":"smonn/ids","owner":"smonn","description":"Public-facing branded IDs for TypeScript apps.","archived":false,"fork":false,"pushed_at":"2026-06-20T04:22:41.000Z","size":319,"stargazers_count":1,"open_issues_count":8,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-20T04:23:05.078Z","etag":null,"topics":["base32","ids","typescript","ulid"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/smonn.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-01T00:03:29.000Z","updated_at":"2026-06-20T04:06:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/smonn/ids","commit_stats":null,"previous_names":["smonn/ids"],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/smonn/ids","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smonn%2Fids","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smonn%2Fids/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smonn%2Fids/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smonn%2Fids/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smonn","download_url":"https://codeload.github.com/smonn/ids/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smonn%2Fids/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34835785,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-26T02:00:06.560Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["base32","ids","typescript","ulid"],"created_at":"2026-06-27T00:00:35.962Z","updated_at":"2026-06-27T00:01:02.969Z","avatar_url":"https://github.com/smonn.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @smonn/ids\n\nPublic-facing branded IDs for TypeScript apps. Type-safe, sortable, and codec-pluggable.\n\n📖 **Full documentation \u0026 interactive playground: [ids.smonn.se](https://ids.smonn.se)**\n\n```bash\npnpm add @smonn/ids\n```\n\nEach ID looks like `usr_06f80z92d2dbsqqg28t5cy4tqg`: a three-letter brand, an underscore, then 26 Crockford base32 characters of payload. The default Timestamp codec encodes a 48-bit millisecond Unix timestamp followed by 80 random bits — the same byte layout as a [ULID](https://github.com/ulid/spec).\n\n## Quickstart\n\n```ts\nimport { type Id, createTimestampId } from \"@smonn/ids\";\n\nconst users = createTimestampId(\"usr\");\n\n// Generate — sortable by creation time via ORDER BY id\nconst id = users.generate(); // \"usr_06f80z92d2dbsqqg28t5cy4tqg\"\n\n// Branded: Id\u003c\"usr\"\u003e and Id\u003c\"org\"\u003e are not interchangeable\nfunction loadUser(id: Id\u003c\"usr\"\u003e) {\n  /* ... */\n}\n\n// Validate untrusted input — lenient in, canonical out\nconst r = users.safeParse(\"USR_06F80Z92D2DBSQQG28T5CY4TQG\");\nif (r.ok) {\n  r.id; // \"usr_06f80z92d2dbsqqg28t5cy4tqg\" as Id\u003c\"usr\"\u003e\n}\n```\n\n`safeParse` accepts mixed case and the Crockford visual aliases (`o → 0`, `i → 1`, `l → 1`) and always returns the canonical lowercase form. See the [Timestamp codec guide](https://ids.smonn.se/codecs/timestamp/) for sorting, backfills (`generateAt`), range queries, structured errors, Standard Schema, and JSON Schema.\n\n## Choosing a codec\n\nAll six codecs share the same `\u003cbrand\u003e_\u003c26 chars\u003e` wire shape but make different trade-offs. They are wire-indistinguishable — `safeParse`, `is`, and `parse` cannot distinguish an Opaque Timestamp ID from a Timestamp ID at runtime. Cross-codec confusion is undetectable by the library; the consumer is responsible for routing a given ID to the correct codec for the brand. Codec choice is therefore a per-brand commitment.\n\n| Codec | Import | Sort direction | Key required | Timestamp extractable |\n| --- | --- | --- | --- | --- |\n| Timestamp | `@smonn/ids` | Ascending (oldest-first) | No | Always (plaintext) |\n| Reverse Timestamp | `@smonn/ids/reverse` | Descending (newest-first) | No | Always (plaintext) |\n| Signed Timestamp | `@smonn/ids/signed` | Ascending (oldest-first) | Yes (signing key) | Always (plaintext) |\n| Opaque Timestamp | `@smonn/ids/opaque` | None (encrypted) | Yes (key material) | With key only |\n| Wrapped key | `@smonn/ids/wrapped` | None | Yes (wrapping key) | N/A — not timestamp-family |\n| Digest | `@smonn/ids/digest` | None | Yes (digest key) | N/A — not timestamp-family |\n\nThe Timestamp codec is the default and ships from the root `@smonn/ids` entry — it has no `/timestamp` subpath by design. If you try `import ... from \"@smonn/ids/timestamp\"` you will get a module-resolution error; use `@smonn/ids` directly. Every other codec uses a named subpath (`/reverse`, `/signed`, `/opaque`, `/wrapped`, `/digest`); this asymmetry is intentional and permanent.\n\n- **Newest-first scans** on forward-only KV stores → [Reverse Timestamp](https://ids.smonn.se/codecs/reverse/)\n- **Tamper-evident share links** verified without a DB lookup → [Signed Timestamp](https://ids.smonn.se/codecs/signed/) (integrity)\n- **IDs that must not leak creation time** → [Opaque Timestamp](https://ids.smonn.se/codecs/opaque/) (confidentiality)\n- **A public handle for an internal integer PK** → [Wrapped key](https://ids.smonn.se/codecs/wrapped/)\n- **Idempotency keys, content-addressed records, or stable public pseudonyms** → [Digest](https://ids.smonn.se/codecs/digest/)\n\nTry them all live in the [playground](https://ids.smonn.se/playground/).\n\n## Integrations\n\nFramework and ORM adapters ship as optional subpath exports (each requires its own peer dependency):\n\n- **HTTP params:** [Hono](https://ids.smonn.se/adapters/hono/), [Express](https://ids.smonn.se/adapters/express/), [Fastify](https://ids.smonn.se/adapters/fastify/) — `idParam`, `idQuery` middleware; [NestJS](https://ids.smonn.se/adapters/nestjs/) — `ParseIdPipe`\n- **ORM columns:** [Drizzle](https://ids.smonn.se/adapters/drizzle/) — `idColumn`, `idColumnMysql`, `idColumnSqlite`, `nullableIdColumn`, [Kysely](https://ids.smonn.se/adapters/kysely/) — `idPlugin` / `idColumn`, `nullableIdColumn`, [MikroORM](https://ids.smonn.se/adapters/mikro-orm/) — `idType`, `nullableIdType`, `idField`, [Prisma](https://ids.smonn.se/adapters/prisma/) — `idField`, `nullableIdField`, [TypeORM](https://ids.smonn.se/adapters/typeorm/) — `idTransformer`, `nullableIdTransformer`\n- **GraphQL:** [GraphQL](https://ids.smonn.se/adapters/graphql/) — `idScalar` custom scalar\n- **CLI:** brand-agnostic `inspect` / `generate` / `keygen` — `npx @smonn/ids --help` ([docs](https://ids.smonn.se/cli/))\n\nEvery codec also implements [Standard Schema v1](https://standardschema.dev/), so it slots into Zod, Valibot, ArkType, tRPC, and any validator-aware library.\n\n**Native `uuid` column storage:** an `Id\u003cBrand\u003e` can be persisted into a native `uuid` column via `codec.toUUID(id)` and read back as a branded ID via `codec.fromUUID(value)` or `codec.safeFromUUID(value)` — a lossless round-trip useful when migrating off UUID primary keys while keeping existing column types and indexes.\n\n## What this is **not** for\n\n- **Internal surrogate primary keys.** If nobody outside your service sees the ID, the brand prefix and lenient parsing are dead weight. Use a `bigint` sequence.\n- **Wire-compatible ULIDs.** The byte layout is ULID-shaped, but the encoding is lowercase and brand-wrapped. Stock ULID parsers will reject these.\n- **Spec-valid UUIDv7 output.** `toUUID` produces a **raw, unversioned** UUID — all 128 payload bits are preserved verbatim (lossless round-trip), which means the version/variant nibble positions hold real data, not `0x7`/`0b10`. It is **not** a spec-valid UUIDv7. Only the Timestamp codec happens to produce a UUID whose leading 48 bits are a real millisecond timestamp; only Timestamp and Reverse Timestamp produce time-sortable UUIDs. Importing a non-time-ordered UUID (e.g. a UUIDv4) into a timestamp-family codec via `fromUUID` yields a structurally valid `Id\u003cBrand\u003e` with a meaningless timestamp and random sort order — the same wire-indistinguishable contract that already governs codec variants.\n- **Distributed-trace / request-correlation IDs.** Use OpenTelemetry-format IDs.\n- **Hiding creation time with the Timestamp codec.** Anyone with one ID at a known creation time can compute the epoch offset. Use the Opaque Timestamp codec to hide creation time per-ID.\n\n## API surface\n\nExports from the main `@smonn/ids` entry point only. Codec-specific subpath exports (`@smonn/ids/reverse`, `@smonn/ids/opaque`, `@smonn/ids/signed`, `@smonn/ids/wrapped`, `@smonn/ids/digest`) and adapter subpaths are not listed here.\n\n### Types\n\n- `Id\u003cBrand\u003e` — Canonical branded ID string for `Brand`; produced by `generate()` and `safeParse()`.\n- `ParseError` — Parse failure reason string returned by `safeParse()` (`\"not_string\"`, `\"invalid_prefix\"`, or `\"invalid_base32\"`) and by `safeFromUUID()` (`\"not_string\"` or `\"invalid_uuid\"`).\n- `ParseResult\u003cBrand\u003e` — Discriminated union returned by `safeParse()`: `{ ok: true; id: Id\u003cBrand\u003e }` or `{ ok: false; error: ParseError }`.\n- `JsonSchema` — Shape of the object returned by a codec's `toJsonSchema()`.\n- `IdsErrorCode` — String-literal union of the eleven stable error codes carried by `IdsError`.\n- `TimestampCodec\u003cBrand\u003e` — Interface of a brand-scoped Timestamp codec instance returned by `createTimestampId()`.\n- `TimestampOptions` — Construction options for `createTimestampId()`: `now`, `rng`, and `allowDuplicateBrand`.\n- `ValidBrand\u003cS\u003e` — Compile-time validation that `S` is a well-formed brand (three lowercase `a–z` characters); intersect it with a codec constructor's brand parameter (`brand: Brand \u0026 ValidBrand\u003cBrand\u003e`) to reject malformed brands at the type level.\n\n### Classes\n\n- `IdsError` — Single error class thrown by caller-reachable failures; carries a stable `code: IdsErrorCode`. Use `isIdsError()` rather than `instanceof` to detect across realms.\n\n### Functions\n\n- `isIdsError(value)` — Type guard for `IdsError`; uses an internal brand to survive ESM/CJS dual-package duplication where bare `instanceof` fails.\n- `createTimestampId(brand, options?)` — Creates a Timestamp codec for `brand` (three lowercase `a–z` characters).\n\n### Shared codec methods (all variants)\n\nEvery codec instance exposes the following UUID interop methods in addition to the codec-specific ones documented on the [full docs site](https://ids.smonn.se):\n\n- `toUUID(id)` — Takes a trusted `Id\u003cBrand\u003e`, returns the 16-byte payload reinterpreted as a canonical lowercase-hyphenated UUID `string`. Total — cannot fail. The brand is shed; the output is a plain `string`, not a branded type.\n- `fromUUID(value)` — Takes an untrusted `string`, returns `Id\u003cBrand\u003e`. Throws `IdsError` (`code: \"invalid_id\"`) with a `ParseError` on `cause` (`\"invalid_uuid\"`, or `\"not_string\"` for untyped JavaScript callers) on malformed input.\n- `safeFromUUID(value)` — Takes `unknown`, returns `ParseResult\u003cBrand\u003e` (`{ ok: true; id }` or `{ ok: false; error: ParseError }`). Never throws.\n\n## Links\n\n- **[Documentation](https://ids.smonn.se)** — full guides, API reference, and playground\n- **[SPEC.md](./SPEC.md)** — descriptive wire-format specification\n- **[Design decisions](./docs/adr/)** — recorded ADRs\n- **[CONTEXT.md](./CONTEXT.md)** — glossary of the project's vocabulary\n- **[Contributing](./CONTRIBUTING.md)** · **[Security](./SECURITY.md)**\n\n## License\n\n[MIT](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmonn%2Fids","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmonn%2Fids","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmonn%2Fids/lists"}