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. The assets directory 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.Registry key declare the body-shape options that decide what an ID looks like, not just :size. Previously monotonic: true could 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 a generate!/2 call could silently disagree:
    • defid :event, prefix: "evt", size: :small, monotonic: true - true, false, or a list of sizes, bypassing the global policy
    • defid :session, prefix: "ses", compact_time: true - 40 rather than 48 timestamp bits, moving the freed byte into the random field
    • defid :ticket, prefix: "tkt", rand_size: 4 - an explicit random-byte count, overriding the width implied by :size
    • Each flows into both generate!/2 and field_opts/1, so an autogenerate: true field mints the shape the key declares; unset means "defer to the global configuration", exactly as UXID.generate!/1 does, so declaring nothing changes nothing
    • A call site can still override all three for a one-off (generate!(:event, monotonic: false)) - unlike :prefix and :size they are defaults, not pins
    • Registry-wide default_monotonic: and default_compact_time: join default_size:/default_validate:
    • Adds a generated monotonic/1 accessor alongside size/1 and category/1
  • Breaking (compile-time only): adds validation of the shape options - an unknown :size (or a size named in a :monotonic list) is now a compile error rather than falling through to :xlarge and silently minting the wrong shape, as are a non-boolean :compact_time and a negative :rand_size. A key declaring both deterministic: true and monotonic: true is 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, and rand_size (null when the key defers to the global config), so a Postgres function or JS client can reproduce the body - compact_time in 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 shipped from: but the registry's generated generate!/1 was arity-1 and dropped caller options, so a registry user had to drop to the plain API and hand-assemble prefix/size:
    • Generated generate!/2 and generate/2 merge caller options over the registry's, so MyApp.IDs.generate!(:export, from: natural_key) mints deterministically by key; existing arity-1 calls are unchanged
    • :prefix and :size belong 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: true declares a key as always derived: minting it without from: 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 with from:
    • deterministic joins 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 autogenerate remains unwired for from: (no per-row input exists), and the declaration cannot reach it - a deterministic: true key must be minted in a changeset, not with autogenerate: true
  • 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 defid option is now a compile error instead of being silently ignored (defid :org, prefix: "org", validat: false previously defaulted validate to true with 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 on generate/1, generate!/1, and new/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/Z is 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/1 reports deterministic: true with time: nil; adds UXID.deterministic?/1
    • from: requires a string (raises otherwise); from: with an explicit monotonic: true raises (deterministic vs. burst-random); Ecto autogenerate is intentionally not wired for from: (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.Registry can live at an app's base layer without a schema: literal inverting the dependency direction:
    • New UXID.Registered mixin (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/xref stay clean
    • MyApp.IDs.verify!(otp_apps: [...]) / build_routes!/1 assemble 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!/1 also validates it - raising on a marker that names an unregistered key, two modules claiming one key, or a route: true key left unmapped
    • Call verify!/1 from your top app's start/2 so every boot (prod, dev, CI's mix test) re-verifies - no bespoke CI job needed
    • schema_for/1 now resolves a compile-time schema: literal first, then the self-registration table, so flat apps are unchanged and layered apps opt in per entry with route: true
    • Adds prefixes/0 (convenience for app-side "every schema draws from the registry" conformance tests) and routes/0
  • 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 via use UXID.Registry with defid/retired:
    • Compile-time uniqueness (active and retired prefixes) 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/retired as paren-free locals and exports the rule for consuming apps
  • Adds opt-in monotonic generation via the monotonic option (per-call/per-field) or config :uxid, :monotonic global policy:
    • Accepts true/false, or a list of sizes (alias-aware, e.g. [:small] matches both :small and :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: true safe, 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/:xsmall auto-enable compact_time so there is a field to increment; explicit compact_time: false on those sizes with monotonic on raises
    • Wire format is byte-identical to a random UXID - no decoder changes

2.4.0 / 2026-07-09

  • Adds UXID.valid?/2 for structural validation of a UXID string (optional :prefix and :delimiter)
  • Adds opt-in strict casting for the Ecto type via validate: true on 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 uuidtext column migrations); disable with allow_uuid: false
    • Default casting is unchanged (any binary passes) when validate is not set - fully backwards compatible
  • 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_times config automatically compacts :xs/:xsmall/:s/:small sizes
    • Per-call override via compact_time: true/false option 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

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!