Sigra ships a complete, enumeration-safe registration flow: a LiveView form, a context function, Argon2id password hashing, and a confirmation email. This guide covers the happy path, common customizations, the confirmation lifecycle, and how to test it.
MyAppWeb.UserRegistrationLive— generated LiveView that renders the form and submits toAccounts.register_user/2. You own it; edit the template freely.MyApp.Accounts.register_user/2— the context function that callsSigra.Auth.register/3with the generated changeset. This is where you add custom side effects (welcome email, analytics, billing signup).MyApp.Accounts.User.registration_changeset/2— the Ecto changeset. Validates email format, password length, password complexity, and email uniqueness. Hashes the password viaSigra.Crypto.hash_password/1onprepare_changes.MyApp.Accounts.deliver_user_confirmation_instructions/2— sends a signed, 24-hour TTL confirmation token to the user's email via the configured mailer.Sigra.Auth.register/3— the library-level primitive. Handles audit logging, telemetry spans, and the enumeration-safe{:error, :email_taken}return shape.
The generator produces this in lib/my_app/accounts.ex:
def register_user(attrs, opts \\ []) do
Sigra.Auth.register(Repo, attrs,
changeset_fn: &User.registration_changeset(%User{}, &1),
audit_schema: AuditEvent
)
end
And this in lib/my_app_web/live/user_registration_live.ex:
def handle_event("save", %{"user" => user_params}, socket) do
case Accounts.register_user(user_params) do
{:ok, user} ->
{:ok, _} =
Accounts.deliver_user_confirmation_instructions(
user,
&url(~p"/users/confirm/#{&1}")
)
{:noreply,
socket
|> put_flash(:info, "Check your email to confirm your account.")
|> push_navigate(to: ~p"/users/log-in")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
That's the whole flow. Hashing, validation, enumeration prevention, and audit logging are all handled for you.
Sigra.Auth.register/3 returns:
{:ok, user}— success{:error, %Ecto.Changeset{}}— validation error (format, length, confirmation mismatch){:error, :email_taken}— the email is already registered
Important: render the same message for :email_taken as for "email was sent" if an attacker can see the response. A common pattern is to show "If an account exists, we sent a confirmation email" on both branches. Or, accept the tradeoff and show "Email is already taken" — enumeration is mitigated by rate limiting.
By default, users can log in before confirming their email. To enforce confirmation, set require_confirmation: true in your Sigra config:
# config/config.exs
config :my_app, MyApp.Auth.Config,
repo: MyApp.Repo,
user_schema: MyApp.Accounts.User,
require_confirmation: true
Now UserAuth.log_in_user/3 rejects unconfirmed users with {:error, :unconfirmed} and the login form shows "Please confirm your email before logging in."
Add custom validations by extending User.registration_changeset/2:
def registration_changeset(user, attrs) do
user
|> cast(attrs, [:email, :password, :display_name])
|> validate_required([:display_name])
|> validate_length(:display_name, min: 2, max: 50)
|> Sigra.User.registration_changeset(attrs) # email + password validation
end
The Sigra.User.registration_changeset/2 helper applies the standard email format, password complexity, and uniqueness validation. Your custom validations run before or after it — your choice.
Hook into register_user/2 in your Accounts context:
def register_user(attrs, opts \\ []) do
case Sigra.Auth.register(Repo, attrs, changeset_fn: &User.registration_changeset(%User{}, &1)) do
{:ok, user} = result ->
# Welcome email is separate from confirmation email
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
MyApp.Mailer.deliver_welcome_email(user)
end)
result
error ->
error
end
end
For guaranteed delivery, use Oban instead of Task.Supervisor:
MyApp.Workers.SendWelcomeEmail.new(%{user_id: user.id}) |> Oban.insert()
test "registers a user with a hashed password" do
attrs = %{"email" => "alice@example.com", "password" => "supersecret12"}
assert {:ok, user} = Accounts.register_user(attrs)
assert user.email == "alice@example.com"
Sigra.Testing.assert_password_hashed(user)
end
test "sends a confirmation email" do
{:ok, user} = Accounts.register_user(%{"email" => "bob@example.com", "password" => "supersecret12"})
Accounts.deliver_user_confirmation_instructions(user, &"/confirm/#{&1}")
Sigra.Testing.assert_email_sent(to: "bob@example.com", subject: "Confirm")
end
See Testing Auth Flows for fixtures, scenario setup, and the full Sigra.Testing helper list.
- Getting Started — full walkthrough from install to working auth.
- Login and Logout — what happens after registration.
- Account Lifecycle — email change, password change, deletion.
Sigra.Auth— library-level primitives.Sigra.Crypto— password hashing and verification.