UXID.Registry (UXID v2.5.0)
View SourceA compile-time registry of an application's UXID prefixes.
Prefixes only deliver their value — "the ID names its resource on sight" — if
they are globally unique and well-formed across an app. use UXID.Registry
moves that governance out of hand-rolled CI tests and into the compiler, and
turns the same declarations into a runtime routing table (prefix → schema)
that powers authorization checks, admin auto-linking, and Relay-style global
object identification.
Declaring a registry
defmodule MyApp.IDs do
use UXID.Registry,
prefix_format: ~r/^[a-z][a-z0-9_]{1,7}$/,
delimiter: "_",
default_size: :medium,
default_validate: true
defid :org, prefix: "org", schema: MyApp.Org, category: :account
defid :contact, prefix: "contact", size: :large, schema: MyApp.CRM.Contact, allow_uuid: true
defid :lead, prefix: "lead", schema: MyApp.CRM.Lead
retired "usr" # reserve a prefix so it counts for uniqueness, never reused
endCompile-time guarantees
- every
:prefixis checked against:prefix_format— a malformed prefix is a compile error; - all prefixes (active and
retired) are checked for uniqueness — a duplicate is a compile error. This replaces the per-app runtime uniqueness test.
Generated API
By key (minting and schema configuration):
MyApp.IDs.generate!(:org) # => "org_01h…"
MyApp.IDs.prefix(:org) # => "org"
MyApp.IDs.size(:org) # => :medium
MyApp.IDs.schema(:org) # => MyApp.Org
MyApp.IDs.field_opts(:org) # => [prefix: "org", size: :medium, validate: true, allow_uuid: true, delimiter: "_"]
MyApp.IDs.all() # => [%{key: :org, prefix: "org", ...}, ...]Cross-source single source of truth — emit a JSON manifest so a database function or a mobile/JS generator mints prefixes the same way the app does:
MyApp.IDs.manifest() # => [%{"key" => "org", "prefix" => "org", "size" => "medium", ...}]
MyApp.IDs.manifest_json() # => ~s([{"key":"org","prefix":"org","size":"medium","category":"account"},...])field_opts/1 is the single-source-of-truth hook — a schema spreads it instead
of restating anything:
@primary_key {:id, UXID, [autogenerate: true] ++ MyApp.IDs.field_opts(:org)}By ID string (runtime routing — see "Parsing" below):
MyApp.IDs.known?("org_01h…") # => true (cheap prefix-only membership)
MyApp.IDs.key_for("org_01h…") # => :org
MyApp.IDs.schema_for("org_01h…") # => MyApp.Org
MyApp.IDs.resolve("org_01h…") # => %{key: :org, schema: MyApp.Org, ...}Parsing
Lookups split an ID into {prefix, body} on the last delimiter. This is
unambiguous without consulting the registry, because a UXID body is Crockford
Base32 and therefore never contains the delimiter: in in_ref_01h… every _
belongs to the prefix except the final joining one. The registry is consulted
only after the split, to answer membership/type — an unregistered but
well-formed string like nope_01h… parses fine yet resolves to nil.
Because of that, the :delimiter must be a character that cannot appear in a
Base32 body ("_" or "-"); a letter or digit is rejected at compile time.
An underscore is preferred for compound prefixes since it does not break
double-click-to-select-the-whole-id.
Summary
Functions
The built-in default :prefix_format (permits an internal underscore).
Registers an id under key. Requires :prefix; :size, :schema,
:category, :validate, and :allow_uuid are optional and fall back to the
registry defaults.
Reserves a prefix so it participates in the uniqueness check but is never generated — mirroring "identifiers are never reused". Accepts a string or atom.
Splits a UXID string into {prefix, body} on the last occurrence of
delimiter. Returns {nil, string} when the delimiter is absent.
Functions
The built-in default :prefix_format (permits an internal underscore).
Registers an id under key. Requires :prefix; :size, :schema,
:category, :validate, and :allow_uuid are optional and fall back to the
registry defaults.
Reserves a prefix so it participates in the uniqueness check but is never generated — mirroring "identifiers are never reused". Accepts a string or atom.
Splits a UXID string into {prefix, body} on the last occurrence of
delimiter. Returns {nil, string} when the delimiter is absent.
iex> UXID.Registry.split_last("in_ref_01h2x…", "_")
{"in_ref", "01h2x…"}
iex> UXID.Registry.split_last("01h2x…", "_")
{nil, "01h2x…"}