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
SolaceLogoSVG → “Dungeon Books” wordmark frompublic/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/searchand/store/filter-product-attributeswith 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 hardcodedclassName="dark" encodeURIComponenton category/collection link builders so handles likecomics-&-graphic-novelsroute correctly. Updated seed to useandnot&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:
Inputwas forcing controlled mode for any consumer that didn’t passvalue- Payment radio sort was comparing non-existent
provider_id(the field isid) → 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'onlib/search-client.ts— accidental client imports fail at build- Env vars renamed
SEARCH_ENDPOINT/SEARCH_API_KEY(noNEXT_PUBLIC_prefix) unstable_noStore()on the route so per-keystroke responses aren’t cached- 150ms debounce +
AbortController-based per-keystroke cancellation parseLimitclampslimitto[1, 20]- Added
localhost:8000to backendSTORE_CORSandAUTH_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
Homepageglobal withhero/midBannergroups (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 clientlib/util/lexical-render.tsx— minimal Lexical → React renderer (~150 LOC, no extra deps). Covers paragraph, heading, list, quote, link, line-break, formats. PlusextractHeadingsfor sidebar TOC withlevel: 2 | 3for 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, returnstotalDocs/totalPages) instead of in-memory slicing BlogInfoonly renders whenpublishedAtis set — no fake “today” fallback
Security in the renderer:
- Heading tags allowlisted to
h1-h6 - Link
hrefs pass throughsanitizeUrlthat allows onlyhttp(s):,mailto:,tel:, root-relative, and fragment URLs. Rejectsjavascript:/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 restackhandled 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
MeiliSearchcapitalization “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
Homepageglobal 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:
- Before going live. One non-negotiable test: cart → address → Stripe → place order. Single test, ~30s in CI, gates every PR.
- When a second contributor lands. Add a smoke suite (~5 tests): homepage loads, product detail flow, checkout, register/login, search returns hit.
- 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:
- Merge the stack. Graphite-side merge.
- Fill in real Payload content. Hero copy, About, FAQ, Privacy, Terms, first blog post. Placeholders drafted but I haven’t pasted them in yet.
- 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/stripewith the signing secret in env. - JSON-LD product structured data. From platform-eval-comparison pre-launch budget. Half-day. Important for book rich snippets in Google.
- 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_updated→charge.succeeded. Manual capture firescharge.captured.medusa-config.tsthrows in prod ifSTRIPE_WEBHOOK_SECRETmissing, warns in dev. - PR #6 — Gitignore Payload generated files.
payload-types.tsandimportMap.jsregen on every Payload boot. Gitignored +predev/prebuildhooks regenerate on fresh checkouts. No more phantom diffs. - PR #7 — JSON-LD product structured data.
Book(or fallbackProduct) +BreadcrumbListon every product page. Server-onlyJsonLdcomponent. Sanitizes link hrefs (rejectsjavascript:/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
/shopand/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:
- In stock — mirrored from Square. Square stays system of record for shelf inventory.
- 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 pathdocs/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.
Related
- emporium
- platform-eval-comparison — Solace + Fashion plan, ~4-5 day pre-launch budget; this session covered ~80% of it
- medusa-v2-platform-guide
- store-rebuild
- guild still ships first per project plan; this session was an opportunistic Emporium sprint