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 touch the which keeps the whole thing inside the lightest PCI bracket there is. 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 I would make for a one-person gallery.
I turned down the obvious alternative too. Squarespace or Shopify would have been running in a weekend, and she would have paid monthly for a template thousands of other artists also use, on a platform she rents. She owns this one.
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 CI confirms what my machine already knew and rarely finds anything new.

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 written down in the repository and not left for somebody to find. 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.