Claim
A recorded run of 20 concurrent checkouts against stock 5 created 5 orders and 15 rejections, leaving stock 0 and 5 open reservations.
A hosted storefront whose sandbox checkout moves an order to PAID through a signed Stripe webhook, with stock decided under concurrency.
Live
Pay with the sandbox card 4242 4242 4242 4242 and watch the order move to PAID.
kestrel-store.onrender.com View the code
Demo sign-in demo@kestrel.test / demo-password-123
What to expect on the free instance
- The free instance sleeps after 15 minutes idle and takes about a minute to wake.
- Payments run in Stripe test mode with the 4242 card; no money moves.
- The catalogue is seeded: 30 products, 4 categories and 22 brands.
- The four shots follow the visitor path — catalogue, product, cart and checkout — so the admin console is not in the set.
01 / What it is
What it is
A seeded Next.js 15 storefront on PostgreSQL 16 with accounts, cart and admin. Checkout turns a cart into an order, reserves stock and creates a Stripe PaymentIntent from the total. A signed webhook moves the order to PAID, and a recorded replay answers 200 duplicate.
02 / The hard part
The hard part
Stock, price and idempotency are decided under concurrency. Reading stock then writing a reservation lets concurrent buyers observe the same last unit; a retried submit can create a second order; a client can post a total the server did not compute. The database arbitrates each.
03 / The decision
The decision
The conditional UPDATE is the stock arbiter
Decision Decrement with UPDATE ... WHERE stock >= $qty inside the checkout transaction; a zero-row update aborts it as OUT_OF_STOCK.
Rejected alternative Read, check in application code, then write in a second statement; concurrent buyers can pass the read for the last unit.
Cost The loser pays for a rollback, and checkouts serialise on the variant row.
Evidence The recorded run returned 5 orders and 15 rejections on stock 5.
04 / The failure it refuses
The failure it refuses
Two buyers reach the last unit at the same moment, or a client posts its own total and retries the submit. The system resolves every such request to one order or an explicit refusal.
Mechanism Price is recomputed from the cart inside the checkout transaction; the conditional UPDATE decrements stock while stock >= qty and returns zero rows otherwise; the (userId, idempotencyKey) unique constraint folds a retry into the existing order. A zero-row update aborts the transaction as OUT_OF_STOCK.
05 / Measured
Measured
Recorded hosted payment: order KST-2610-53808809, a 14900-cent PaymentIntent and one signed webhook delivery.
Four invariants hold: a forged total is ignored; one order per (userId, idempotencyKey); 20 concurrent checkouts on stock 5 produced 5 orders and 15 rejections; a replayed webhook is a 200 no-op. The load run measured p95 4.58 s over 250 requests and 2 P2028 timeouts.
| Metric | Value | Method | Artifact |
|---|---|---|---|
| Hosted sandbox payment | order KST-2610-53808809 PAID · 14900 cents · replay 200 duplicate | node scripts/live-payment-proof.mjs against the hosted store with Stripe test keys; order and webhook detail in docs/PROOF.md | scripts/live-payment-proof.mjs |
| Checkout concurrency on stock 5 | 5 orders · 15 rejections · stock 0 · 5 open reservations | npx vitest run tests/integration/checkout.oversell.test.ts against PostgreSQL 16; transcript in docs/proof/concurrency-run.txt | tests/integration/checkout.oversell.test.ts |
| Checkout load, 50 virtual users | p95 4.58 s · 250 checkout requests · 248 orders · 2 P2028 | k6 run load/checkout.js, 50 virtual users x 5 iterations against a production next start; summary in docs/load/checkout.md | load/checkout.js |
Gate checks (test counts, not metrics)
- Verification gate: 181 tests · 33 files · 5 Playwright tests. npm run verify (eslint, tsc, vitest, prisma migrate diff, Playwright)
package.json
Invariants and their tests
- A forged price in the checkout body cannot change the charge
tests/integration/checkout.forged-price.test.ts - One order per (userId, idempotencyKey) under concurrent double submit
tests/integration/checkout.double-submit.test.ts - 20 concurrent checkouts on stock 5 create exactly 5 orders and stock never goes negative
tests/integration/checkout.oversell.test.ts - An amount mismatch becomes REFUND_REQUIRED, never PAID
tests/integration/checkout.amount-mismatch.test.ts - A replayed Stripe webhook is a 200 no-op
tests/integration/webhook.replay.test.ts - Expired reservations release stock and cancel the order
tests/integration/reservations.sweeper.test.ts