Nineteen workflows and no front door
The event branches were ready. Nothing verified, accepted or routed the events that were supposed to reach them.
- Engineering
- Security
- Automation
- n8n
I exported the production n8n instance to check the CrowdRelay integration. There were 49 workflows in total and 19 with CrowdRelay in the name.
That sounded reassuring for about ten seconds.
Branches without an entry point
The CrowdRelay workflows were branch handlers triggered by Execute Workflow. They knew how to react to events such as a paid ticket order, an issued admission pass or a reminder becoming due. What they didn't have was a public entry point: no webhook, no verified dispatcher, nowhere for an event from the Rust worker to actually enter the system.
I'd built the rooms and forgotten the front door.
Verifying the signature
A generic webhook would have made the diagram green, but it would also have accepted whatever JSON anybody sent to the URL. CrowdRelay already signs outbound requests, so the ingress had to preserve and verify the exact request instead of just checking that a header existed.
The signature is HMAC-SHA256 over:
timestamp + "." + exact raw request bodyKeeping the raw body
The exact body matters. Parsing JSON and serialising it again may produce the same object with different bytes, and different bytes produce a different signature. So the n8n webhook captures the raw body before any transformation happens.
The ingress checks the timestamp window, event ID, event type, schema version and signature. Signature comparison is constant-time. Unknown event types are rejected instead of being silently accepted and forgotten. A valid event is routed only to the branch assigned to that exact type.
Replay protection
Replay protection needed its own decision, because a properly signed request can still be resent. The ingress stores recently accepted event IDs and treats a duplicate as already handled. The sender gets a successful response, but the branch doesn't run a second time. That matters for events that issue a pass, send a message or change operational state.
The responses are deliberately different. A fresh accepted event returns 202. A known duplicate returns 200. A broken signature, stale timestamp, inconsistent headers or unsupported version gets a 4xx. The worker can now tell “try again later” apart from “this request will never become valid”.
Publishing the workflows
Publishing was another missing piece. In n8n 2.x the webhook and the referenced subworkflows need published versions; a workflow being visible in the editor doesn't mean it's available for production execution. I published the canonical branches and quarantined a duplicate handler for the same Discord reminder event instead of leaving two possible targets behind.
Request flow
The final shape is simple:
CrowdRelay outbox workersigned HTTP requestone verified n8n ingressexact event routingone branch workflowprovider-specific deliveryThere's no credential attached to every branch and no public webhook duplicated nineteen times. Verification happens once, routing happens once, and the branch receives an event that has already crossed the security boundary.
A collection of handlers doesn't make an integration on its own. Most of the work sits between the systems: authentication, replay handling, versioning, retries, publication state, and the basic question of whether anything is listening at all. Now something is, and it checks every request before passing it on.