Barnett Art
Anna Barnett needed to sell original work online and add new pieces without me. It takes card payments through Stripe, cannot sell the same original twice, and switches between a shop and an enquiry site on one environment variable.

What it had to do
Two jobs that usually fight each other. A portfolio has to treat the paintings as work worth looking at, with room around them and nothing competing for attention. A shop has to take money reliably, know what is still in stock, and never sell the same original twice.
The harder constraint was not on the page. Anna had to be able to photograph a new piece and get it online without me, and she was not certain she wanted to run a card checkout at all. She might prefer enquiries. Whatever I built had to work both ways without a rewrite when she decided.
What I chose
Stripe's hosted checkout. I did not write a card form of my own, so card details never reach the server and the site stays in the lightest PCI bracket available. Writing my own payment form would have bought a slightly smoother handoff in exchange for handling other people's card numbers, which is not a trade worth making for a one-person gallery.
The obvious alternative was a hosted platform. Squarespace or Shopify would have been running in a weekend, and for many artists that is the right answer. It would have meant a monthly fee, a shared template and a storefront she rents rather than owns, and she wanted the site to be hers.
Prices live in Postgres as integer pence and the total is computed on the server. The browser is never asked what something costs, because anything the browser sends can be edited.
The undecided question became a setting. COMMERCE_MODE flips the entire site between card checkout and enquire-to-buy, one variable, no code change and no data migration. It cost about a day to build and it meant her decision stopped being a deadline.
How it works
A signed, HttpOnly cart cookie holds the basket and the checkout runs through server actions, so the shop still works with JavaScript switched off. Stripe's webhook is verified against the raw request bytes before anything in the database moves. Fulfilment then runs in one transaction that marks the order paid and decrements stock at the same moment, so an original flips to sold the instant payment succeeds and a second buyer cannot slip through behind it. Stripe retries deliveries, so a ledger of processed event ids makes a repeat delivery do nothing.
Adding a painting is a photograph and one command. The import script converts HEIC to WebP, rotates it the right way up, strips every EXIF field including the GPS coordinates of the artist's house, pulls an accent colour out of the image, and writes the gallery manifest. A single idempotent seed pushes the result into Postgres.
Every pull request runs Prettier, ESLint, a full TypeScript check, 101 unit tests, an npm audit gate and a gitleaks secret scan. The same lint and secret scan run before a commit is allowed and the type check runs before a push, so most problems are caught locally and CI is the second line rather than the first.

What it cost
The storefront renders on every request instead of once at build time, because stock has to be true at the moment somebody looks at it. That is the right call for a shop selling one-of-one items and it gives up the caching a static gallery would have had.
Pulling an accent colour from each painting turned out to need a guard. Extracted colours are often far too pale to sit on paper as text, so anything used as text is pushed darker until it clears 4.5:1. The accent gets to be the painting's colour right up until the point where it would stop being readable.
And the gaps, which are listed in the repository's own README rather than left to be discovered. Images still run through the local disk adapter, with the S3 path built but not switched on. Shipping is a flat placeholder waiting on her real rules. The privacy and terms pages are sound drafts that have not been through a solicitor. The script policy still allows inline scripts, which wants a per-request nonce.
What I'd change
The admin interface should have come before the payment plumbing. Editing a few data files and running two commands is self-service if you are a developer. She is a painter. I built the part that carried the interesting risk, and the result is a shop with careful money handling that she still has to ask me to restock.