CI Version

Invisible honeypot spam protection for any Plug-based Elixir app.

Bots fill every form field they can find. Honeytrap adds a hidden field that real users never touch. It also measures how fast the form was submitted and collects a few passive interaction signals. If any of those look wrong, the request is flagged. You decide what to do with the flag.

Install

Add honeytrap to your deps in mix.exs:

def deps do
  [
    {:honeytrap, "~> 0.1.0"}
  ]
end

Usage

1. Arm the trap

Set a timestamp on the session when you render the form. Do this in your controller action.

def new(conn, _params) do
  conn = Honeytrap.arm(conn, :website)
  render(conn, :new)
end

2. Render the fields

Add the honeypot field and the signals field to your form template.

<form action={~p"/contact"} method="post">
  <%= Honeytrap.field(:website) %>
  <%= Honeytrap.signals_field() %>

  <label>Message <textarea name="message"></textarea></label>
  <button type="submit">Send</button>
</form>

[!NOTE] The honeypot field is hidden with inline styles by default. Pass a :class or :style to use your own hiding.

3. Plug the check into your pipeline

pipeline :submissions do
  plug Honeytrap.Plug, fields: [:website]
end

On matching requests the plug puts a result in conn.assigns.honeytrap:

%{
  bot: true,
  reasons: [{:filled, :website}, {:too_fast, :website}],
  human_rating: 0.2
}

4. Act on the result

Honeytrap does not halt the pipeline. Your action decides what to do. That way you keep an escape hatch for false positives.

def create(conn, params) do
  if conn.assigns.honeytrap.bot do
    render(conn, :thanks)
  else
    Contact.deliver(params)
    render(conn, :thanks)
  end
end

Configuration

Pass options to the plug directly:

  • :fields (required). List of honeypot field names to check. Set per plug call, since different forms use different fields.

These options can also be set globally via Application env under the :honeytrap key. Per-plug values override the global default.

  • :default_delay (default 2.0). Minimum seconds between arm and submit.
  • :disable_delay (default false). Skip the elapsed time check.
  • :minimum_human_rating (default nil). If set, flags submissions below this score.
  • :signals_input_name (default "honeytrap_signals"). Name of the signals input.

Example app config:

# config/config.exs
config :honeytrap, default_delay: 3.0

How it works

Three independent checks:

  1. Filled honeypot. Real users cannot see the field. Bots often fill it anyway.
  2. Elapsed time. Real users take at least a second or two. Instant submits are suspicious.
  3. Human signals. A tiny script watches for mouse, touch, scroll, keyboard, and focus events on the form. The result is packed into a hidden JSON blob. You can act on the rating or ignore it.

Session storage is used for the arming timestamp. No cookies beyond what you already have.

License

MIT.