A controller plus a body-reader plug — articles arrive signed.
Phoenix renders the blog; DraftSEO delivers the posts. The receiver is a controller action, but there is one Phoenix-specific step: Plug.Parsers parses the body by default, so you add a :body_reader that caches the raw body (the same pattern used for Stripe webhooks) and read it from conn.assigns[:raw_body]. Then :crypto.mac and Plug.Crypto.secure_compare verify it.
| Connection | A controller + body-reader plug — router.ex route + cached raw body, recipe provided |
|---|---|
| Security | HMAC-signed + timestamped — secure_compare over ${timestamp}.${raw_body}, replay-window protected |
| Payload | Full article JSON — title, HTML, images, meta, slug, language |
| Images | DraftSEO CDN URLs — URLs ride in the payload; rehost to your own storage if you prefer |
| Framework fit | LiveView-friendly — store via a context; LiveView renders the posts live |
Who does what
Add the raw-body reader plug, paste the controller, verify the HMAC over the cached body, then store the post via an Ecto context. After the green test, zero effort per article.
Your contexts, schemas and LiveViews stay yours.
Setup
Connect a site → Phoenix (Elixir). You get a signing secret and the controller recipe.
~1 minute
Give Plug.Parsers a :body_reader that stores the raw body, wire the route in router.ex, verify the signature and store the post.
~10 minutes
DraftSEO sends a signed test event; a green check confirms the connection.
~1 minute
Every generated article is delivered to your controller on cadence.
Ongoing · zero clicks
FAQ
A JSON body { event, deliveryId, data } — data holds the title, HTML article, slug, language, meta fields and image URLs, which Jason.decode! turns into a map for your context.
Each request is HMAC-signed over ${timestamp}.${raw_body} with a timestamp header; you recompute it with :crypto.mac and compare using Plug.Crypto.secure_compare, rejecting anything past the 5-minute window.
Because Plug.Parsers consumes the request body before your controller runs, and the signature must be computed over the exact raw bytes. The recipe adds a Plug.Parsers :body_reader that caches the raw body (the standard Stripe-webhook pattern) so you can read it from conn.assigns[:raw_body]. This step is required, not optional.
No card required. Signed test delivery in under a minute.