Standard UXIDs are random: calling generate!/1 twice with the same options
gives two different IDs. Deterministic mode is the opposite - the same input
string always maps to the same ID, forever, across processes and machines.
This is what a name-based UUID (UUIDv5) gives you, adapted to the UXID format.
Reach for it when you want a stable ID derived from an external key without a lookup table: an email, a vendor SKU, a webhook idempotency key, or a natural key during a migration. It makes idempotent upserts trivial (recompute the ID instead of querying for it) and gives you stable fixtures and seeds in tests.
You switch it on by passing from: with the input string; the option is in
The from: option. A registry
key can require it: see Deterministic keys
in the registry guide.
Prefix is the namespace
The prefix is folded into the hash as the namespace, so the same string under two
prefixes produces two unrelated bodies - usr + "alice@example.com" and org
"alice@example.com"never collide:
UXID.generate!(prefix: "usr", from: "alice@example.com") # "usr_zcvt7epac..."
UXID.generate!(prefix: "org", from: "alice@example.com") # "org_zes4c99fn..." (unrelated)Most callers pass just prefix + from and get correct scoping with zero extra
config. (There is no separate namespace: option; the prefix does that job. No
prefix at all hashes into the global scope.)
The z marker and sort order
A deterministic body always starts with z (or Z in upper case) - Crockford
value 31, the maximum symbol. It does two jobs:
- Self-identifying - a human or the decoder can see at a glance that the ID
is hash-derived, not time-derived.
UXID.deterministic?/1reports it, anddecode/1returnsdeterministic: truewithtime: nil. - Sorts last - because
zis the highest value, every deterministic ID sorts after every time-based ID. They cluster at the end of an index instead of interleaving with (and polluting) the K-sortable time range. Among themselves they sort in no meaningful order.
To keep z an unambiguous marker, compact-time encoding reserves value 31: a
compact timestamp can no longer start with z, which trims the compact horizon
to ~mid-2038 (standard 48-bit timestamps are unaffected). See the
Sizes & Encoding guide.
The body length and hash width at each size are in Size and length.
Properties, honestly
- Deterministic & idempotent - same
{namespace, name, size, case}→ same ID, everywhere, forever. - Not time-ordered - hash order is effectively random-but-stable. These sort after time IDs and among themselves in no meaningful order, so do not rely on them for K-sortability.
- Not a secret - a deterministic ID is a hash of a known input, so it is exactly as guessable as that input. Anyone who knows the namespace + name can recompute the ID. Do not derive an ID from a low-entropy secret and treat the ID as unguessable. (This is the same caveat RFC 4122 gives for name-based UUIDs.)
Deterministic IDs are not unguessable
If you need an identifier an attacker cannot guess, use a random UXID (:large
or :xl), not a deterministic one. from: is for stability, not secrecy.
Minting one into an Ecto field, which autogenerate cannot do, is in
Minting a deterministic ID in a changeset.