2026-04-29 — Emporium foundation: Solace port, Stripe, search, Payload swap

Summary

Foundation phase for emporium landed in one session as a 4-PR stack on Graphite. Storefront is on Solace base, Stripe checkout works end-to-end with test cards, MeiliSearch powers the nav search dropdown, and Strapi is fully replaced by Payload. Stack is reviewed (Copilot + manual), all comments replied to in-thread, descriptions written. About to merge.

What shipped

PR #1 — Solace port (feat/storefront-solace-port)

Vendored solace-medusa-starter into emporium/storefront/. Per platform-eval-comparison the plan was Solace + Fashion’s MeiliSearch port; that’s exactly what the stack delivers across PR #1 and #3.

Customizations on top of the import:

  • Branding: Strapi-driven SolaceLogo SVG → “Dungeon Books” wordmark from public/logo.svg, copy swept site-wide
  • Dropped [countryCode] URL segment entirely. We’re a US-only bookshop; pages live at /shop, /cart, etc. Simpler URLs, less Solace plumbing to maintain
  • Replaced Solace’s custom /store/search and /store/filter-product-attributes with stock Medusa endpoints. Lost price/material filtering temporarily (re-enabled via Meili in PR #3)
  • Removed next-themes (warning under Next 16 + React 19); locked to dark mode via hardcoded className="dark"
  • encodeURIComponent on category/collection link builders so handles like comics-&-graphic-novels route correctly. Updated seed to use and not & in handles for new categories
  • React 19 forced via pnpm.overrides + peerDependencyRules.allowedVersions (Medusa UI v4 still pins React 18 internally; community-validated approach)
  • Tailwind kept at 3.4 — Medusa UI Preset is a v3 preset, v4 is a config-format break with no override path

PR is ~430 files / 35k+ insertions. Most is the starter itself. Copilot couldn’t review (>300 file limit), which is fine — it’s a vendor import.

PR #2 — Stripe payment provider (feat/payments-stripe)

Backend: @medusajs/payment-stripe@2.13.1 registered in medusa-config.ts. Top-of-config guard throws if STRIPE_API_KEY is missing so misconfig fails fast at boot. Storefront’s existing Solace Stripe components verified end-to-end with test card 4242 4242 4242 4242 — order placed, card charged, captured in Stripe dashboard.

Drive-by storefront fixes that surfaced during the smoke test:

  • Input was forcing controlled mode for any consumer that didn’t pass value
  • Payment radio sort was comparing non-existent provider_id (the field is id) → SSR/client mismatch
  • Cart’s delivery-step redirect still prepended /${countryCode} and 404’d

Stripe webhooks deferred. Sufficient for 4242 immediate-capture; needed for 3DS (4000 0027 6000 3184), refunds, disputes. Local: stripe listen --forward-to localhost:9000/hooks/payment/stripe then drop the whsec_ into STRIPE_WEBHOOK_SECRET.

PR #3 — MeiliSearch instant-search (feat/storefront-search-meili)

Nav search bar now hits /api/search (Next route) which proxies Meili and enriches each hit with current Medusa pricing via getProductsById + a Map keyed by id. Browser only ever talks to the storefront origin → no CORS, no Meili master key in client bundle.

Architecture decisions:

  • import 'server-only' on lib/search-client.ts — accidental client imports fail at build
  • Env vars renamed SEARCH_ENDPOINT / SEARCH_API_KEY (no NEXT_PUBLIC_ prefix)
  • unstable_noStore() on the route so per-keystroke responses aren’t cached
  • 150ms debounce + AbortController-based per-keystroke cancellation
  • parseLimit clamps limit to [1, 20]
  • Added localhost:8000 to backend STORE_CORS and AUTH_CORS

Skipped Fashion’s react-aria-components ComboBox stack — Solace’s UI is built on Headless UI, not worth a second a11y stack for one popover. Kept Fashion’s “Meili → Medusa price enrichment” data pattern + thumbnail/title/price hit layout.

Note: meilisearch@0.57.0 exports Meilisearch (lowercase ‘s’), not MeiliSearch. Caught when the build broke; Copilot’s suggestion to “fix the capitalization” was wrong, the lowercase is correct.

PR #4 — Strapi → Payload (feat/payload-content)

Replaces all Strapi-driven content (homepage hero, blog, content pages) with Payload-backed equivalents. Drops Strapi/MDX deps and the /api/strapi-revalidate webhook route entirely.

  • New Payload Homepage global with hero/midBanner groups (eyebrow/title/subtitle/image/cta)
  • Existing Pages and BlogPosts collections used as-is — they were already in Payload from earlier setup, schema fits
  • lib/payload-client.ts — server-only REST client
  • lib/util/lexical-render.tsx — minimal Lexical → React renderer (~150 LOC, no extra deps). Covers paragraph, heading, list, quote, link, line-break, formats. Plus extractHeadings for sidebar TOC with level: 2 | 3 for nested rendering
  • About-Us and FAQ flatten from Solace’s multi-section schemas to single Pages docs with rich text. Owner edits via headings + paragraphs in Payload admin
  • Blog list pagination is real (Payload limit+page, returns totalDocs/totalPages) instead of in-memory slicing
  • BlogInfo only renders when publishedAt is set — no fake “today” fallback

Security in the renderer:

  • Heading tags allowlisted to h1-h6
  • Link hrefs pass through sanitizeUrl that allows only http(s):, mailto:, tel:, root-relative, and fragment URLs. Rejects javascript: / data:

getProductVariantsColors is hardcoded [] — book products have no color variants. Refactoring OptionSelect to drop the dependency entirely is its own cleanup.

Process notes

  • Graphite stacked PRs worked well. Each branch as a focused PR; gt restack handled the cascade after amending lower commits with Copilot fixes
  • Copilot review on stacked PRs: PR #1 too big for Copilot (>300 file limit). PRs #2–4 got 27 comments combined; resolved 23 inline applies + 4 declines (each with reasoning). Replied in-thread to all
  • Real bug in Copilot output: the MeiliSearch capitalization “fix” was wrong. Worth checking suggestions empirically rather than applying blind
  • WSL networking gotcha: initial Meili client read was browser-direct → WSL Docker port forwarding fight. Pivoting to a Next route handler (server → Meili → server) sidestepped CORS and hid the master key. Right call regardless of WSL
  • Payload schema and stacked branches don’t mix gracefully. Checking out a branch without the Homepage global made Payload’s dev mode prompt to drop the homepage table (data loss). Either disable auto-schema-push in dev (use migrations), or only run Payload while on a branch that declares the schema. For this stack: kept payload off when on lower branches

E2E testing in CI — punt for now

Recommendation: don’t add e2e to CI yet. Solo build, manual click-through after each PR catches more than a brittle suite. Three thresholds for when it’s worth the cost:

  1. Before going live. One non-negotiable test: cart → address → Stripe → place order. Single test, ~30s in CI, gates every PR.
  2. When a second contributor lands. Add a smoke suite (~5 tests): homepage loads, product detail flow, checkout, register/login, search returns hit.
  3. After a real regression you didn’t catch. Write a test for it then. Don’t backfill speculatively.

Solace’s inherited e2e/ suite is salvage material, not a starting point — most assertions are wrong (countryCode URLs, Strapi shapes, payment provider names). Plan: write 5 fresh tests when the threshold hits, skip repairing the inherited ones.

data-testid discipline starts now even if tests don’t exist yet — every interactive element added in this PR series should have a stable selector. Cheap today, expensive to backfill.

What’s next

In priority order:

  1. Merge the stack. Graphite-side merge.
  2. Fill in real Payload content. Hero copy, About, FAQ, Privacy, Terms, first blog post. Placeholders drafted but I haven’t pasted them in yet.
  3. Stripe webhooks (separate branch). Required before going live; without them 3DS-required cards leave orders stuck pending. Local test via Stripe CLI; in prod, dashboard endpoint pointed at /hooks/payment/stripe with the signing secret in env.
  4. JSON-LD product structured data. From platform-eval-comparison pre-launch budget. Half-day. Important for book rich snippets in Google.
  5. First e2e test in CI — only the checkout happy path. Add when getting close to launch, not before.

Punted, no dates yet:

  • PayPal (Solace ships SDK, no creds)
  • Real product photos in seed (currently null thumbnails → gray placeholders)
  • Domain + deploy plan to Railway

Continuation (later same day)

Same-day session continued. Stack of merged PRs (#1–4) extended with five more, plus a strategy doc.

What also shipped

  • PR #5 — Stripe webhooks. 3DS verified end-to-end (test card 4000 0027 6000 3184). payment_intent.requires_action → auth modal → amount_capturable_updatedcharge.succeeded. Manual capture fires charge.captured. medusa-config.ts throws in prod if STRIPE_WEBHOOK_SECRET missing, warns in dev.
  • PR #6 — Gitignore Payload generated files. payload-types.ts and importMap.js regen on every Payload boot. Gitignored + predev/prebuild hooks regenerate on fresh checkouts. No more phantom diffs.
  • PR #7 — JSON-LD product structured data. Book (or fallback Product) + BreadcrumbList on every product page. Server-only JsonLd component. Sanitizes link hrefs (rejects javascript:/data:). Verified against Google Rich Results + schema.org validator.
  • PR #8 — Product page tweaks. Description default-expanded (out of accordion). “Complete the look” → “You might also like.” Shipping copy: dropped Solace’s literal “return the chair” reference.
  • PR #9 — Categories sidebar. Browse-style left sidebar on /shop and /categories/{handle}. Top-level + indented children. “All products” entry highlighted on /shop. Hidden below medium breakpoint (mobile keeps the hamburger).

All Copilot reviews on #5–9 worked through (~25 comments combined). Several Copilot suggestions accepted, several declined with reasoning (e.g. Meilisearch lowercase-s is correct; deferred e2e per project policy). Replied in-thread to all, updated PR descriptions.

Strategy doc with Carrie

extended-catalog-and-preorders — approved 2026-04-29.

Drafted after a conversation with Carrie about why we’re moving off Weebly. The point is to sell books we don’t have on the shelf, captured as pre-orders, batched into Omnibus. Two product tracks on one storefront:

  1. In stock — mirrored from Square. Square stays system of record for shelf inventory.
  2. Available to order — Medusa-only, manage_inventory: false. Ingestion via Omnibus CSV import for now (no API).

Important architectural correction caught mid-conversation: the original brief had me sketching a Medusa-side distributor batching queue. Carrie pointed out Omnibus already does that better — distributor selection, batch suggestions, wholesale pricing tiers, freight breaks. Medusa’s job narrows to:

  • Capturing the customer pre-order
  • Holding it in an “awaiting distributor” status
  • Providing a worklist staff can scan when planning the next Omnibus order
  • Auto-flipping to “ready to ship” when matching Square stock arrives

Hard separation of concerns: Medusa = customer capture + catalog enrichment + online sales engine. Square = inventory truth + walk-in POS. Omnibus = procurement brain. Each owns what it already owns; the seams are narrow.

This kills the original Phase 3 briefs (3.1–3.3 assumed full bidirectional Square sync). Replaced by the new plan + a callout in task-briefs.md.

Audit done

Walked all 22 task briefs against shipped work. Roughly:

  • Done (9): 1.1, 1.2, 1.4, 1.5, 1.6 (Stripe portion), 2.1, 2.2, 2.7 (static pages + JSON-LD)
  • Partial (5): 2.3 (reskin), 2.4 (homepage), 2.7 (nav/footer/sitemap not finished), 4.2 (placeholder content drafted not pasted)
  • Not started (8): 1.3 (ISBN scanner), 1.7 (Railway), 2.5 (Staff Picks), 2.6 (Events), 2.8 (Wishlist), 2.9 (gift cards), 3.x (now superseded), 4.1, 4.3
  • Added on top of briefs: countryCode removal, Lexical renderer, URL encoding, categories sidebar, product page UX, Stripe webhooks separately, Solace+Next 16 bug pile

Recommended priority for next sessions: 1.7 Railway → 4.2 content → 4.3 cutover is the launch path. Pre-order track (extended-catalog-and-preorders) is the post-launch revenue-stream-unlock work.

Doc updates this session

  • New: docs/plans/extended-catalog-and-preorders.md (Carrie-approved)
  • docs/plans/task-briefs.md: Phase 3 superseded callout; Brief 1.3 note about parallel ingestion path
  • docs/emporium.md: Status section rewritten (Phase 1 shipped, 5 PRs in flight); design line updated (“not just books”); added pointer to new plan + this journal entry

Stack as of end of session

main
├── PR #5 — Stripe webhooks
│   ├── PR #6 — gitignore payload generated
│   │   ├── PR #7 — JSON-LD products
│   │   │   ├── PR #8 — product page tweaks
│   │   │   │   └── PR #9 — categories sidebar

All 5 open. All have descriptions, all Copilot comments addressed.