UXID.Registry (UXID v2.5.0)

View Source

A 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
end

Compile-time guarantees

  • every :prefix is 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

default_prefix_format()

The built-in default :prefix_format (permits an internal underscore).

defid(key, opts \\ [])

(macro)

Registers an id under key. Requires :prefix; :size, :schema, :category, :validate, and :allow_uuid are optional and fall back to the registry defaults.

retired(prefix)

(macro)

Reserves a prefix so it participates in the uniqueness check but is never generated — mirroring "identifiers are never reused". Accepts a string or atom.

split_last(string, delimiter)

@spec split_last(String.t(), String.t()) :: {String.t() | nil, String.t()}

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…"}