The BasicHTTP front for durable executions: a Plug-shaped helper that
takes one POST at an execution's location, decodes it with
Statifier.Send.BasicHTTP.decode/1, and delivers the event to the
execution the location names (ADR-0002, the Amendment of 2026-09-30,
decisions 3 and 5).
A location is a bearer capability. Anyone who holds it can post
events to that execution, and this front authenticates nothing beyond
possession of the location (ruled by the operator, 2026-09-30). Hand a
location only to the parties that should reach the execution, keep it
out of logs, serve the base URL over TLS, and rotate it with
StatifierRouter.BasicHTTP.rotate_location/2 when it may have leaked.
It is Plug-shaped, not a Plug, as StatifierRouter.Webhook is: it adds
no dependency on Plug or Phoenix and starts no process. The host routes
POST <base_url>/:token (and, for the 405, every other method) to a
controller action that builds the request map and calls handle/3, then
answers with response/1. The README's "A BasicHTTP front" section
shows it. handle/3 needs a configuration that sets :basichttp.
The request
:token- the path segment after the base URL, which the host cuts from the request path;:method- the request's method, as a string;:content_type- itscontent-typeheader, ornil;:body- its body, as it arrived;:query- its query string, ornil;:send_key- itsscxml-send-keyheader, ornil; optional.
The last five are handed to Statifier.Send.BasicHTTP.decode/1 as they
are. A request missing one of the first five, or carrying any of them
with the wrong type, is {:error, {:invalid_request, keys}}, naming the
keys and never the token.
What one request does
The request is checked, then the token is resolved, then the request is
decoded, then the event is delivered. While a route runs in the calling
process the front refuses with {:error, {:reentrant_route, execution_id}} before it resolves anything, as
StatifierRouter.Delivery.deliver/4 does.
- Resolution. A token with no location row, a string that is not
a minted token's shape, and an address row already stamped
terminal_seen_atare each{:error, :unknown_location}: the front delivers nothing and writes nothing. The answer does not say which. - Delivery. The event goes through
StatifierRouter.Delivery.deliver_event/4under the plan namebasichttp, the address row's document and key,create: :neverand ADR-0001's default horizon, with the row's scope. That is the one transaction every delivery takes: the dedupe claim, the address lookup, the step, the ledger row, whosebinding_idisbasichttp. - Deduplication. With a send key, the claim's message id is the
execution id,
/, and the key, so one key is deduplicated per execution; a request already enqueued within the horizon is a duplicate and nothing is enqueued. Without one, the message id is minted fresh and every such request is delivered. The key is the sender's claim, trusted as far as the location is.
A request that fails before the delivery writes no row: before the token
resolves there is no scope, and the ledger's scope is NOT NULL. No
row and no error the front returns carries the token.
The answer and the status
handle/3 answers {:ok, outcome}, one of
StatifierRouter.Delivery.deliver_event/4's outcomes, or {:error, reason}. response/1 maps it to the status and headers to answer with:
handle/3 answers | status | headers |
|---|---|---|
{:ok, {:delivered, "basichttp", execution_id}} | 204 | none |
{:ok, {:duplicate, "basichttp"}} | 204 | none |
{:ok, {:dropped, "basichttp", :finished}} | 404 | none |
{:ok, {:dropped, "basichttp", :no_execution}} | 404 | none |
{:error, :unknown_location} | 404 | none |
{:error, {:method_not_allowed, method}} | 405 | allow: POST |
| any other decode error | 400 | none |
any other {:error, reason} | 500 | none |
An event the execution selects no transition for is delivered and
answered 204: it was added to the execution's queue. A dropped: finished stamps the address row terminal, so a retry of the same POST
resolves as an unknown location and is answered 404 again. A 500 is a
delivery that did not settle, and a sender may retry it.
Summary
Types
What handle/3 answers, and what response/1 reads.
Types
@type answer() :: {:ok, StatifierRouter.outcome()} | {:error, term()}
What handle/3 answers, and what response/1 reads.
@type request() :: %{ :token => String.t(), :method => String.t(), :content_type => String.t() | nil, :body => binary(), :query => String.t() | nil, optional(:send_key) => String.t() | nil, optional(atom()) => term() }
The request a host hands handle/3; the moduledoc describes each key.
Functions
@spec handle(StatifierRouter.Config.t(), request(), keyword()) :: answer()
Takes one request at a location, as the module documentation describes.
opts: :now, a DateTime in UTC, the time the delivery's rows carry,
defaulting to DateTime.utc_now/0. Any other key is
{:error, {:unknown_key, name}}.
The status and headers to answer one handle/3 answer with; the module
documentation's table lists each.
iex> StatifierRouter.BasicHTTP.Front.response({:ok, {:duplicate, "basichttp"}})
{204, []}
iex> StatifierRouter.BasicHTTP.Front.response({:error, {:method_not_allowed, "GET"}})
{405, [{"allow", "POST"}]}