Every print began as a painting
Debbie Graviss paints nature, animals and faith from a studio in Woodford County, Kentucky — "beauty, joy and goodness shared through art," in her words — and sells the work from gravissstudios.com: greeting cards and blank notecards, prints, wooden art boards, the occasional original, and seats at workshops and retreats taught from the studio at Nature's Rest. Seventy-odd products, most of them available in several sizes. The site that is live today is the first one. This post is about the second, which is being built from a clean Laravel skeleton rather than by migrating the first, and about the handful of decisions that make it worth rebuilding at all.
I should say up front where it stands, because a write-up that reads as finished when it is not is the one thing this blog tries never to do. The rebuild is at phase zero: a theme, a layout, a home page and three tests. Everything below the next heading is the plan of record, written down in August and argued with since, and I am writing it up before building it because the decisions are the part worth reading. The code, when it comes, will be an implementation of this.
The painting is the record
Walk the current catalogue and one fact organises all of it: every piece of merchandise is a reproduction of a specific painting. A 5 × 7 print of Mending the Net and a greeting card of Mending the Net are two products, but they are one painting, and a visitor who likes the card wants to know what else exists of it. A store modelled as a flat list of products with variants cannot answer that question without a tag somebody remembered to apply. So the rebuild's first entity is not Product. It is Artwork: the original piece, with its title, its story and its canonical image, as a first-class record that products hang from.
What that buys is a page per painting — the piece, its story, and everything made from it — and provenance for originals: when the one-of-one sells it is marked sold and hidden from the shop, but it stays in a gallery of sold work, and its prints keep selling, because selling a painting does not un-make its reproductions. Both of those are queries the flat shape cannot ask.
And the link is optional. It is true today that every product derives from a painting; it is not guaranteed to stay true — a tote bag, a book, a boxed set of several cards — and a model that requires a parent painting would force one to be invented. So artwork_id is nullable from the first migration, and the rule written in the plan is that no code may assume a product has a parent artwork: every query, view and admin form handles the null from day one, and the model tests cover a product with and without one. The alternative, making it required now and relaxing it later, is the kind of change that looks small and touches every screen.
Four kinds of selling
The other decision that shaped the model came from somewhere else in this workspace, where a farm store turned out to be four commerce models wearing one word. Graviss Studios is the same shape at a smaller scale. A print is merchandise: variants with a price, a stock count or no count at all. An original is a product with exactly one unit, which sounds like merchandise with stock = 1 and is not, because two people can try to buy it in the same second and exactly one of them may succeed — that is a race-condition test in the plan, not a stock field. A workshop is a dated session with a capacity: a seat is reserved when the checkout session is created, for thirty minutes, which is how long a Stripe Checkout Session lives, and released by the webhook that says the session expired, so a seat can never be oversold and never stays reserved by someone who walked away. And classes — multi-session, possibly a subscription — are deferred deliberately, with the note that if they turn out to be subscriptions the answer is Cashier, decided then and not now.
| Kind | What it is | Inventory rule | The test that guards it |
|---|---|---|---|
| Merch | Prints, cards, boards — a product with variants | A count per variant, or null for untracked | Variant pricing; inactive and out-of-stock edge cases |
| Original | The painting itself, one of one | Exactly one; sold → hidden from shop, kept in the gallery | Two simultaneous checkouts, exactly one succeeds |
| Workshop | A dated session with a capacity and a roster | Seats reserved with a 30-minute TTL, released on expiry | Overselling prevented under concurrency; expiry releases the seat |
| Class | Multi-session; one-time or subscription | Not decided — deliberately | — |
Underneath all four is one order. A cart becomes a Stripe Checkout Session — the hosted page, chosen because it is the fastest thing to ship, keeps card handling entirely with Stripe, and brings Apple Pay and Google Pay for free; if a fully branded on-site checkout is ever wanted, the embedded element replaces the redirect and the order model does not change. The order is pending when the session is created and paid when the checkout.session.completed webhook arrives, then fulfilled or picked_up — Debbie ships by hand with flat-rate shipping, and there is free local pickup at the studio — then completed; cancelled and refunded branch off. The transitions live in one place and are tested, every webhook is idempotent on its event id and handled on the queue, and money is integer cents everywhere, never a float. Stripe Tax is on, pending a conversation with the accountant. Guest checkout is allowed; an account is optional.
Around the order, the notifications are the ones a one-person studio actually needs: a text to Debbie when an order comes in, an email to the customer when it is paid and when it ships or is ready to collect, a booking confirmation for a workshop and a reminder forty-eight hours before it, by text to attendees who opted in at booking. Postmark for the mail, Twilio for the texts, each behind a small service class so a test never calls either.
Paper, not white
The part that is built is the look, and it starts from a refusal: the site should not feel like a stock storefront, because the work is warm, hand-made and spiritually grounded, and a cool grey grid would be lying about it. The surfaces are paper — cream, parchment and linen, not white. The accent is sage, drawn from foliage; calls to action are clay, from Kentucky earth; gold appears only on a sold badge or a rule. Headings are set in Fraunces, a serif with some character, and the text in Instrument Sans. All of it is exposed as design tokens — bg-cream, text-ink, border-sage-300 — so no page ever hard-codes a hex value and the palette can move in one place. The home page says "Midway, Kentucky" above the fold and names the paintings it will one day show, in placeholders, which is phase zero being honest about itself.
Three UI stacks in three weeks, and nothing to remove
The plan's own history is worth a paragraph, because it is the cheapest lesson in the project. Written in mid-August, it specified a panel framework for the admin and as the shared component library for the storefront, on the argument that one vocabulary across admin and public pages was worth having. Three days later a workspace-wide directive split the two — a CSS component library for the public side, the panel for the admin — and traded that argument away on purpose. The day after that, a second directive removed both and put every surface on the in-house component library, which got the single vocabulary back without the panel. The plan was rewritten each time. The storefront was not, because nothing had been installed under either earlier line: the whole first direction lived in a document and in one test. Correcting a plan cost a document. Every other application in the workspace that had actually installed the panel paid for a removal.
The one test is the sharper half. It skipped itself unless a class from the panel framework existed, so that it would run once the panel was installed. The panel was never installed. The test therefore never ran once, and reported as coverage for thirteen days — green, counted, meaningless. It was deleted rather than left looking like a test, and the lesson generalises past this project: a test keyed on the existence of something you are not going to install is not a pending test. It is a permanent silence that looks like a pass.
What is next
Phase one is the catalogue and the admin: artworks, products, variants and their images, with a seeder that loads Debbie's actual paintings and their print and card variants so the storefront always demos with real work rather than lorem ipsum. Then the storefront proper — shop, gallery, the page per painting, cart — then checkout, originals, workshops, launch hardening, and a blog she writes herself. Each phase ends green, documented, and demonstrable in a browser before the next begins. The site that is live today keeps selling cards in the meantime, which is the only reason a rebuild can afford to be careful.