Skip to content

ID format ​

One format for every primary ID in Agentfy, Bonsite, Securify, Shiffty and Store Deputy (owner decision 2026-10-15). This page is the reference: the other four products copy the module, the test vector and the table below from here.

<prefix>-<adjective>-<noun>-<suffix>
user-calm-rivers-k8m4p2d7n9x3

New IDs are minted only in this canonical form. The earlier format of each product is still accepted for IDs already issued (LEGACY_ID_FORMATS), never minted. The AGNT2-588 prefix-only migration is the exception: organization-* becomes org-*, preserving the entire tail. It does not change the word/suffix format.

The format ​

PartRule
prefixThe whole model name in kebab-case, with the word Organization shortened to org (OrganizationMember → org-member, AgentMcpServer → agent-mcp-server), lower case, at most 32 characters, may itself contain -. Registry: ID_ENTITIES in api/src/slices/admin/idMigration/data/idInventory.ts.
adjectiveOne word from adjectives of human-id 4.1.3 — 200 words.
nounOne word from nouns of human-id 4.1.3 — 300 words. No verb, no adverb, no dictionary of our own.
suffixExactly 12 characters of lower-case Crockford base32: 0123456789abcdefghjkmnpqrstvwxyz (no i, l, o, u). 60 bits.
randomnessCryptographic: Node crypto.randomBytes by default. Words by masked rejection; the suffix by nanoid 3.3.11 customRandom.
uniquenessThe primary key. On a collision the whole write is repeated, at most 5 times (withIdRetry). Words never count as uniqueness.
lengthAt most 32 + 1 + 7 + 1 + 8 + 1 + 12 = 62 characters. Any accepted value, legacy included, is at most 80 (ID_MAX_LENGTH).

Dictionaries are the arrays the package exports, in the package's own order, lower-cased; a word with a character outside [a-z] would be dropped at load (4.1.3 has none). Dependencies are pinned exactly, without ^: "human-id": "4.1.3", "nanoid": "3.3.11" (the CommonJS line).

Note: the shared task text uses user-calm-river-… as its example; river is not in human-id (rivers is), so that literal string is not a valid ID.

Generation — exact algorithm ​

The reference, byte for byte, in every product (fixed by the coordinator from SHIF-28, Agentfy/shiffty#37). formatId(prefix, random = crypto.randomBytes) reads from a source random(n) that returns n bytes, in this order:

  1. adjective — index(200)
  2. noun — index(300)
  3. suffix — nanoid 3.3.11 customRandom(ID_SUFFIX_ALPHABET, 12, random)() with the same source

index(n) takes the fewest whole bytes that hold n − 1, reads them big-endian, masks the value to the bit length of n − 1, and draws again while the value is not below n:

nbitsbytes per trymaskaccepted when
200 (adjectives)81255value < 200
300 (nouns)92511(b0·256 + b1) & 511 < 300

nanoid's customRandom for a 32-letter alphabet and size 12 asks for ceil(1.6 × 31 × 12 / 32) = 19 bytes in one request, reads them from the last one back, keeps byte & 31 of each (every value is in range), and stops after 12 letters; the first 7 bytes of the request are unused.

Control test vector ​

A source that hands out bytes 0, 1, 2, 3, … in order (each call continues the count) gives:

formatId('user', counter) === 'user-afraid-swans-nmkjhgfedcba'

Byte 0 → adjectives[0] = afraid; bytes 1, 2 → 258 & 511 = 258 → nouns[258] = swans; one request of bytes 3…21, read from 21 down to 10 → letters 21…10 of the alphabet = nmkjhgfedcba. Exactly 22 bytes are read. Every product's suite asserts this string; a different result means a different dictionary, order, alphabet or draw — not a valid copy.

Validation ​

isCanonicalId(value, prefix?) accepts exactly

^(<registry prefixes, longest first, escaped>)-<adjective>-<noun>-[0-9abcdefghjkmnpqrstvwxyz]{12}$

with both words checked against the dictionaries (not just [a-z]+). prefix may be one prefix, a list, or omitted (any well-formed prefix). There is no normalisation: upper case, Unicode look-alikes, percent encoding and whitespace are refused. Because the number of trailing segments is fixed, a hyphenated prefix is never ambiguous: team-api-key-… is never a team ID.

isLegacyId(value, prefix?) accepts the product's LEGACY_ID_FORMATS. isId(value, prefix) — what DTO validators (@IsEntityId), pipes and gateways use — is isCanonicalId || isLegacyId. Validation never looks anything up and never grants access.

Refused as canonical (tested in every product): a third word (verb), a 6- or 20-character suffix, a suffix of another length of hex, i/l/o/u in the suffix, a word outside the dictionaries — unless the value matches that product's legacy format. Twelve hex digits are a subset of the Crockford alphabet, so …-a1b2c3d4e5f6 is canonical; that cannot and need not be told apart.

The module ​

api/src/slices/setup/id/domain/idFormat.ts — the same content in all five repositories, except LEGACY_ID_FORMATS. Exports:

Export
ID_SUFFIX_ALPHABET, ID_SUFFIX_LENGTH = 12the suffix
ID_WORDS { adjectives, nouns }the dictionaries, as loaded
formatId(prefix, random?)the only way a new ID is made
isCanonicalId(value, prefix?), isLegacyId(value, prefix?), isId(value, prefix)validation
LEGACY_ID_FORMATS, legacyIdFormat(value, prefix?)the product's earlier formats
assertIdPrefix, idPrefixOf, idPattern, ID_MAX_LENGTHprefix checks, OpenAPI pattern

The prefix registry stays where each product keeps it. Pure TypeScript (no enums or parameter properties), so Node can load it natively.

Legacy formats by product ​

ProductLEGACY_ID_FORMATS (accepted, never minted)
Agentfy<prefix>-<adjective>-<noun>-<verb>-<20 Crockford> — human-id 4.1.3 adjective/noun/verb, rows made on dev after AGNT2-535/537; a bare lower-case UUID — historical rows the AGNT2-535 cutover still has to translate
Bonsite3 words + 20 Crockford
Securify2 words + 12 hex (indistinguishable from canonical — see above)
Shiffty2 words from its own dictionary + 6 Crockford
Store Deputy2 words + 12 [a-z0-9]

Agentfy: the cutover ​

Dev completed the original cutover in AGNT2-580. AGNT2-588 (owner decision 2026-10-24) supersedes the Organization part of AGNT2-537: organization, organization-member, organization-credential and organization-policy become org, org-member, org-credential and org-policy. OrganizationAudit uses a technical UUID and is not minted by the prefix registry.

The maintenance command handles both a fresh legacy database (directly to the current prefixes) and an already migrated database (prefix-only hops preserving the tail). The platform ID is org-silver-days-zxy8s9bpp6hp. No HTTP aliases are created; old organization addresses are not found. Existing provider references resolve through the recorded migration chain. Historical audit data, mapping hops and opaque user/partner content remain historical; they are not live IDs to mint.

Stop writers, back up and restore-check before using api/scripts/id-migrate.ts. The same transaction updates references, encryption ownership and defaults and revokes sessions/signed contexts. Repeat runs change zero identities. Run the cutover before seeding an existing local database; seed refuses old IDs instead of duplicating organizations. See specs/AGNT2-535-id/transition.md and the AGNT2-588 report for environment-specific execution evidence.