# `Teac.Plug.EventSub`
[🔗](https://codeberg.org/FullStacking/teac)

A Plug for receiving Twitch EventSub events via webhook.

Verifies the `Twitch-Eventsub-Message-Signature` HMAC-SHA256 header,
handles the one-time challenge verification handshake, and dispatches
`notification` and `revocation` messages to your handler module —
the same `Teac.WssClient.Handler` behaviour used for WebSocket delivery.

## Phoenix setup

Add a route before your main pipeline so body parsers don't consume the
raw body before signature verification:

    # router.ex
    scope "/webhooks" do
      forward "/twitch", Teac.Plug.EventSub,
        secret: "your-webhook-secret",
        handler: MyApp.TwitchHandler,
        handler_state: %{}
    end

Because Phoenix's `Plug.Parsers` reads the body, you **must** configure a
raw body cache for the webhook path. Add this to your `Endpoint`:

    plug Plug.Parsers,
      parsers: [:urlencoded, :multipart, :json],
      pass: ["*/*"],
      body_reader: {Teac.Plug.CacheBodyReader, :read_body, []},
      json_decoder: JSON

Then use `Teac.Plug.CacheBodyReader` (included in this library) so the raw
bytes remain available for HMAC verification after parsing.

## Handler

Your handler receives the raw string-keyed subscription and event maps,
exactly as with `Teac.WssClient`. The return value of `handle_event/3` is
ignored in this context — the `handler_state` passed at startup is static.
Use side effects (PubSub, GenServer messages, database writes) for state.

    defmodule MyApp.TwitchHandler do
      @behaviour Teac.WssClient.Handler

      @impl true
      def handle_event(%{"type" => "channel.follow"}, event, _state) do
        IO.puts("#{event["user_name"]} followed!")
        {:ok, nil}
      end

      def handle_event(_sub, _event, state), do: {:ok, state}

      @impl true
      def handle_revocation(%{"type" => type}, _state) do
        Logger.warning("Subscription revoked: #{type}")
      end
    end

## Webhook secrets

Generate a secret per subscription and store it. Pass it to the Plug via
`:secret`. If you use a single webhook endpoint for all subscriptions, use
one shared secret. If you route multiple secrets, wrap this Plug in your own
Plug that selects the correct secret per request path.

## Deduplication (idempotency)

Twitch retries failed webhook deliveries up to **three times**, each carrying
the **same** `Twitch-Eventsub-Message-Id` header. This Plug does **not**
deduplicate retries — it is stateless by design.

If your handler performs non-idempotent operations (e.g. inserting a row,
sending a message), you are responsible for deduplication. A common approach:

1. Extract the message ID from the conn before calling the handler:

       msg_id = List.first(get_req_header(conn, "twitch-eventsub-message-id"))

2. Check whether you have already processed that ID (Redis, ETS, database).
3. If seen, skip processing and respond 204 anyway — Twitch will stop retrying
   once it receives a 2xx response.

The message ID is a stable UUID per notification; Twitch considers any 2xx
response a successful delivery.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
