UXID (UXID v2.7.0)

View Source

Generates UXIDs and acts as an Ecto ParameterizedType

User eXperience focused IDentifiers (UXIDs) are identifiers which:

  • Describe the resource (aid in debugging and investigation)
  • Work well with copy and paste (double clicking selects the entire ID)
  • Can be shortened for low cardinality resources
  • Are secure against enumeration attacks
  • Can be generated by application code (not tied to the datastore)
  • Are K-sortable (lexicographically sortable by time - works well with datastore indexing)
  • Do not require any coordination (human or automated) at startup, or generation
  • Are very unlikely to collide (more likely with less randomness)
  • Are easily and accurately transmitted to another human using a telephone
  • Can optionally be deterministic (name-based) — the same input always maps to the same ID

Many of the concepts of Stripe IDs have been used in this library.

Deterministic IDs

Passing from: to generate/1, generate!/1, or new/1 produces a deterministic (name-based) ID: the same input string always maps to the same ID (UUIDv5-style), with the prefix acting as the namespace. Deterministic IDs carry a leading z/Z marker and sort after time-based IDs. They are a hash of a known input, so they are not unguessable — see generate/1 and the Deterministic IDs guide for the full picture.

Summary

Types

Options for generating a UXID

t()

A UXID represented as a String

Functions

Generates a loaded version of the UXID.

Casts the given input to the UXID ParameterizedType with the given parameters.

Returns whether compact time encoding is enabled for small sizes. When true, :xs/:xsmall and :s/:small use 40 bits for timestamp instead of 48, adding 8 bits to randomness. This is a global policy that can be overridden per-call with the compact_time option. This provides perfect 5-bit Crockford Base32 alignment (8 chars vs 10 chars). K-sortability is maintained until ~mid-2038 for compact mode: value 31 (the first char z/Z) is reserved as the deterministic-ID scheme marker, so compact encoding raises for timestamps at/after that band. Standard 48-bit timestamps are unaffected.

Decodes a UXID string and returns a Codec struct with extracted components.

Returns the delimiter used to separate a prefix from the encoded body. Defaults to "_" and can be overridden globally with the :delimiter application env, or per-call/per-field with the :delimiter option.

Returns true if the given value is a deterministic (name-based) UXID.

Dumps the given term into an Ecto native type.

Dictates how the type should be treated if embedded. For UXIDs, we use :self since they're already strings.

Checks if two UXIDs are equal.

Returns an encoded UXID string along with response status.

Returns an unwrapped encoded UXID string.

Converts the options specified in the field macro into parameters to be used in other callbacks.

Loads the given term into a UXID.

Returns the minimum size configuration. When set, any requested size smaller than this will be upgraded.

Returns the global monotonic generation policy.

Returns a new UXID.Codec struct. This is useful for development.

Returns the underlying schema type for a UXID.

Returns true if the given value is a structurally valid UXID.

Types

option()

@type option() ::
  {:case, atom()}
  | {:time, integer()}
  | {:size, atom() | nil}
  | {:rand_size, integer() | nil}
  | {:prefix, String.t() | nil}
  | {:delimiter, String.t() | nil}
  | {:compact_time, boolean() | nil}
  | {:monotonic, boolean() | [atom()] | nil}
  | {:from, String.t() | nil}

Options for generating a UXID

options()

@type options() :: [option()]

t()

@type t() :: String.t()

A UXID represented as a String

Functions

autogenerate(opts)

Generates a loaded version of the UXID.

Field options are threaded through to generate!/1, including monotonic for opt-in monotonic generation:

field :id, UXID, autogenerate: true, prefix: "evt", size: :small, monotonic: true

Deterministic (from:) IDs are intentionally not wired here — there is no per-row input available at autogenerate time. Mint them explicitly in application code (e.g. in a changeset via generate!/1) and store/cast the result as an ordinary string.

cast(data, params)

Casts the given input to the UXID ParameterizedType with the given parameters.

By default any binary is accepted unchanged (backwards compatible). When the field opts in with validate: true, the value must be either a structurally valid UXID carrying the field's configured :prefix (see valid?/2) or a legacy bare UUID string; anything else casts to :error. UUID coexistence can be turned off with allow_uuid: false.

field :owner_org_id, UXID, prefix: "org", validate: true

compact_small_times()

Returns whether compact time encoding is enabled for small sizes. When true, :xs/:xsmall and :s/:small use 40 bits for timestamp instead of 48, adding 8 bits to randomness. This is a global policy that can be overridden per-call with the compact_time option. This provides perfect 5-bit Crockford Base32 alignment (8 chars vs 10 chars). K-sortability is maintained until ~mid-2038 for compact mode: value 31 (the first char z/Z) is reserved as the deterministic-ID scheme marker, so compact encoding raises for timestamps at/after that band. Standard 48-bit timestamps are unaffected.

decode(uxid)

@spec decode(String.t()) :: {:ok, UXID.Codec.t()}

Decodes a UXID string and returns a Codec struct with extracted components.

default_delimiter()

Returns the delimiter used to separate a prefix from the encoded body. Defaults to "_" and can be overridden globally with the :delimiter application env, or per-call/per-field with the :delimiter option.

deterministic?(term, opts \\ [])

@spec deterministic?(
  term(),
  keyword()
) :: boolean()

Returns true if the given value is a deterministic (name-based) UXID.

Deterministic IDs carry a leading z/Z scheme marker (Crockford value 31) instead of an encoded timestamp, so this only inspects the first body character — it does not otherwise validate structure (pair it with valid?/2 if you need both). Time-based IDs start with a lower value and return false.

Options

  • :delimiter - the prefix delimiter (defaults to the configured delimiter, see default_delimiter/0).

Examples

iex> UXID.deterministic?("usr_z9m4k7p2q3r8t5v6w0abcde")
true

iex> UXID.deterministic?("cus_01emdgjf0dqxqj8fm78xe97y3h")
false

iex> UXID.deterministic?("nope!")
false

dump(data, dumper, params)

Dumps the given term into an Ecto native type.

embed_as(format, params)

Dictates how the type should be treated if embedded. For UXIDs, we use :self since they're already strings.

encode_case()

equal?(left, right, params)

Checks if two UXIDs are equal.

generate(opts \\ [])

@spec generate(opts :: options()) :: {:ok, t()}

Returns an encoded UXID string along with response status.

Deterministic (name-based) IDs

Passing from: switches to deterministic mode: the same input string always maps to the same ID (UUIDv5-style), across processes and machines, forever. The prefix is folded in as the namespace, so the same string under two prefixes yields two different bodies. from: must be a string (raises otherwise).

Deterministic IDs are marked with a leading z/Z and sort after every time-based ID; they are not K-sortable among themselves. They are a hash of a known input, so they are exactly as guessable as that input — do not derive an ID from a low-entropy secret and treat the ID as unguessable.

{:ok, id} = UXID.generate(prefix: "usr", from: "alice@example.com")
# {:ok, "usr_z…"} — identical for this input on every call

generate!(opts \\ [])

@spec generate!(opts :: options()) :: t()

Returns an unwrapped encoded UXID string.

Passing from: returns a deterministic (name-based) ID: the same input always maps to the same ID, with the prefix acting as the namespace. See generate/1 for the full deterministic-mode notes.

UXID.generate!(prefix: "usr", from: "alice@example.com")
# => "usr_z…" (stable for this input, forever)

init(opts)

Converts the options specified in the field macro into parameters to be used in other callbacks.

load(data, loader, params)

Loads the given term into a UXID.

min_size()

Returns the minimum size configuration. When set, any requested size smaller than this will be upgraded.

monotonic()

Returns the global monotonic generation policy.

When active, the random field is treated as a big-endian counter: the first ID in a millisecond is seeded from the CSPRNG and each subsequent same-ms ID advances it by a random positive step (per process, per prefix). This guarantees uniqueness and K-sortability within a burst while keeping consecutive IDs within a bounded window ahead — a mitigation, not cryptographic unpredictability — which is why it is off by default and opt-in per resource.

Accepts true/false, or a list of sizes (alias-aware, e.g. [:small] matches both :small and :s). Overridable per-call/per-field with the monotonic option. See UXID.Monotonic.

new(opts \\ [])

@spec new(opts :: options()) :: {:ok, UXID.Codec.t()}

Returns a new UXID.Codec struct. This is useful for development.

type(opts)

Returns the underlying schema type for a UXID.

valid?(term, opts \\ [])

@spec valid?(
  term(),
  keyword()
) :: boolean()

Returns true if the given value is a structurally valid UXID.

A valid UXID is a binary made up of an optional prefix, the delimiter, and a Crockford Base32 body of at least 8 characters. This checks structure, not authenticity: it cannot distinguish a generated UXID from an arbitrary Base32 string of the same shape, and it deliberately does not accept a bare UUID (see cast/2's :validate mode for UUID coexistence).

Options

  • :prefix - when present, the value must carry exactly this prefix. When omitted, any prefix (or none) is accepted.
  • :delimiter - the prefix delimiter (defaults to the configured delimiter, see default_delimiter/0).

Examples

iex> UXID.valid?("cus_01emdgjf0dqxqj8fm78xe97y3h")
true

iex> UXID.valid?("cus_01emdgjf0dqxqj8fm78xe97y3h", prefix: "cus")
true

iex> UXID.valid?("cus_01emdgjf0dqxqj8fm78xe97y3h", prefix: "usr")
false

iex> UXID.valid?("nope!")
false