bloccs
Use cases

Use case

Webhook & event processing

A webhook endpoint is a small pipeline pretending to be a controller action. 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.

The flow

webhooks.bloccs
Webhook & event processing as a bloccs network

Where this stands today

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

The manifest

The whole network is one TOML file. Drop it in, run mix bloccs.compile, and bloccs emits a Broadway supervision tree from it — the compiler checks every edge, schema, and declared effect first.

  • Edges match ports by schema, or it won't compile.
  • Effects are declared per node — nothing touches the outside world undeclared.
  • Supervision and concurrency are part of the file, not an afterthought.
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 }

Anatomy

Node by node

Each node declares its kind and the capabilities it's allowed to use. Pure nodes touch nothing; effectful nodes carry a badge for exactly what they reach.

receive

Source +HTTP

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

verify

Node

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

route

Split

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

persist

Sink +DB

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

dispatch

Sink +HTTP

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

dlq

Sink

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

Build this one for real.

$ {:bloccs, "~> 0.9"}