// Field Guide — Base Camp — entry 02

Installation

Two ways into the woods. Both end at the same clearing.

The guided trail (igniter)

If your project already uses igniter:

mix igniter.install squatch_mail

This single command:

  • adds :squatch_mail to mix.exs
  • configures it in config.exs
  • generates the migration that creates its tables
  • mounts the dashboard in your router at /squatch
  • teaches your endpoint to preserve the raw bytes SNS webhook signatures need (step 5 below — the one thing that genuinely can’t be skipped)

Run mix ecto.migrate, start your server, and the expedition is underway.

Bushwhacking (manual installation)

Prefer full control over each step? Respect. Five steps.

1. Add the dependency

def deps do
  [
    {:squatch_mail, "~> 0.1"}
  ]
end

Then mix deps.get.

2. Configure SquatchMail

In config/config.exs:

config :squatch_mail,
  repo: MyApp.Repo,
  otp_app: :my_app,
  prefix: "squatch_mail"

:repo is required — it’s the Ecto.Repo SquatchMail uses for its own tables, which live in their own squatch_mail Postgres schema so they never collide with yours. See Configuration for everything else.

3. Generate and run the migration

mix ecto.gen.migration add_squatch_mail
defmodule MyApp.Repo.Migrations.AddSquatchMail do
  use Ecto.Migration

  def up, do: SquatchMail.Migrations.up()
  def down, do: SquatchMail.Migrations.down()
end

Then mix ecto.migrate. Future releases ship schema changes behind this same API — up()/down() always resolve to “latest”/“initial”, so this file never needs editing as SquatchMail evolves. One migration, versioned forever, the Oban / ErrorTracker way.

4. Mount the dashboard

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  import SquatchMail.Web.Router

  scope "/" do
    pipe_through :browser

    squatch_mail_dashboard "/squatch"
  end
end

Before this goes anywhere near production, put it behind your own auth pipeline — see Keeping the Forest Safe.

5. Preserve the evidence (raw request bytes)

SquatchMail’s SNS webhook must verify each notification’s signature against the exact bytes SNS sent. But by the time any router sees a request, your endpoint’s Plug.Parsers has already read and discarded the raw body. Plug.Parsers’s :body_reader option is endpoint-wide, not per-route — so this is the one piece of wiring that has to happen in your own endpoint.ex, before the router plug:

# in your endpoint.ex
defmodule MyAppWeb.SquatchMailBodyReader do
  @path_segments ["squatch"]

  def read_body(conn, opts) do
    if webhook_path?(conn.path_info) do
      SquatchMail.SNS.RawBodyReader.read_body(conn, opts)
    else
      Plug.Conn.read_body(conn, opts)
    end
  end

  defp webhook_path?(path_info) do
    prefix = @path_segments

    case Enum.split(path_info, length(prefix)) do
      {^prefix, ["webhooks", "sns", _token]} -> true
      _ -> false
    end
  end
end

plug Plug.Parsers,
  parsers: [:urlencoded, :multipart, :json],
  pass: ["*/*"],
  json_decoder: Phoenix.json_library(),
  body_reader: {MyAppWeb.SquatchMailBodyReader, :read_body, []}

Adjust @path_segments if you mounted the dashboard somewhere other than /squatch.

Skip this step and every real SNS notification will fail signature verification. The evidence must be preserved exactly as found at the scene. Tampered footprints are inadmissible.

The igniter installer does this automatically when your endpoint looks like a stock mix phx.new endpoint. If your Plug.Parsers options aren’t a plain literal keyword list, or you already have a :body_reader, it won’t guess — it leaves your endpoint untouched and prints the snippet above instead.

Connect SES

Once the dashboard is up, head to Base Camp and hit Connect SES. SquatchMail provisions the configuration set, SNS topic, HTTPS subscription, and event destination for you via AWS.SESv2 / AWS.SNS — the afternoon you’d otherwise spend clicking around the AWS console, done from a button.