Email

Better Auth Ruby does not send email directly. Configure Ruby callables that receive email payloads and the Rack request.

Email Verification

To use one-time-password based verification, use the email OTP plugin. This page covers link-based verification.

Adding Email Verification To Your App

config/auth.rb
auth = BetterAuth.auth(
  base_url: "https://app.example.com/api/auth",
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  email_and_password: {
    enabled: true
  },
  email_verification: {
    send_verification_email: lambda do |data, request|
      AuthMailer.with(
        user: data.fetch(:user),
        url: data.fetch(:url),
        token: data.fetch(:token),
        ip: request&.ip
      ).verify_email.deliver_later
    end
  }
)

The payload includes :user, :url, and :token.

Triggering Email Verification

1. During Sign-Up

Send a verification email when a user signs up:

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  email_and_password: {enabled: true},
  email_verification: {
    send_on_sign_up: true,
    send_verification_email: ->(data, _request) { AuthMailer.verify_email(data).deliver_later }
  }
)

When email_and_password.require_email_verification is true, sign-up also sends the verification email.

2. Require Email Verification

Require users to verify before email/password sign-in succeeds:

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  email_and_password: {
    enabled: true,
    require_email_verification: true
  },
  email_verification: {
    send_on_sign_in: true,
    send_verification_email: ->(data, _request) { AuthMailer.verify_email(data).deliver_later }
  }
)

With send_on_sign_in, Better Auth sends a fresh verification link when an unverified user attempts to sign in.

3. Manually

auth.api.send_verification_email(
  body: {
    email: "john@example.com",
    callbackURL: "/dashboard"
  }
)

If the user exists and is unverified, the configured email callback is called. Missing users return the same success response to avoid account enumeration.

Verifying The Email

Verification links point to /verify-email.

auth.api.verify_email(
  query: {
    token: params[:token],
    callbackURL: "/dashboard"
  }
)

The endpoint marks the user email as verified and redirects to callbackURL when provided.

Auto Sign In After Verification

Set auto_sign_in_after_verification to create a session cookie after verification.

email_verification: {
  auto_sign_in_after_verification: true,
  send_verification_email: ->(data, _request) { AuthMailer.verify_email(data).deliver_later }
}

When calling server-side, use as_response: true or return_headers: true to forward the Set-Cookie header.

Callback Before Email Verification

Run custom logic before a user is marked as verified.

email_verification: {
  before_email_verification: lambda do |user, request|
    raise BetterAuth::APIError.new("BAD_REQUEST", message: "domain blocked") if user["email"].end_with?("@blocked.test")
  end
}

Callback After Successful Email Verification

Run side effects after verification succeeds.

email_verification: {
  after_email_verification: lambda do |user, request|
    Analytics.track("email_verified", user_id: user["id"])
  end
}

Ruby also supports on_email_verification during the verification flow.

Callback On Duplicate Sign-Up Attempt

Use on_existing_user_sign_up to react when someone attempts to sign up with an existing email.

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  email_and_password: {
    enabled: true,
    on_existing_user_sign_up: lambda do |data, request|
      SecurityMailer.existing_signup(data.fetch(:user)).deliver_later
    end
  }
)

Password Reset Email

Configure send_reset_password for password reset links.

auth = BetterAuth.auth(
  base_url: "https://app.example.com/api/auth",
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  email_and_password: {
    enabled: true,
    send_reset_password: lambda do |data, request|
      AuthMailer.with(
        user: data.fetch(:user),
        url: data.fetch(:url),
        token: data.fetch(:token)
      ).reset_password.deliver_later
    end,
    on_password_reset: lambda do |user, request|
      AuditLog.create!(user_id: user["id"], action: "password_reset")
    end,
    revoke_sessions_on_password_reset: true
  }
)

Request a reset email:

auth.api.forget_password(
  body: {
    email: "john@example.com",
    redirectTo: "/reset-password"
  }
)

Reset the password:

auth.api.reset_password(
  body: {
    token: params[:token],
    newPassword: "new-password123"
  }
)