ExCuid2

Hex.pmHex Docs

An Elixir implementation of CUID2 (Collision-Resistant Unique Identifiers).

ExCuid2 generates secure, collision-resistant unique identifiers designed for efficiency and horizontal scaling. They are an excellent choice for primary keys in distributed databases.

Features

Installation

The package is available in Hex and can be installed by adding ex_cuid2 to your list of dependencies in mix.exs:

def deps do
[
{:ex_cuid2, "~> 0.9.2"}
]
end

Usage

Generating IDs

You can generate a CUID2 with the default length (24) or specify a custom length.

# Generate a default CUID2
iex> ExCuid2.generate()
"v8p7k3f9z1m0c2x4b6n5j7h8"
# Generate a CUID2 with a custom length (e.g., 30)
iex> ExCuid2.generate(30)
"b5n6m4j3h2g1f0d9s8a7q6w5e4r3t2"

Validating a CUID2

You can check if a given string conforms to the CUID2 format using is_valid?/1. It performs a check based on length and character set and returns false for any non-binary input without raising an error.

iex> id = ExCuid2.generate()
"t9p7k3f9z1m0c2x4b6n5j7h8"
iex> ExCuid2.is_valid?(id)
true
# --- Invalid Cases ---
# Too short
iex> ExCuid2.is_valid?("a123")
false
# Starts with a number
iex> ExCuid2.is_valid?("1abcdefghijklmnopqrstuvw")
false
# Contains invalid characters (uppercase)
iex> ExCuid2.is_valid?("aBcdefghijklmnopqrstuvwX")
false
# Wrong data type
iex> ExCuid2.is_valid?(12345)
false

Ecto Integration

ExCuid2 provides an optional Ecto.Type module for seamless integration with your Ecto schemas.

1. Configure Your Repo

First, register ExCuid2.Ecto.Type as a custom type in your application's configuration (config/config.exs).

# in config/config.exs
config :my_app, MyApp.Repo,
ecto_types: [cuid2: ExCuid2.Ecto.Type]

(Replace :my_app and MyApp.Repo with your application's name and Repo module.)

2. Use in Your Schema

It's recommended to use it for your primary key with autogenerate: true and to set the @foreign_key_type.

defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
# Define the primary key as a CUID2
@primary_key {:id, ExCuid2.Ecto.Type, autogenerate: true}
@foreign_key_type ExCuid2.Ecto.Type
schema "users" do
field :name, :string
field :email, :string
timestamps()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email])
|> validate_required([:name, :email])
end
end

You could use this : @primary_key {:id, ExCuid2.Ecto.Type, autogenerate: 32} if you use a cuid2 longer.

With this setup, Ecto will automatically generate a new CUID2 for the :id field whenever you insert a new record, giving you secure and scalable primary keys out of the box.

defmodule MyApp.Accounts.Migrations.CreateAccount do
use Ecto.Migration
def change do
# primary_key: false is not needed here since we are defining a custom primary key
create table(:record, primary_key: false) do
# we use char beacuse cuid2 has a fixed length.
add :id, :char, primary_key: true, size: 24 # check the cuid2 length
add :title, :string
add :body, :string
timestamps(type: :utc_datetime)
end
end

Optional Custom Postgres Domain

defmodule MyApp.Repo.Migrations.CreateCuid2DomainType do
use Ecto.Migration
# Create a custom domain type for cuid2
# This is a PostgreSQL specific feature, so ensure your database supports it.
# The cuid2 format is a 24-32 character string starting with a letter followed by lowercase alphanumeric characters.
# The regex checks that the value starts with a letter and is followed by 23 lowercase alphanumeric characters.
# Adjust the regex as necessary to fit your specific requirements.
# Note: The size of 24 is based on the standard CUID2 length.
# If you are using a different length, adjust the size accordingly.
def up do
execute("""
CREATE DOMAIN cuid2 AS character(24)
CONSTRAINT cuid2_check CHECK (VALUE ~ '^[a-z][a-z0-9]{23}$');
""")
end
def down do
execute("DROP DOMAIN cuid2;")
end
end

After this migration you can use this :

defmodule MyApp.Repo.Migrations.CreateRecord do
use Ecto.Migration
def change do
create table(:record, primary_key: false) do
# add :id, :char, primary_key: true, size: 24
add :id, :cuid2, primary_key: true
add :title, :string
add :body, :string
timestamps(type: :utc_datetime)
end
end
end

Advanced Usage: Supervision

For production applications, you should run ExCuid2 under your supervision tree. This ensures the counter Agent is started and managed correctly by OTP.

1. Add to your Supervisor

Add ExCuid2 as a child in your application.ex file.

# in application.ex
def start(_type, _args) do
children = [
# ... other children
ExCuid2
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end

When started this way, ExCuid2 will automatically use a supervised, named counter (:cuid2_counter). You can then call ExCuid2.generate() from anywhere in your application.

2. Using Multiple Counters

If you need multiple independent counters (for example, to handle different domains of IDs), you can start and supervise multiple named workers.

# in application.ex
children = [
# ...
{ExCuid2, name: :user_id_generator},
{ExCuid2, name: :post_id_generator}
]

You can then generate IDs by passing the name of the counter process.

# Generate an ID for a new user
ExCuid2.generate(24, :user_id_generator)
# Generate an ID for a new blog post
ExCuid2.generate(24, :post_id_generator)