Matt Pearson
Client work · storefront and portfolio for a painter

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.

The Barnett Art home page: the artist's name set large in a serif on warm paper, with a landscape painting framed alongside it.
The home page. Paintings are the only image on the site, so they carry all of the colour.

Built with

Next.js 16, Postgres, Drizzle, Stripe, Terraform

Links

Live site
Private repository, client work.

Role

Sole developer unless noted in the case study.

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.

Architecture diagram of the Barnett Art platform: a visitor reaching a Next.js app on Vercel, which branches into a checkout path through Stripe and an enquiry path through Resend, over Neon Postgres, Upstash Redis and S3, with CI and the content pipeline underneath.
One codebase, two commerce modes. The green and rust paths are the same site under a different setting.

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.

← All workRequest the CV →