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 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.

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 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.

← All workRequest the CV →