Skip to content
All use cases

IN PRACTICE / WEBHOOKS

Webhook & event processing

A webhook endpoint is a small pipeline pretending to be a controller action.

Read the manifest
webhooks.bloccs06 NODES / DIRECTED FLOW
Webhook & event processing: an isometric view of the six-node network described below

Typed connections / Top pip = declared effect

01 / 06

receive

Accepts the inbound POST and emits a raw Webhook@1. The only node allowed to touch the network edge.

Node reference

THE ARCHITECTURE

Every hop has
a responsibility.

bloccs makes the pipeline explicit: one node owns the HTTP edge, a pure node verifies the payload, a split decides where each event goes, and the effectful sinks each declare exactly what they touch. Unknown events are dead-lettered instead of dropped, and every hop is typed and traced — so a malformed payload fails at the boundary, not three stages deep.

Implementation status

Where this stands today: verification, routing, persistence, dispatch and dead-lettering all run in 0.8. By design, bloccs doesn't serve HTTP itself — receive is fed by your web layer (Phoenix or any endpoint), and everything from there down is bloccs.

SOURCE OF TRUTH

From shape
to source.

The network is declared in TOML. Compile it with its referenced node manifests and bloccs checks the wiring, schemas, and capabilities before emitting a Broadway supervision tree.

COMPILEmix bloccs.compile

Node implementations live in the referenced nodes/*.bloccs files.

Manifest documentation
webhooks.bloccs
[network]
id      = "webhooks"
version = "0.1.0"
runtime = "beam"

[nodes]
receive  = { use = "nodes/receive.bloccs" }
verify   = { use = "nodes/verify.bloccs" }
route    = { use = "nodes/route.bloccs" }
persist  = { use = "nodes/persist.bloccs" }
dispatch = { use = "nodes/dispatch.bloccs" }
dlq      = { use = "nodes/deadletter.bloccs" }

[[edges]]
from = "receive.received"
to   = "verify.webhook"

[[edges]]
from = "verify.valid"
to   = "route.webhook"

# Fan-out: a known event is both persisted and dispatched downstream.
[[edges]]
from = "route.known"
to   = ["persist.event", "dispatch.event"]

[[edges]]
from = "route.unknown"
to   = "dlq.event"

[expose]
in  = { hook = "receive.received" }
out = { stored = "persist.stored", sent = "dispatch.sent", dead = "dlq.recorded" }

[supervision]
strategy     = "rest_for_one"
max_restarts = 5
max_seconds  = 60

[deploy]
concurrency = { persist = 1, dispatch = 4 }
06

One node. One responsibility.

The kind describes its role. The capability declares its reach.

01

receive

Source+HTTP

Accepts the inbound POST and emits a raw Webhook@1. The only node allowed to touch the network edge.

02

verify

Node

Checks the signature and shape. Pure: same input, same verdict, no I/O — trivially testable.

03

route

Split

Branches on event type. Known types continue; everything else is forced down the dead-letter path.

04

persist

Sink+DB

Writes the event to the store. Declares the DB capability — a stray HTTP call here won't compile.

05

dispatch

Sink+HTTP

Fans the known event downstream. Runs concurrently with persist under the supervisor.

06

dlq

Sink

Records unknowns instead of dropping them, so nothing silently disappears.

Build this one for real.

Declare the graph. Check the contracts. Let the BEAM run it.

Read the docs
{:bloccs, "~> 0.9"}