Skip to content

The seams

A fork’s src/ tree looks like one undifferentiated pile of components, and it is not. Some of it is styling that exists to be overwritten. Some of it is a documented seam meant to be extended. Some of it is the specific place an invariant is enforced, and breaks without a compile error when someone edits it as if it were the first kind. This page is the map.

Presentation. Rewrite, restyle or delete; nothing depends on the internals, only on the data they are handed.

  • src/pages/LandingPage.tsx reads store.hero (every field optional, with a fallback to the store name and a Shop everything button) and getFeaturedProducts / getVisibleCollections from shared/catalog.ts. The collection tiles are inline here; there is no separate component to swap.
  • src/components/layout/Banner.tsx and Footer.tsx. Both build their links from store.pages and the visible collections. The footer deliberately lists every published page whether or not it is in the header menu; keep that.
  • src/components/product/ProductCard.tsx takes a name, a preformatted price, an image and a sold-out flag, and nothing from shared/, so the theme editor can render one with no catalogue behind it. ProductList.tsx is the thin layer above it. If you replace the list too, keep it deriving price and sold-out state from the catalogue rather than caching them.
  • src/index.css. The --beluga-* custom properties are the baseline every component is styled against and what Settings → Look overwrites at runtime. Read colours and radius from these tokens in anything new.
  • emails/*.hbs. Handlebars locals are pre-formatted display strings, so a template does no arithmetic.

Each of these is a function or interface with an existing example of exactly the substitution it exists for.

  • The catalogue’s source: loadStore in src/lib/store-source.ts. Fetches /api/store and validates it with storeSchema; with VITE_BELUGA_API=false it validates the demo fixture instead. Point it at a different backend by keeping the return type, a schema-validated Promise<Store>, the same.
  • Search and sort: searchProducts / sortProducts in shared/catalog.ts, client-side against the snapshot. Past STORE_SNAPSHOT_LIMIT (200 products), the swap is to GET /api/products?search=, which already exists, behind the same store-source.ts file.
  • Which email goes out when: templateForStatus in server/email.ts maps an order status to a template. Add a status and a matching .hbs pair.
  • Where uploaded images live: ImageStore in server/image-store.ts, with put, delete and publicUrl. Two drivers ship, local disk and S3-compatible, chosen by environment. A third backend implements the interface; the database stores the same relative path under all of them and the storefront keeps asking for /assets/<path>.

These look like the files in section 1 and are not.

  • src/pages/CartPage.tsx. The cart holds identifiers and quantities, never a price or an image URL; every displayed price is re-derived from the current catalogue on render. If the cart cached a price, that is the value a stale tab would send back.
  • src/pages/ConfirmPage.tsx polls GET /api/checkout/:sessionId rather than assuming payment succeeded because the buyer landed there.
  • src/components/product/ProductDetails.tsx clamps quantity through normalizeQuantity on every change, including a re-clamp when switching variants, so a variant with less stock cannot keep a quantity that oversells it.
  • server/routes/checkout.ts reads price, in cents, from the database for every id the client sent. Skip that read for a ā€œquickā€ optimisation and a client can name its own price.
  • server/routes/webhook.ts is the only place paid, refunded and cancellation states are written, and it deduplicates by event id. Add a new webhook handler here, not a new payment-state write anywhere else.
  • Anything under db/: both dialect schemas edited together, a migration for each, and db/dialect.test.ts green on both engines.

The theme editor changes tokens. Forking changes components. A forked component that hardcodes a colour renders correctly until a merchant opens the theme editor, then quietly stops matching the rest of the store. Theming has the token list; Restyle the storefront walks through doing both.