Verify the bytes that Stripe signed
Taskboard receives a request saying a workspace subscription changed. Anyone can send JSON to a public HTTP endpoint. A familiar event shape is not evidence that Stripe sent it.
Stripe signs webhook deliveries. Taskboard verifies the signature using the endpoint's webhook secret and the original request bytes. The secret is distinct from the API key used to make provider requests.
Middleware order changes correctness
Normal JSON middleware parses bytes into an object. Re-serializing the object can change whitespace or property ordering. The JSON still means the same thing, but the bytes no longer match the signature.
app.post("/api/webhooks/stripe", express.raw({ type: "application/json" }), handler);
app.use(express.json());
The webhook route is registered before the ordinary JSON parser. Its handler receives a buffer, reads Stripe-Signature, and calls the provider library's verification method. Stripe requires an unmodified raw body for this check. Stripe webhook signature guidance.
Verification includes the signature timestamp tolerance. A captured signed request should not remain valid for arbitrary future replay. The host also needs a sensible clock; signature checks depend on time.
Accept only after durable storage
After verification, Taskboard validates the event fields it uses and saves the provider event in an inbox. The handler returns success after the database accepts the event and associated reconciliation work. Expensive provider reads happen later in the worker.
If durable storage fails, a success response would tell Stripe that delivery is finished even though Taskboard lost the event. A failure response preserves the possibility of provider retry. An invalid signature receives a client error and never creates a trusted inbox record.
The webhook endpoint does not use a browser session or CSRF token. Its authentication mechanism is the provider signature. A global CSRF rule must therefore distinguish this verified machine endpoint from browser-authenticated mutations.
Raw-body handling is in Read api/src/app.ts. Signature verification and event acceptance are in Read api/src/billing.ts.
Authenticate a webhook before trusting its content. Signature verification requires the original bytes, so parser order is part of the security design.
Can a secret URL replace signature verification?
No. URLs can leak through logs or configuration. Verification binds a delivery's bytes to the signing secret and checks the signature's age.