Alexander's Antiques — A Gallery That Sells by Conversation
Alexander's Antiques is a gallery in Manhattan whose pieces used to live on 1stDibs. It wanted its own house: a storefront where a collector can decide from across an ocean, an inquiry instead of a checkout, and one workbench where the staff answer. I built all three — storefront, admin, and API — and it is live at alexanderantiques.com.
01 — Problem
A gallery, not a marketplace
On 1stDibs the gallery's pieces sat beside everyone else's. The house wanted its own address: every piece its own inventory, a real gallery on Second Avenue, and a sale that ends in a conversation with the people who own the piece.
That rules out a cart. A collector in London looking at a $19,850 cloisonné elephant wants a shipping quote, a condition report, maybe a video call — not a checkout button. The product works when a collector reaches a confident inquiry about the right piece, and the staff can answer it from one place.
02 — The piece
Decidable from a distance
The piece page carries what a remote buyer needs to judge it: the photographs, the period, the maker and whether the piece is by or attributed to them, the measurements as named dimensions, and an obvious next question. One button opens it.
Photographs load because someone asked for them: the viewport arrived, a pointer settled, a thumbnail was pressed. Measured on the live site, a piece with 19 photographs fetches 9 images before the first click.

03 — Inquiry
Six questions instead of a checkout
The dialog offers six ways in: purchase, make an offer, request information, a condition report, a shipping quote, or a viewing — in Manhattan or by video. Each carries its own fields: an offer in cents, a destination and a postal code, in person or on a call.
A verified phone is the price of entry. Collectors sign in without a password by a code to that phone, Turnstile guards the send, and the SMS consent is stored with the version of the words they agreed to. The opening submission becomes the first card of a thread, not a message.

04 — Workbench
Answer from one place
Staff work a queue: new, responded, closed, closed by the collector. Opening a thread records who opened it first. A reply moves it to responded and sends the collector a letter, logged on the same page with its trace. A collector writing into a closed thread reopens it.
Every status write is a guarded update, so a collector writing while staff close the thread resolves to one stored state, never two.
05 — Architecture
One seam, proven by count
The storefront and the admin shipped first, on fixtures, behind repository ports. When the NestJS API arrived, no component changed: an HTTP adapter implemented the same 83 methods across 16 ports, parsed every response with the same Zod schemas, and mapped the API's errors back to the codes the fixtures already threw.
A parity suite asserted every method by name, so cutover was a count reaching 83, not a feeling. dependency-cruiser holds the boundaries — the API may import the domain and nothing else — and OpenAPI is generated from the domain schemas, committed, and diffed in CI, so a breaking route fails the build instead of the launch.
06 — Migration
917 pieces, and nothing invented
The catalogue came from the gallery's 1stDibs listings: 917 pieces across a four-level taxonomy of 124 categories. The model followed the data, not the other way round. 294 pieces have a diameter and five have no measurement at all, so dimensions became a named map instead of height, width, and depth. A source that says Unknown stores nothing.
The import is an operator command, never a seed. It reports first, refuses to write when its counts disagree with the signed figures, is safe to run twice, and touches catalogue rows only — never an account, a session, or a setting. A piece publishes with exactly the facts its listing states.

07 — Phone
Two columns, and an inquiry that stays
On a phone the collection is two columns, the header compacts as you scroll, and the inquiry action stays on screen while the photographs move under it.
The storefront also lost weight while it gained motion. The three animations that pulled a JavaScript runtime into the first load became CSS, and home's first-load script fell from 558 KiB to 476 KiB.
08 — Quality
A gate, not a review
Typecheck, lint, 4,300+ tests, the build, and the dependency boundaries run in that order on every change. The domain package holds a 90% coverage floor and everything else 80%. The storefront targets WCAG 2.2 AA and holds a Lighthouse accessibility score of 100 in CI.
Work moved through 27 written specifications — specify, plan, tasks, implement — and shipped UI changed only under an exception signed in the roadmap. That discipline is how three people shipped this in four months without the codebase fighting back.
09 — Outcome
Live, under the house's own name
alexanderantiques.com is live: 939 pieces in seven departments, a collector's portal with a wishlist and threads, the Collector's Letter with confirm-to-subscribe, and a workbench where staff publish, merchandise the home page, invite colleagues, and answer.
Nothing on the site claims more than the house can stand behind. There is no provenance field, because there is no document to back one. That was a product decision, and the schema enforces it.
Stack
Next case
Smart School — One App, Two Desks→