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.
- Collision-Resistant: Uses multiple entropy sources to minimize the probability of collisions, even in high-concurrency systems.
-
Secure: Starts with a random letter to prevent enumeration attacks and uses
:crypto.strong_rand_bytes
for cryptographically secure entropy. - Scalable: Includes a process fingerprint to ensure uniqueness across different nodes and application restarts.
-
Efficient: Implemented with a stateful
Agent
to manage an atomic counter quickly and safely. - Customizable: Allows generating IDs with a length between 24 and 32 characters.
- Supervisable: Can be added directly to your application's supervision tree.
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
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"
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
ExCuid2
provides an optional Ecto.Type
module for seamless integration with your Ecto schemas.
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.)
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
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
For production applications, you should run ExCuid2
under your supervision tree. This ensures the counter Agent
is started and managed correctly by OTP.
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.
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)