2.9.1 / 2026-08-24
- Adds CI and HexDocs badges to the README, and bumps the docs generator (
ex_doc, dev-only) to~> 0.40 - Adds a UXID wordmark at
assets/uxid-wordmark.svg, shown at the top of the README. Theassetsdirectory ships in the Hex package and is copied into the generated docs, so the mark renders on GitHub, hex.pm, and HexDocs alike
2.9.0 / 2026-07-30
- Lets a
UXID.Registrykey declare the body-shape options that decide what an ID looks like, not just:size. Previouslymonotonic: truecould only be set per call or per Ecto field, so the one place meant to be the single source of truth couldn't express "this key is monotonic" - and a schema field and agenerate!/2call could silently disagree:defid :event, prefix: "evt", size: :small, monotonic: true-true,false, or a list of sizes, bypassing the global policydefid :session, prefix: "ses", compact_time: true- 40 rather than 48 timestamp bits, moving the freed byte into the random fielddefid :ticket, prefix: "tkt", rand_size: 4- an explicit random-byte count, overriding the width implied by:size- Each flows into both
generate!/2andfield_opts/1, so anautogenerate: truefield mints the shape the key declares; unset means "defer to the global configuration", exactly asUXID.generate!/1does, so declaring nothing changes nothing - A call site can still override all three for a one-off (
generate!(:event, monotonic: false)) - unlike:prefixand:sizethey are defaults, not pins - Registry-wide
default_monotonic:anddefault_compact_time:joindefault_size:/default_validate: - Adds a generated
monotonic/1accessor alongsidesize/1andcategory/1
- Breaking (compile-time only): adds validation of the shape options - an unknown
:size(or a size named in a:monotoniclist) is now a compile error rather than falling through to:xlargeand silently minting the wrong shape, as are a non-boolean:compact_timeand a negative:rand_size. A key declaring bothdeterministic: trueandmonotonic: trueis rejected too - the pair can never mint, since one asks for a stable hash and the other for burst-random bits - The JSON manifest carries
monotonic,compact_time, andrand_size(nullwhen the key defers to the global config), so a Postgres function or JS client can reproduce the body -compact_timein particular changes the encoded length (8 timestamp characters rather than 10)
2.8.0 / 2026-07-26
- Makes deterministic minting reachable through
UXID.Registry- 2.7.0 shippedfrom:but the registry's generatedgenerate!/1was arity-1 and dropped caller options, so a registry user had to drop to the plain API and hand-assemble prefix/size:- Generated
generate!/2andgenerate/2merge caller options over the registry's, soMyApp.IDs.generate!(:export, from: natural_key)mints deterministically by key; existing arity-1 calls are unchanged :prefixand:sizebelong to the key and raise if overridden - the registry's contract is that a key determines its shape; the intended override surface is:from,:case,:monotonic,:compact_time,:rand_size- New
defid :export, deterministic: truedeclares a key as always derived: minting it withoutfrom:raises, so one call site cannot derive while another mints randomly. It is a requirement, not a permission - an undeclared key may still be minted withfrom: deterministicjoins the JSON manifest (as a real boolean) so a Postgres function or JS client knows which scheme a key uses; the hash rule itself still comes from the deterministic guide- Ecto
autogenerateremains unwired forfrom:(no per-row input exists), and the declaration cannot reach it - adeterministic: truekey must be minted in a changeset, not withautogenerate: true
- Generated
- Adds
defid :enrichment, legacy: <term>- opaque app metadata with no library behavior, so a "deliberately still on the old scheme, retrofit deferred" backlog lives in the registry rather than a moduledoc - Breaking: an unrecognized
defidoption is now a compile error instead of being silently ignored (defid :org, prefix: "org", validat: falsepreviously defaultedvalidatetotruewith no warning) - a sharp edge for a registry whose selling point is compile-time checking
2.7.0 / 2026-07-23
- Adds opt-in deterministic (name-based) IDs via the
from:option ongenerate/1,generate!/1, andnew/1- the same input string always maps to the same ID (UUIDv5-style), across processes and machines:- Body is a truncated SHA-256 of the input (SHA-256, not the deprecated SHA-1 of real UUIDv5, since no wire interop is needed)
- The prefix is the namespace (folded into the hash), so the same string under two prefixes yields two unrelated bodies; no separate
namespace:option - Marked with a leading
z/Z(Crockford value 31): self-identifying and sorts after every time-based ID; not K-sortable among themselves - Reserves value 31 in compact-time encoding so
z/Zis an unambiguous scheme marker - compact timestamps now raise past ~mid-2038 (standard 48-bit timestamps unaffected) - Deterministic bodies reuse the standard (non-compact) lengths per
:size, spending the whole body (minus the marker) on hash bits (45-125 bits) decode/1reportsdeterministic: truewithtime: nil; addsUXID.deterministic?/1from:requires a string (raises otherwise);from:with an explicitmonotonic: trueraises (deterministic vs. burst-random); Ectoautogenerateis intentionally not wired forfrom:(mint explicitly in a changeset)- Not a secret: a hash of a known input is exactly as guessable as the input - do not derive an ID from a low-entropy secret and treat it as unguessable
2.6.0 / 2026-07-17
- Adds runtime prefix → schema routing for layered apps, so a
UXID.Registrycan live at an app's base layer without aschema:literal inverting the dependency direction:- New
UXID.Registeredmixin (use UXID.Registered, key: :contact) marks a schema under a registry key; the reference points down (schema names a key), so the base-layer registry never references an upper-layer module -Boundary/xrefstay clean MyApp.IDs.verify!(otp_apps: [...])/build_routes!/1assemble the routing table at boot by scanning the given apps for the marker (via reflection, no compile-visible module reference) and store it in:persistent_term;verify!/1also validates it - raising on a marker that names an unregistered key, two modules claiming one key, or aroute: truekey left unmapped- Call
verify!/1from your top app'sstart/2so every boot (prod, dev, CI'smix test) re-verifies - no bespoke CI job needed schema_for/1now resolves a compile-timeschema:literal first, then the self-registration table, so flat apps are unchanged and layered apps opt in per entry withroute: true- Adds
prefixes/0(convenience for app-side "every schema draws from the registry" conformance tests) androutes/0
- New
- Restructures the documentation: a slimmed README plus dedicated guides (Sizes & Encoding, Ecto Integration, Monotonic IDs, Prefix Registry, Configuration) that render on both GitHub and HexDocs
2.5.0 / 2026-07-16
- Adds
UXID.Registry, an opt-in compile-time DSL for an app's prefixes viause UXID.Registrywithdefid/retired:- Compile-time uniqueness (active and
retiredprefixes) and prefix-format checks, replacing per-app CI tests - By-key API:
generate!/1,generate/1,prefix/1,size/1,schema/1,category/1,field_opts/1,all/0,keys/0,reserved/0 - By-ID-string routing (prefix → schema):
known?/1,key_for/1,schema_for/1,resolve/1; parsing splits on the last delimiter (unambiguous because a Base32 body never contains it), and the configurable delimiter is validated as Base32-disjoint (defaults to_) - JSON manifest export (
manifest/0,manifest_json/0) so database functions and mobile/JS clients mint prefixes from the same source of truth, with no added dependency - Registers
defid/retiredas paren-free locals and exports the rule for consuming apps
- Compile-time uniqueness (active and
- Adds opt-in monotonic generation via the
monotonicoption (per-call/per-field) orconfig :uxid, :monotonicglobal policy:- Accepts
true/false, or a list of sizes (alias-aware, e.g.[:small]matches both:smalland:s) - Within a millisecond the random field is seeded once then advanced by a random positive step (uniform over
[1, 2^(bits/2)], drawn from the CSPRNG), guaranteeing uniqueness and K-sortability for a burst - process-local,async: truesafe, no GenServer/ETS - Per-call option takes precedence over the global policy; off by default (consecutive IDs stay in a bounded window - a mitigation, not cryptographic unpredictability - weakening enumeration resistance)
:xs/:xsmallauto-enablecompact_timeso there is a field to increment; explicitcompact_time: falseon those sizes with monotonic on raises- Wire format is byte-identical to a random UXID - no decoder changes
- Accepts
2.4.0 / 2026-07-09
- Adds
UXID.valid?/2for structural validation of a UXID string (optional:prefixand:delimiter) - Adds opt-in strict casting for the Ecto type via
validate: trueon a field:- Accepts a well-formed UXID carrying the field's configured
:prefix, or a legacy bare UUID string - Rejects malformed values (empty, wrong prefix, non-Base32) with
:error - UUID coexistence is on by default (eases
uuid→textcolumn migrations); disable withallow_uuid: false - Default casting is unchanged (any binary passes) when
validateis not set - fully backwards compatible
- Accepts a well-formed UXID carrying the field's configured
- Adds default_delimiter config accessor
2.3.0 / 2026-01-16
- Adds min_size config option to enforce minimum UXID sizes (useful for test environments)
- Adds compact_time feature for improved collision resistance in small UXIDs:
- Global policy via
compact_small_timesconfig automatically compacts :xs/:xsmall/:s/:small sizes - Per-call override via
compact_time: true/falseoption works for any size - Uses 40-bit timestamps (8 chars) instead of 48-bit (10 chars), freeing 8 bits for randomness
- Example: :small gains 50% more randomness (24 bits vs 16 bits)
- Decoder automatically detects compact format from length and reconstructs full timestamp
- K-sortability maintained until ~September 2039
- Global policy via
2.2.0 / 2025-09-12
- Adds UXID.Decoder module with full pipeline processing and uppercase/lowercase support
2.1.0 / 2025-09-10
- Adds delimiter option (default is '_')
- Adds UXID.Codec with encoding struct and type
- Fixes Dialyzer issues
- Changes UXID type to String.t() to work better with TypedEctoSchema
- Changes how Ecto.Paramaterized type is implemented to work in projects without Ecto
2.0.0 / 2025-04-27
Breaking Changes
- Adds case config and functionality
- Makes lowercase the default
- Removes deprecated Ecto.UXID
1.0.0 / 2025-04-13
- Fixes compiler warnings
0.2.3 / 2020-11-29
- Uses new project URL
0.2.2 / 2020-11-26
- Passes options cleanly down for autogenerate
0.2.1 / 2020-11-24
- Updates Mix project description
0.2.0 / 2020-11-22
- Deprecates Ecto.UXID
- Removes Decoder and CrockfordBase32
- Adds size option (T-Shirt sizes)
0.1.2 / 2020-11-07
- Updates description and README
0.1.1 / 2020-11-07
- Updates description and README
0.1.0 / 2020-11-06
- Updates description and README
- Has enough usable functionality and documentation for 0.1.0!
0.0.6 / 2020-10-30
- Marks Ecto as optional dependency and excludes xref
0.0.5 / 2020-10-19
- Adds Ecto.UXID
0.0.4 / 2020-10-19
- Includes Erlang crypto application
0.0.3 / 2020-10-12
- Fixes documentation
0.0.2 / 2020-10-11
- Adds generation of UXIDs
0.0.1 / 2020-06-17
- Birthday!