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_mailtomix.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.