512kg / dima
Commercial

Sell art

I sell original paintings and prints through three storefronts I built. The gallery of originals is the one with the hard problem: every piece exists once, so every mistake is a painting sold twice or not at all.

A Cloudflare Worker with static assets. There is no database, no CMS, no build step and nothing to install: the catalogue is one file, and the Worker reads it.

Checkout goes through Stripe. Before a session is created, a Durable Object reserves every painting in the order, all or none, for seven minutes; a second buyer gets a 409 instead of a payment page. The full story of that lock.

The price never comes from the browser

The checkout request carries three things: which paintings, the country and an optional discount code.

{ "ids": ["<slug>", "<slug>"], "country": "US", "code": "" }

No price, no title, no shipping cost. The Worker looks all of that up in the same catalogue file the visitor’s browser loaded. Changing a price in that file and deploying changes the price shown and the price charged in one step. A tampered client can change which painting it buys, not what it costs.

A cart that sat for three weeks

A buyer adds a painting to the cart and leaves it there. Three weeks later someone else buys it. The first buyer comes back, and their browser still holds a catalogue in which the painting is available.

Nothing on the client can be trusted here, so the answer has three parts:

  1. The webhook marks the sale at once. When the payment completes, a mark is written within seconds. It does not wait for the catalogue to be edited and redeployed.
  2. The cart tells the truth when it opens. Every time the panel opens it asks the server which of its items are still available. The call is made after the panel is painted and is not awaited, so opening the cart never waits on the network. If it fails, nothing breaks.
  3. The server refuses anyway. Checkout re-checks every item and answers 409 with the list of the ones already sold. A stale page, a second tab or a hand-written request cannot get past it, because the request only ever carries identifiers.

The client then strikes exactly those rows and lets the buyer pay for the rest. A cart of six with two sold does not fail as a whole.

Two rules I would not relax: a repeated identifier in one order is collapsed, so a unique painting can never be billed twice; and an invalid discount code rejects the order instead of quietly charging full price.

Looking at the paint without handing over the painting

A painter wants visitors to see the real brushwork. A painter does not want a print-quality file reaching anyone’s browser.

The original never leaves my machine; it has no address, not even a private one. A script cuts it into a fixed grid of cells about 540 pixels wide and writes only the “white” squares of a checkerboard. The “black” squares do not exist anywhere at high resolution, so no address, size or parameter can produce them.

Fig. 1 — Half the cells do not exist. A schematic grid, smaller than a real one. The cells written at full resolution are those where row plus column is even. The others were never generated, so asking for one returns 404 whatever the size or parameter.

That last part is the point. Another site’s checkerboard preview was defeated by requesting the tiles at several sizes and overlapping them. Here there is one size, one fixed grid and no resizing on the server. The most anyone can extract is the normal photo plus half the cells.

The obvious alternative was to keep the original in storage and let a Worker resize it on request. It works, but the original would sit in the cloud, and a single bug in the list of allowed sizes would serve it whole. With tiles generated in advance, that failure cannot happen. I prefer protections that hold because the thing is not there over protections that hold because a rule is correct.

Buyers can make an offer on a painting. There are no accounts: the buyer is an email address plus a private page with a 20-character identifier.

  • Below a minimum, the server says no without revealing the number.
  • Accepting an offer does not reserve the painting. It stays on sale at full price and whoever pays first takes it; other open offers on it are closed and their buyers told.
  • Links in the emails only show a page. Every action is a form submission. Mail scanners open links, and an offer must not be accepted by an antivirus.

Smaller decisions

  • The Stripe webhook signature is verified by hand with WebCrypto: a 300-second window against replays, the raw body rather than re-serialised JSON, and a constant-time comparison.
  • Fonts are served from the site itself. Loading them from Google sent every visitor’s IP address there before they had consented to anything.
  • The catalogue, the reviews and the blog are rendered on the server. AI crawlers do not run JavaScript, and what they cannot read does not exist for them.
  • Payment routes are limited to 8 requests per minute, and the rest of the API to 30.
  • Purchases are reported to GA4 from the server, with a mark per session so that a Stripe retry is not counted as a second sale.

Two more storefronts

One is rendered entirely by a Worker from a single catalogue file.

The other is a Next.js 16 app, statically generated and deployed to Workers through OpenNext, with Server Components by default. Two details I like in it:

  • Filtering and sorting without data in the client. The catalogue bar changes two attributes on the container. display: none removes what does not match the filter, and order, with each cell’s position computed on the server as a CSS variable, sorts by price. The page stays static and the catalogue never enters the client bundle.
  • No media queries for the product art. It is sized in container units (cqw), so it keeps its proportion in a grid cell and on the full product page alike.
  • JavaScript72.3%
  • HTML15.3%
  • CSS6.0%
  • TypeScript5.8%
  • Other0.6%