Block disposable email signups in Phoenix
Add a custom Ecto changeset validation. The check runs as part of `cast → validate` chain so disposable emails surface as standard `changeset.errors`.
The code
# lib/myapp/disposable_email.ex
defmodule MyApp.DisposableEmail do
@url "https://api.checkdisposable.email/v1/check"
def disposable?(email) when is_binary(email) do
key = System.fetch_env!("CDE_KEY")
url = "#{@url}?email=#{URI.encode_www_form(email)}"
case Req.get(url, headers: [{"authorization", "Bearer #{key}"}], receive_timeout: 3_000) do
{:ok, %{status: 200, body: %{"is_disposable" => true}}} -> true
_ -> false # fail open on any error
end
end
end
# lib/myapp/accounts/user.ex
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
alias MyApp.DisposableEmail
schema "users" do
field :email, :string
field :hashed_password, :string, redact: true
timestamps()
end
def registration_changeset(user, attrs) do
user
|> cast(attrs, [:email])
|> validate_required([:email])
|> validate_format(:email, ~r/@/)
|> validate_not_disposable()
|> unique_constraint(:email)
end
defp validate_not_disposable(changeset) do
case get_change(changeset, :email) do
nil -> changeset
email ->
if DisposableEmail.disposable?(email),
do: add_error(changeset, :email, "is not allowed — please use a real address"),
else: changeset
end
end
endNotes
- Req over HTTPoison
- Req is the modern HTTP client in the Elixir ecosystem — built on Finch, with sane defaults. Add `{:req, "~> 0.5"}` to mix.exs.
- phx.gen.auth
- If you generated auth with `mix phx.gen.auth`, drop `validate_not_disposable()` into the existing `registration_changeset/2`. No other changes needed.
- LiveView UX
- In LiveView signup forms, the changeset error renders inline via `<.error :for={msg <- @form[:email].errors} />` without any JS — Phoenix replaces the field in place via the websocket.
Get a free API key
500 checks/month, no credit card. No credit card. 30 seconds.
Sign up free →