Oversell-proof checkout: D1, anonymous carts and conditional updates
Every e-commerce backend has the same nemesis: two people buying the last item at the same moment. This post is about how a Worker and a D1 database handle it — with a one-line SQL condition doing most of the work.
The story
The storefront is a SPA served from the Worker’s static assets; the catalog, carts and orders
live in D1. Carts are anonymous by design: an HttpOnly cookie maps the browser to a carts
row, and no account is required until signup. When someone does sign up (or sign in), the
existing cart is claimed — attached to the user — so its contents and order history survive
across devices.
How it works
Adding to the cart validates stock against D1 before anything is written, and returns a helpful 409 with the real availability when it doesn’t fit:
const product = await env.DB.prepare(
'SELECT id, stock FROM products WHERE id = ? AND active = 1'
).bind(productId).first<{ id: string; stock: number }>();
const newQty = (existing?.qty ?? 0) + qty;
if (newQty > product.stock) {
return jsonResponse({ error: `Only ${product.stock} in stock.`, availableStock: product.stock }, 409);
}
Checkout is where the oversell guard lives. Every decrement is a conditional update — the
WHERE stock >= qty clause means the row only changes if the stock is actually there:
const statements = [
...items.map((item) =>
env.DB.prepare('UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?')
.bind(item.qty, item.product_id, item.qty)
),
env.DB.prepare('INSERT INTO orders (id, email, status, total_cents) VALUES (?, ?, ?, ?)'),
...items.map((item) => env.DB.prepare('INSERT INTO order_items (...) VALUES (...)')),
env.DB.prepare("UPDATE carts SET status = 'converted' WHERE id = ?"),
];
const batchResults = await env.DB.batch(statements);
DB.batch runs as a single transaction — all four statements or none. And because a conditional
update that loses a race changes 0 rows (meta.changes !== 1), the code can detect the loss
and compensate: refund the stock that did land, delete the half-created order, put the cart
back to active, and tell the shopper honestly:
const failedIndex = decrements.findIndex((r) => r.meta.changes !== 1);
if (failedIndex !== -1) {
// refund decrements that succeeded, delete order + items, reactivate cart
await env.DB.batch(compensation);
return jsonResponse(
{ error: `"${conflicted.name}" just sold out. Order cancelled, nothing was charged.` },
409
);
}
Two details worth stealing for any real project:
- The email rendered in the admin Orders tab is validated against markup characters (
<>) at the boundary — defense in depth behind the panel’s output escaping. - Order history is a union: the signed-in account’s orders plus everything placed from this browser’s cart. History follows the account across devices once orders are placed signed-in.
What the demo shows
The beret is seeded at stock 0 and the minimalist watch at stock 3 — on purpose, so inventory states are demoable without scripting. Open two browser windows, buy the last watch in one, attempt it in the other: the sold-out guard fires, and no one oversells anything.
Evidence: what to capture
- Storefront product cards with stock badges (in stock / only N left / out of stock) →
02-stock-badges.png - Cart drawer with items and subtotal →
02-cart-drawer.png - Demo 4 run: window A buys the last watch (order confirmed), window B gets the sold-out guard → two screenshots
02-checkout-race-a.png/02-checkout-race-b.png - Cloudflare dashboard → D1 →
lumina-users→orderstable showing the confirmed order →02-orders-table.png - The checkout-flow diagram: open
Blog/src/assets/diagrams/checkout-flow.excalidrawin Excalidraw, export PNG →02-d1-storefront-checkout/checkout-flow.png(embedded above)
Key takeaways
UPDATE … WHERE stock >= qtyis the cheapest reliable oversell guard — the database wins races for you.- Detect the lost race with
meta.changes, then compensate explicitly: refund, delete, restore, explain. - Anonymous-cookie carts + a claim step gives you a no-signup UX that still converts into accounts cleanly.