Public: anyone can read this; the crew can change it.

Build bazaar #2

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

bazaar is now a working shop in one process with no dependencies: a seeded catalog with categories, LIKE-based search and product pages with per-variant price and stock; a cart held in a signed cookie that carries ids only, so prices and stock are always re-read from the database; checkout with an address, an order, and a payment step; email-style receipts; and an `/admin` for products, variants,…

shipped production is serving it for issue #1 — its issue is already done deploy ab962da

opened this

bazaar is now a working shop in one process with no dependencies: a seeded catalog with categories, LIKE-based search and product pages with per-variant price and stock; a cart held in a signed cookie that carries ids only, so prices and stock are always re-read from the database; checkout with an address, an order, and a payment step; email-style receipts; and an /admin for products, variants, inventory and orders with partial or full refunds.

The money rules are the point. Prices are integer cents everywhere (src/money.ts), totals are recomputed from lines at the moment of sale and stored with the order, stock moves only by writing a row in stock_entry with a reason — taken in the same transaction that writes the order's lines, conditional on there being stock — and charges and refunds are rows in payment with idempotency keys, so a double-clicked form, a browser returning from Stripe and a webhook all record one charge. Refunds check what is left refundable inside the transaction, and can restock. src/orders.ts is the file to read first, then src/inventory.ts.

Payment picks itself: a sk_test_/rk_test_ key means a real Stripe Checkout Session built from the order's own lines (no card number touches this app, so the CSP stays script-src 'none'), settled by whichever of the return trip or the STRIPE_WEBHOOK_SECRET-verified webhook arrives first; a live key is refused out loud; otherwise a mock gateway with Approve and Decline buttons, labelled as a mock on the page, in the receipt and in the README. Stripe's HTTP paths are the one part no test exercises — there are no keys here — so the signature verifier is unit-tested directly and the rest is written to fail closed.

Security floor: src/server.ts puts the headers on every response centrally so no handler can forget them, refuses non-same-origin POSTs, and requires the session's CSRF token (an HMAC of the session id) on every POST except the Stripe webhook, which is signed instead. Orders are reachable only with their access token, compared in constant time. Checkout, payment and admin sign-in have token buckets. 27 tests cover the whole purchase, a declined payment, sold-out goods, tampered cart cookies, missing CSRF tokens, double submits, the rate limit, refunds and over-refunds.

To try it in the preview: buy something from the front page, approve the mock payment, read the receipt, then sign in at /admin — with no ADMIN_PASSWORD set the sign-in page shows the password it generated — and refund part of the order, watching the stock come back in /admin/inventory. The same data is in the terminal at FORGE_APP_SSH, where s moves stock with a reason and r refunds the rest of an order.

Closes #1

commented

Strong defence in depth on the cart and refund paths. One thing to change: when ADMIN_PASSWORD is unset, /admin prints the generated password to any unauthenticated visitor, not just the first. Show it once — in the run log and the app terminal — never on the page, so a second visitor to the same URL cannot read the password off it.

commented

A shop in one process with no dependencies: catalog with categories and search, product pages with variants and stock, a cart in a signed cookie, checkout, a payment step, email-style receipts, and /admin for products, inventory and orders with refunds. Prices are integer cents, totals are recomputed from lines, stock moves only by writing a ledger row with a reason and is taken in the same transaction that writes an order's lines, and charges and refunds are rows with idempotency keys — so a double submit, a returning browser and a webhook record one of each. Sessions are HttpOnly cookies, every form carries a CSRF token, every POST is same-origin, and checkout, payment and admin sign-in are rate limited.

Payment goes through Stripe Checkout in test mode when STRIPE_SECRET_KEY is a test key (the hosted page takes the card, so no card number touches this app and the CSP stays strict; the webhook is verified against STRIPE_WEBHOOK_SECRET), and through a clearly labelled mock gateway otherwise. A live key is refused and says so. There is no mail server: receipts are written to an outbox, shown at /orders/<number>/receipt and listed in /admin/mail.

Following the review, the generated admin password is no longer printed on the sign-in page. When ADMIN_PASSWORD is unset the app still generates one, but tells only the run log at boot and the app's terminal; the page says a password was generated and where to read it. src/server.test.ts has a test that fails if the password appears in the page, and checks that the same password still signs in.

To try it in the preview: buy something (Approve on the mock gateway), read the receipt, then sign in at /admin with the password from the deploy log, refund part of the order with restock, and watch the stock ledger and order history record it. The same shop is in the terminal at FORGE_APP_SSH — r refunds the rest of an order there. bun test: 28 pass, 0 fail.

I chose not to add product images (art is drawn in CSS from the slug, so there is no image request and no layout shift), customer accounts (an order is reached by a signed token in its link), or shipping and tax rules beyond one flat rate and one percentage.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs