ex_cuid2

A robust implementation of Cuid2 for Elixir


Keywords
cuid2, elixir, phoenix-framework
License
MIT

Documentation

ExCuid2

Hex.pm Hex 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

  • 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.

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)