DD-005: inbound webhook construct — parked until a consumer asks¶
id: DD-005
status: PARKED
issues: []
adrs: [0057, 0003]
decided: 2026-10-12
decided_at_sha: b60baaf4e
parent: flense-programme
One-line intent¶
The removed top-level webhook construct was outbound (entity events →
HTTP) and no consumer used it. Both consumers that need webhooks need the
inbound direction, and both hand-write it. This DD records the inbound shape
so it is built once, against those consumers, rather than reviving the
outbound design.
Plan (what we already concluded)¶
An inbound webhook construct that declares a vendor endpoint and lets the
framework own the transport concerns the consumers re-implement today:
- Route —
POST /webhooks/<name>, CSRF-exempt (the framework already exempts the/webhooks/prefix), no session auth. - Signature verification — declared scheme (HMAC-SHA256 over the raw body, header name, secret from an env/config reference), verified before parsing. Constant-time comparison; reject with 401 on mismatch. Support the intent-to-receive handshake shape Xero uses (respond 401 to a bad signature, 200 to a good one, on an empty event list).
- Idempotent dispatch — a declared event-id path; a framework table records
(webhook, event_id)so a replayed delivery is acknowledged and not re-dispatched. Dispatch goes to a declared handler (aprocessor a service hook), never to inline code in the DSL. - Health — an optional
GETfor portal reachability checks (AegisMark shipsroutes/wonde_webhook_health.pyfor this).
Counter-priors: do not restore the outbound construct to stand in for this; do not let the DSL carry payload-mapping logic (that is the handler's job); do not accept a delivery whose signature was not checked when a secret is configured.
Reopen when (consumer force)¶
- Named consumer — AegisMark (
routes/wonde_webhook.py, Wonde partner lifecycle events) or cyfuture (routes/xero_webhook.py,services/vendor/webhooks.py) asks to replace its hand-written route with a declared webhook. - Evidence of host workaround — the routes above: route overrides that verify HMAC signatures and dispatch by hand.
- Acceptance that unblocks — the consumer's webhook validates, rejects a bad signature, de-duplicates a replayed delivery, and dispatches to its handler in that consumer's CI.
When a force lands:
- Set
status: FORCEDand date it. - Comment on every linked issue with the consumer + this DD path.
- Implement against the plan above; open child issues if the plan splits.
- When shipped, set
status: DONEand close issues.
Already shipped (do not re-litigate)¶
- The outbound
webhookconstruct (parser, IR, sentinel heuristics) is in history at the commit above; it was removed in flense ship D because no consumer used it. - The
/webhooks/CSRF exemption and route-override mechanism that the consumers' hand-written routes rely on stay.
Explicitly out of scope until force¶
- Outbound webhooks (entity events → HTTP).
- Retry queues, dead-letter handling and delivery dashboards.
Trail¶
| Date | Event |
|---|---|
| 2026-10-12 | Parked (this DD); outbound webhook removed in flense ship D |