PR Quorum
Sign inStart free
Sample review

This is what lands on your PR.

A real panel run on a (fictional) billing PR: three specialist reviewers in parallel, deduplicated and ranked into one review. No install needed to read it — this page is the review, verbatim.

github.com/acme-payments/api/pull/2841 · fix: per-seat billing webhookspr-quorum[bot] commented
needs attn3 reviewers, 4 findings · 00:42 · $0.012

This PR touches 9 files: 6 application code, 2 tests, and 1 configuration. The panel flagged one critical security gap on the webhook retry path and a billing race; the rest is non-blocking.

critical94%Security
Webhook signature verification skipped on retry path
src/billing/webhooks.ts:142

The retry branch re-processes the raw payload without calling verifyStripeSignature() — only the first-attempt path validates. A forged POST to the retry endpoint would be accepted and mutate subscription state. Move verification above the attempt branch so every path is covered.

high86%Correctness
Race condition between webhook handler and seat refresh
src/billing/sync.ts:73

refreshSeats() reads seat_count, then writes it back after an await on the Stripe API. A concurrent customer.subscription.updated webhook between the read and the write silently loses one of the two updates. Use an atomic UPDATE … SET seat_count = excluded.seat_count or take a row lock for the read-modify-write.

suggested change · committable
- const current = await getSeatCount(accountId);
- await stripe.subscriptions.update(subId, { quantity: current + 1 });
- await setSeatCount(accountId, current + 1);
+ await db.rpc('increment_seats_atomic', { account_id: accountId });
+ await stripe.subscriptions.update(subId, { quantity: await getSeatCount(accountId) });
high79%Architecture
Stripe client constructed inside the request handler
src/billing/index.ts:21

new Stripe(…) per request re-parses the key, re-creates the agent pool, and defeats keep-alive. Hoist it to module scope (the SDK is stateless across requests) — this is also where a request-scoped client quietly leaks API version overrides across handlers.

medium68%Correctness
Idempotency key derived from event.id only
src/billing/webhooks.ts:89

Stripe replays the same event.id on automatic retries, but a manually re-sent event from the dashboard mints a NEW id for the same logical change — the dedupe table won’t catch it. Key on (event.type, subscription.id, created) for state-transition events, or accept the rare double-apply and make the handler idempotent on content.

PR Quorum · 3 reviewers, one verdict · advisory — humans decide merge · @prquorum review re-run · @prquorum pause / resume this PR

Your next PR gets this treatment about 40 seconds after you open it.

Start free on GitHubSee all 8 reviewersHow the panel works

No credit card · Advisory only, never blocks a merge