UXID
User eXperience focused IDentifiers for Elixir: prefixed, K-sortable,
Stripe-style IDs a person can read, copy and route back to their resource, with
optional Ecto types and a prefix registry. An ID looks like
usr_01epey2p06tr1rtv07xa82zgjj: the prefix names the resource, and the body
carries a timestamp and randomness in lowercase Crockford Base32.
Why UXID
An auto-increment key leaks how many rows a table holds and invites enumeration; a random UUID fixes both but is long, unordered and anonymous, so in a log line, a URL or a support thread nobody can tell what it points at, and a double-click selects only part of it. A UXID names its resource in its prefix, selects whole on a double-click, reads aloud without ambiguity, sorts by creation time so it indexes well, and is generated in the application with no coordination between nodes. Its size is tunable for low-cardinality resources, a monotonic mode keeps a burst within one millisecond unique and ordered, a deterministic mode maps the same input to the same ID, and a registry keeps every prefix in an app unique and routes an ID back to its resource. Why a UXID has a prefix, a time and randomness explains the trade-offs behind these properties.
Install
Add uxid to your dependencies in mix.exs:
def deps do
[
{:uxid, "~> 2.9"}
]
end
Ecto is an optional dependency: UXID only uses it if your app already does.
Basic usage
# No options generates a plain ULID
UXID.generate!() # "01emdgjf0dqxqj8fm78xe97y3h"
# Add a prefix to name the resource
UXID.generate!(prefix: "cus") # "cus_01emdgjf0dqxqj8fm78xe97y3h"
# Shrink the random part for low-cardinality resources
# T-shirt sizes: :xs :s :m :l :xl (or :xsmall :small :medium :large :xlarge)
UXID.generate!(prefix: "cus", size: :small) # "cus_01eqrh884aqyy1"
# Deterministic: same input -> same id, forever (prefix is the namespace)
UXID.generate!(prefix: "usr", from: "alice@example.com")
# => "usr_zcvt7epac0t1ebcsjfyf7cwz25"
# As an Ecto field type, primary keys included, with the same options
defmodule YourApp.User do
use Ecto.Schema
@primary_key {:id, UXID, autogenerate: true, prefix: "usr", size: :medium}
schema "users" do
field :api_key, UXID, autogenerate: true, prefix: "apikey", size: :small
end
end
Documentation
- Do
- How to use UXIDs in Ecto schemas: primary and foreign keys, strict
validate:casting,allow_uuidcoexistence,UXID.valid?/2, and migrating auuidcolumn. - How to govern prefixes with a registry: the compile-time DSL that keeps every prefix unique, routes an ID back to its schema, works in layered and umbrella apps, and exports a JSON manifest.
- How to use UXIDs in Ecto schemas: primary and foreign keys, strict
- Look up
- The API reference: every public module and function.
- Sizes & Encoding: the t-shirt sizes, how much randomness each carries, and compact-time mode.
- Configuration: every
config :uxidkey in one place, with per-call and global precedence. - The changelog: what changed in each version.
- Understand
- Why a UXID has a prefix, a time and randomness: what each part of an ID is for, and what you trade when you tune it.
- Monotonic IDs: same-millisecond uniqueness and ordering, the security tradeoff, and when to use it.
- Deterministic IDs: name-based (UUIDv5-style) IDs, with the prefix as the namespace.
- Designing APIs for humans: object IDs: the Stripe ID design many of UXID's choices follow.
- UXIDs in Elixir/Ecto: Adam Kirk's ElixirConf US 2025 talk, the source of the registry and routing patterns.
Compatibility
mix.exs declares elixir: "~> 1.8". CI runs the full quality gate on the
toolchain mise.toml pins and the test suite alone on Elixir 1.16 / OTP 25,
the oldest pair it is tested on. There is no required runtime dependency. Ecto
is optional ({:ecto, "~> 3.12"}): when it is loaded, UXID is also an
Ecto.ParameterizedType; without it, everything but the Ecto type works the
same.
License
UXID is released under the MIT License.