Dungeon Books Webshop Rebuild Spec
Date: 2026-03-11 (original), 2026-03-12 (revised — platform pivot to Medusa v2) Author: Technical spec for solo developer rebuild Status: Pre-implementation (Phase 1 in progress)
Platform pivot (2026-03-12): Original spec was Saleor + Paper. After auditing three Medusa starters (Solace, Agilo Fashion, Official) and comparing integration complexity, pivoted to Medusa v2 + Solace starter + Agilo’s MeiliSearch module. Rationale: 3 fewer standalone services, ~$540/yr less infrastructure, ~5 fewer dev-weeks in year 1, working Stripe+PayPal out of the box, and MeiliSearch autocomplete critical for Ingram search fallback. Full evaluation in
plans/platform-eval-comparison.md.
1. Project Overview
Rebuild the Dungeon Books online storefront from the current Square-backed Next.js application to a Medusa v2 + Payload + Next.js architecture. The new stack replaces Square as the online commerce backend while keeping Square POS for in-store sales.
Goals
- Eliminate the frontend component development bottleneck by adopting the Solace Medusa starter
- Add customer accounts, order history, wishlists, gift cards, and email notifications
- Establish Payload CMS as the editorial content layer for staff picks, blog, and events
- Build bidirectional inventory sync between Square POS and Medusa (in-process module, not a standalone service)
- Ship MeiliSearch-powered search with autocomplete from day one (foundation for Ingram fallback)
- Create the foundation for Ingram drop-ship fulfillment (post-MVP)
- Establish a new visual identity for the brand
Non-Goals (MVP)
- Ingram catalog integration and drop-ship fulfillment (Phase 2)
- Reviews and ratings
- Loyalty program
- Bookshop.org / Libro.fm affiliate integration (keep existing footer links)
- Multi-language / multi-currency
2. Architecture
System Diagram
┌─────────────────────────────────────────────────────────┐
│ Frontend │
│ Next.js (App Router, RSC) │
│ Fork of Solace Medusa starter (Strapi → Payload) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Commerce │ │ Content │ │ Search │ │
│ │ Pages │ │ Pages │ │ │ │
│ │ (Medusa SDK) │ │ (Payload API)│ │ (MeiliSearch)│ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└──────────┼─────────────────┼─────────────────┼─────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Medusa v2 │ │ Payload │ │ MeiliSearch │
│ (Commerce) │ │ (Content) │ │ (Search) │
│ │ │ │ │ │
│ Products │ │ Staff Picks │ │ Product index │
│ Cart/Checkout │ │ Blog Posts │ │ Autocomplete │
│ Orders │ │ Reading │ │ Typo tolerance │
│ Customers │ │ Lists │ │ Relevance scores │
│ Inventory │ │ Pages │ │ │
│ Payments │ │ Announce- │ └──────────────────┘
│ (Stripe+PayPal) │ │ ments │
│ Gift Cards │ │ │
│ │ └──────────────┘
│ ┌────────────┐ │
│ │ In-process │ │
│ │ modules: │ │
│ │ - Square │ │
│ │ sync │ │
│ │ - MeiliSearch│ │
│ │ indexing │ │
│ │ - Resend │ │
│ │ emails │ │
│ └──────┬─────┘ │
└─────────┼────────┘
│
│ Square sync module
│ (in-process, not a separate service)
│
▼
┌──────────────────┐
│ Square POS │
│ (In-store) │
│ │
│ Same catalog │
│ Same inventory │
│ POS terminals │
└──────────────────┘
Technology Stack
| Layer | Technology | Notes |
|---|---|---|
| Frontend | Next.js 16+ (App Router) | Fork of Solace Medusa starter |
| Commerce API | Medusa v2 (Node.js/TypeScript) | Self-hosted on Railway |
| Content CMS | Payload 3.x (TypeScript) | Self-hosted on Railway |
| Search | MeiliSearch | Product indexing + autocomplete (ported from Agilo Fashion starter) |
| Payments | Stripe + PayPal | Built into Solace (working out of the box) |
| Resend (React Email templates) | Ported from Agilo Fashion starter | |
| In-store POS | Square | Unchanged |
| Events | hi.events or Payload | Evaluate during build |
| Database | PostgreSQL 16 | Shared instance, separate databases |
| Cache / Event Bus | Redis 7 | Medusa event bus + cache |
| Media | S3-compatible (Cloudflare R2) | Product images, CMS uploads |
| Hosting | Railway | All services |
| DNS/CDN | Cloudflare | Current setup, keep |
Data Ownership
| Data | Source of Truth | Synced To |
|---|---|---|
| Product catalog (items, prices, images) | Medusa (via ISBN scanner + enrichment tool) | Square POS (via sync module, ISBN-matched only) |
| In-store inventory | Square POS | Medusa (via in-process sync module) |
| Online orders | Medusa | Square (inventory adjustment only) |
| Customer accounts | Medusa | Not synced to Square |
| Editorial content (blog, staff picks, lists) | Payload | Not synced |
| Events | hi.events or Payload | Not synced |
| Gift card balances | Square | Redeemed via Medusa checkout (see Gift Cards section) |
| Search index | MeiliSearch | Auto-synced from Medusa via subscribers |
Why Medusa Over Saleor
The original spec chose Saleor for its Paper storefront. After auditing Medusa starters (Solace: ~21,500 LOC with CMS, blog, real payments, dark mode; Agilo: ~15,900 LOC with MeiliSearch autocomplete), the frontend gap between Paper and Solace is negligible (~3,000 LOC). Meanwhile:
- Integration architecture: Every Saleor integration (Ingram, Square sync, payments) requires a standalone deployed service (“Saleor App”). Medusa runs all custom logic in-process via modules and subscribers. This eliminates 3 standalone services from production.
- Infrastructure: ~105/mo. $540/yr savings.
- Runtime: TypeScript everywhere vs Python (Saleor) + TypeScript (apps/storefront).
- Payments: Solace ships with working Stripe + PayPal. Paper ships with dummy payments.
- Search: MeiliSearch module from Agilo provides autocomplete with relevance scoring — critical for the Ingram search fallback (post-MVP).
3. Design Direction
Brand Context
Dungeon Books is a niche sci-fi, fantasy, and RPG bookstore in Jersey City. The audience is readers, gamers, and collectors who care about genre fiction and tabletop RPGs. The store runs regular in-person events. The brand should feel like a place — warm, curated, opinionated — not a catalog.
Design Principles
Curated, not comprehensive. The store carries ~1,700 items, not millions. The design should feel like walking into a well-organized independent bookstore, not scrolling through Amazon. White space, editorial pacing, and intentional product presentation over information density.
Genre-aware, not themed. Avoid cliché fantasy/dungeon aesthetics (stone textures, torch flames, medieval fonts). The audience is sophisticated — they read Ursula K. Le Guin and play Mörk Borg. The design should signal taste, not cosplay.
Warm and tactile. Books are physical objects. The design should evoke the feeling of paper, ink, and well-designed covers. Rich but restrained color, high-quality typography, generous spacing around product images.
Visual Direction: “The Indie Publisher”
The aesthetic reference is independent press design — think Penguin Classics covers, A24 film marketing, or the editorial design of McSweeney’s. Clean, confident, typographically driven, with moments of personality.
Typography:
- Display / headings: A high-contrast serif with character. Candidates: Playfair Display, Fraunces, Instrument Serif, or Gambetta. The heading font should feel like it belongs on a book spine.
- Body / UI: A humanist sans-serif with good readability. Candidates: Satoshi, General Sans, Switzer, or Cabinet Grotesk. Warm and slightly rounded — not geometric, not grotesque.
- Monospace (accents only): Used sparingly for prices, ISBNs, stock status, code-like UI elements. JetBrains Mono or IBM Plex Mono.
Color Palette:
Background: #FAFAF8 (warm off-white, like uncoated paper)
Surface: #F2F0EC (slightly darker, card backgrounds)
Text primary: #1A1A18 (near-black, high contrast)
Text secondary: #6B6960 (warm gray)
Accent: #C4501A (burnt orange — warm, distinctive, not generic blue)
Accent hover: #A13D10 (darker on interaction)
Success: #2D6A4F (muted forest green)
Warning: #B8860B (dark goldenrod)
Error: #9B2226 (deep red)
Border: #E2DFD9 (warm light gray)
The accent color (burnt orange) references vintage paperback spines and publisher marks. It’s distinctive without being aggressive.
Dark mode:
Background: #141413
Surface: #1E1E1C
Text primary: #EAEAE6
Text secondary: #8A877F
Accent: #E0732B (slightly brighter for contrast)
Border: #2E2D2A
Layout:
- Maximum content width: 1280px
- Product grid: 4 columns desktop, 2 mobile. Generous gap (24-32px).
- Product cards: image-dominant. Book cover at natural aspect ratio (no forced squares). Title, author, price below. No badges, no “NEW” tags, no visual clutter. Let the covers speak.
- Product detail page: large cover image left, details right. Tabs for description, details (ISBN, publisher, page count, format), and editorial content (staff review if available).
- Homepage: editorial-first. Hero featuring a curated pick or event, followed by curated sections (“Staff Picks This Month”, “New Arrivals”, “Upcoming Events”) rather than raw product grids.
- Category pages: sidebar filtering on desktop, sheet on mobile. Clean product grid.
Imagery:
- Product images: book covers on white/transparent background. No lifestyle photography, no staged shots. The cover IS the product.
- Editorial images: event photography, store interior shots, author photos. Used on blog/events pages, not product pages.
- No stock photography anywhere.
Micro-interactions:
- Subtle hover states on cards (slight scale or shadow lift)
- Cart drawer slides in from right
- Page transitions via Next.js loading states with skeleton screens (Solace provides these)
- No gratuitous animation. No parallax. No scroll-triggered reveals on product pages.
Radius and borders:
- Border radius: 6px (subtle, not aggressively rounded)
- Borders: 1px, warm gray. Used to define cards and sections, not as decoration.
- No drop shadows except on hover states and the cart drawer.
Implementation: Reskinning Solace
Solace uses a custom Tailwind preset (preset/plugins/colors.js) with CSS custom properties for light and dark mode, layered on @medusajs/ui-preset. Semantic token classes like --bg-primary, --content-action-primary, etc.
Step 1 (30 min): Replace color tokens in preset/plugins/colors.js with the palette above. Set border radius to 6px in Tailwind config.
Step 2 (2-4 hours): Replace Solace’s default fonts with chosen serif + sans-serif pair via next/font/google or self-host. Apply serif to headings, sans to body, mono to prices/ISBNs.
Step 3 (1-2 days): Review each major page template. Adjust product card layout to prioritize book cover aspect ratios. Adjust homepage to editorial-first layout with curated sections. Add Dungeon Books logo and navigation structure.
Step 4 (1-2 days): Polish. Fine-tune spacing, hover states, empty states. Verify dark mode. Verify mobile breakpoints.
Total reskin estimate: 2-4 days. (Slightly less than Paper — Solace’s dark mode and Tailwind preset are already well-structured.)
4. Medusa Configuration
Product Types and Metadata
Medusa v2 products have: title, handle (slug), description, status, type, tags, categories, options (variant axes), and metadata (key-value). Custom data models can be added via modules.
| Type | Product Metadata | Variant Options | Notes |
|---|---|---|---|
| Book | author, publisher, page_count, publication_date, series, series_number, language | format (Hardcover / Paperback / Mass Market / Board Book) | Primary product type. ISBN as variant SKU. |
| RPG Product | system (D&D 5e, PF2e, OSR, etc.), publisher | format (Hardcover / Softcover / PDF / Boxed Set / Cards / Screen) | TTRPGs, supplements, modules |
| Comic / Graphic Novel | author, artist, publisher, issue_number, series | format (Single Issue / Trade Paperback / Hardcover / Omnibus) | Comics, manga, GNs |
| Merchandise | item_type, material | size (S / M / L / XL / 2XL / One Size) — only for apparel | Non-book items |
| Gift Card | — | denomination (25 / 100) | Digital gift cards via Medusa |
For MVP, book metadata lives in product.metadata (JSON key-value). Post-MVP, consider a custom “bookstore” module (like Agilo’s “fashion” module) with typed Author, Publisher, Series data models and admin widgets.
Stock Locations
| Location | Purpose | Inventory Tracking |
|---|---|---|
| Jersey City Store | Physical in-store stock | Tracked, synced with Square POS |
| Ingram | Virtual location for drop-ship titles | Tracked (high stock) or untracked. Phase 2. |
Sales Channels and Regions
| Sales Channel | Region | Currency | Countries | Notes |
|---|---|---|---|---|
| Online Store | United States | USD | US | Main storefront |
A single region/channel is sufficient for MVP. Medusa supports multi-region natively (Solace already has country routing).
Product Categories
Mirror the current site structure:
Books
├── Science Fiction
├── Fantasy
├── Horror
├── Literary Fiction
└── Non-Fiction
RPG
├── Dungeons & Dragons
├── Pathfinder
├── OSR
├── Other Systems
└── Accessories (dice, minis)
Comics & Graphic Novels
├── Comics
├── Manga
└── Graphic Novels
Stationery & Gifts
Zines
Board Games
Categories managed via Medusa admin. Staff can add/remove via the admin panel.
Collections (Curated)
Collections are editorial groupings distinct from categories:
- Staff Picks (rotating monthly)
- New Arrivals (auto-populated or manually curated)
- Signed Copies
- Local Authors
- Event-related collections (e.g., “Author Visit: Joe Abercrombie” with all their titles)
Collections managed in Medusa admin or referenced from Payload by handle.
Shipping
| Profile | Methods |
|---|---|
| Default | Standard Shipping (50) |
| Local Pickup | In-Store Pickup (free) |
5. Payload CMS Configuration
Architecture Decision
Payload operates as an independent content layer — no product sync from Medusa. Editorial content in Payload references products by handle or ISBN. The frontend queries both APIs at render time and merges the data.
This avoids sync complexity and gives editors full control over content without coupling to the commerce data model.
Collections
Staff Picks
staffPicks:
fields:
- title: text (e.g., "March 2026 Staff Picks")
- slug: text (auto-generated)
- intro: richText
- picks: array
- productHandle: text (references Medusa product by handle)
- isbn: text (backup reference)
- staffName: text
- review: richText
- pullQuote: text (short blurb for card display)
- publishedAt: date
- status: select (draft / published)
Blog Posts
blogPosts:
fields:
- title: text
- slug: text
- author: text
- content: richText (with image blocks)
- excerpt: text
- featuredImage: upload
- tags: array of text
- relatedProductHandles: array of text (optional product references)
- publishedAt: date
- status: select (draft / published)
Reading Lists
readingLists:
fields:
- title: text (e.g., "Best Grimdark Fantasy 2026")
- slug: text
- description: richText
- items: array
- productHandle: text
- isbn: text
- note: text (why this book is on the list)
- position: number
- featuredImage: upload
- publishedAt: date
- status: select (draft / published)
Store Announcements
announcements:
fields:
- message: richText
- type: select (info / warning / promotion)
- active: boolean
- startDate: date
- endDate: date
- linkUrl: text (optional)
- linkText: text (optional)
Pages (General)
pages:
fields:
- title: text
- slug: text
- content: richText (with image blocks, embed blocks)
- seoTitle: text
- seoDescription: text
- status: select (draft / published)
Used for About, Shipping, FAQ, Contact, and any other static-ish pages.
Events Decision
Two options. Decide during build:
Option A: Payload events collection. Simple fields: title, date, description, image, ticketUrl (link to Eventbrite/hi.events/Square for ticketing). Payload is just the CMS for display. Ticketing is external.
Option B: hi.events integration. Full event management platform. Payload stores minimal event reference data; hi.events handles RSVPs, ticketing, capacity. Frontend embeds or links to hi.events for registration.
For MVP, start with Option A. Migrate to Option B if event management complexity grows.
events:
fields:
- title: text
- slug: text
- date: date
- endDate: date (optional, for multi-day)
- time: text (e.g., "7:00 PM - 9:00 PM")
- description: richText
- featuredImage: upload
- location: text (default: "Dungeon Books, 115 Brunswick St")
- ticketUrl: text (external link, optional)
- ticketPrice: text (e.g., "Free", "$10", "RSVP Required")
- capacity: number (optional)
- relatedProductHandles: array of text (books being discussed/signed)
- status: select (draft / published / cancelled)
6. Frontend: Pages and Routes
Solace uses /[countryCode]/ prefix for multi-region routing. Since Dungeon Books is US-only, strip the country code prefix during the fork — all routes live at the root. This simplifies URLs and avoids unnecessary redirects. If multi-region is ever needed, the Medusa SDK supports it and the routing can be added back.
Commerce Pages (Medusa data via Solace)
| Route | Source | Notes |
|---|---|---|
/ | Custom homepage | Editorial-first: hero, staff picks, new arrivals, upcoming events |
/shop | Solace store template | All products, filterable |
/categories/[...category] | Solace category page | Nested categories, filtering |
/collections/[handle] | Solace collection page | Staff picks, signed copies, etc. |
/products/[handle] | Solace PDP | Cover image, details, tabs, add to cart, lightbox gallery |
/cart | Solace cart page | Full page cart with quantity controls |
/checkout | Solace multi-step checkout | Addresses → Shipping → Payment → Review |
/order/confirmed/[id] | Solace order confirmation | Thank you + order summary |
/account | Solace account hub | Parallel routes: @login / @dashboard |
/account/orders | Solace order history | List with detail view |
/account/addresses | Solace address book | CRUD, modal form |
/account/profile | Solace profile settings | Name, email, phone, password |
/account/wishlist | Custom | Product grid of saved items |
/results/[query] | Solace search results | MeiliSearch-powered with autocomplete |
/gift-cards | Custom | Purchase digital gift cards |
Content Pages (Payload data — replacing Solace’s Strapi integration)
| Route | Source | Notes |
|---|---|---|
/blog | Payload blogPosts (replacing Strapi blog) | List with pagination, categories, MDX |
/blog/[slug] | Payload blogPost | Full post with related products |
/staff-picks | Payload staffPicks | Current month’s picks, archive |
/staff-picks/[slug] | Payload staffPicks | Individual picks page |
/reading-lists | Payload readingLists | All lists |
/reading-lists/[slug] | Payload readingList | List with product cards |
/events | Payload events | Upcoming events calendar/list |
/events/[slug] | Payload event | Event detail with ticket link |
/about-us | Payload page (replacing Strapi) | Store info, hours, map |
/faq | Payload page (replacing Strapi) | Common questions |
/privacy-policy | Payload page | Privacy policy |
/terms-and-conditions | Payload page | Terms |
/shipping | Payload page | Shipping policy |
/contact | Payload page | Contact form or info |
Blended Pages (Both APIs)
The homepage and editorial pages query both Medusa and Payload:
Homepage:
→ Payload: current staff picks, upcoming events, announcements
→ Medusa: new arrivals collection, featured products
→ Merge at render time in RSC
Staff Picks page:
→ Payload: pick data (staff name, review, pull quote, productHandle)
→ Medusa: product data for each handle (price, image, availability)
→ Merge: display editorial content with live commerce data
Reading List page:
→ Same pattern as Staff Picks
Blog post with product references:
→ Payload: post content, relatedProductHandles
→ Medusa: product cards for referenced products
→ Merge: inline product cards within editorial content
7. Square POS Inventory Sync
Overview
An in-process Medusa module that maintains bidirectional inventory sync between Square POS and Medusa. Runs inside the Medusa server — no standalone service to deploy.
medusa/src/
├── modules/square-sync/
│ ├── service.ts # SquareSyncService (Square API client, sync logic)
│ └── models/sync-log.ts # Sync event tracking table
├── subscribers/
│ ├── order-placed.ts # order.placed → decrement Square inventory
│ └── inventory-updated.ts # Inventory change → update Square (if needed)
├── api/webhooks/square/
│ └── route.ts # POST endpoint for Square webhooks
└── jobs/
└── nightly-reconcile.ts # Scheduled reconciliation job
Square → Medusa (In-Store Sale)
- Customer buys book at POS register
- Square fires
inventory.count.updatedwebhook (<5 sec) - Medusa receives webhook at
POST /webhooks/square/inventory - SquareSyncService looks up product in Medusa by ISBN (variant SKU)
- Updates Medusa inventory via InventoryModuleService
- Logs sync event to sync_log table
Medusa → Square (Online Sale)
- Customer orders book online
- Medusa event bus fires
order.placed - Subscriber inspects line items — filters to “Jersey City Store” stock location
- SquareSyncService calls Square
inventory.batchCreateChanges()withADJUSTMENTtype - Decrements Square inventory for each sold variation (lookup by ISBN → Square variation ID)
- Logs sync event
Sync Loop Prevention
When Medusa→Square adjusts Square inventory, Square fires a webhook back. To prevent infinite loops:
- After a Medusa→Square update, store
(ISBN, timestamp)in a short-lived Redis key (TTL 30s) - In the Square→Medusa handler, check Redis — if recently synced FROM Medusa, skip the event
Shared Key
ISBN stored as:
- Square:
catalogVariations.gtin - Medusa: variant
skufield
For non-book items without ISBN: custom SKU scheme (e.g., DICE-001, TOTE-LBX2025). Maintained in both systems manually or via migration script.
Reconciliation
Nightly scheduled job (Medusa jobs/ — runs via Medusa’s built-in job scheduler):
- Fetch all inventory counts from Square (batched by 100)
- Fetch all inventory levels from Medusa “Jersey City Store” stock location
- Compare by ISBN/SKU
- Flag discrepancies > 2 units
- Auto-correct if discrepancy is 1 unit (likely a single missed event)
- Alert via email/Slack if discrepancy > 2 units
- Log all corrections
Failure Handling
- Square webhooks retry on failure (up to 3 attempts)
- Webhook handler returns 200 immediately, processes async
- Dead letter queue (sync_log table with error status) for failed sync events — retry on next reconciliation run
- Health info available via admin API endpoint
8. Gift Cards
Current State
Dungeon Books sells physical gift cards that work with Square POS and Square’s online shop. Customers have existing gift card balances in Square.
MVP Approach
Medusa handles online gift card purchases. Square gift cards are redeemed manually.
-
New digital gift cards: Sold via Medusa’s built-in gift card system. Customer purchases on the website, receives a code via email. Redeemable on the online store only.
-
Existing Square gift cards: Cannot be automatically redeemed in Medusa checkout (no Square ↔ Medusa gift card sync). Two options:
- Option A (recommended for MVP): Add a note in checkout: “Have a Dungeon Books gift card? Email us at [email] to apply it to your order.” Staff manually processes as a partial refund or discount code.
- Option B (post-MVP): Build a custom workflow hook that validates Square gift card numbers against Square’s Gift Cards API at checkout, deducts the balance, and applies a discount to the Medusa order. This runs in-process — no separate service needed.
-
In-store redemption of online gift cards: Staff enters the Medusa gift card code in the Medusa admin to verify balance. Applies as a manual discount on the Square POS. Not ideal but workable for low volume.
Long-Term
Unify gift cards on one system. If Medusa becomes the primary commerce platform, migrate gift card issuance to Medusa and phase out Square gift cards for new purchases. Existing Square balances honored via manual process until depleted.
9. Wishlists
Medusa v2 does not have a built-in wishlist feature. Implementation options:
MVP: Customer Metadata
Use Medusa’s customer metadata to store wishlist product IDs:
// Add to wishlist
await medusa.admin.customer.update(customerId, {
metadata: {
wishlist: JSON.stringify([...existing, productId])
}
})Frontend reads metadata, fetches product data, renders a product grid on /account/wishlist.
Limitations: No sharing, no public wishlists, no “notify when back in stock.” Sufficient for MVP.
Post-MVP: Custom Wishlist Module
A custom Medusa module with its own data model:
wishlist_itemstable (customer_id, product_id, added_at)- API routes for CRUD operations
- Supports public/private lists, sharing via URL, back-in-stock notifications
10. Email
Transactional (Medusa + Resend)
Port the Resend + React Email module from the Agilo Fashion starter. This provides:
| Trigger | Template | |
|---|---|---|
| Welcome | Customer registration | React Email template |
| Order confirmation | Order placed | Line items, total, shipping address |
| Shipping notification | Fulfillment created | Tracking number, carrier |
| Password reset | Customer request | Reset link |
| Gift card issued | Gift card purchased | Code, balance, redemption instructions |
Templates are React components (medusa/src/modules/resend/templates/) — easy to brand with Dungeon Books colors, logo, and footer with store address and hours.
Newsletter / Marketing
Separate from transactional. Options:
Option A: Resend Audiences. If already using Resend for transactional, use their Audiences feature for newsletter. Simple, one vendor.
Option B: Buttondown. Indie-friendly newsletter platform. Markdown-based. Good for a bookstore’s editorial voice. Embed signup form on the site, manage sends via Buttondown’s interface.
For MVP, implement a newsletter signup form (email field in the site footer and on the homepage). Store subscribers via the chosen platform’s API. Staff writes and sends newsletters via the platform’s UI — no custom send infrastructure needed.
Checkout includes an opt-in checkbox (pre-checked): “Keep me updated on new arrivals, events, and staff picks.” On order creation, subscribe the customer’s email to the newsletter list.
11. Phases and Milestones
Phase 1: Foundation (Weeks 1-2)
Infrastructure:
- Update Docker Compose for Medusa server, PostgreSQL, Redis, MeiliSearch
- Deploy Payload on Railway
- Set up S3-compatible media storage (Cloudflare R2)
Medusa setup:
- Adapt
setup-saleor.ts→setup-medusa.ts: product types, categories, stock locations, regions, sales channels, shipping profiles - Port MeiliSearch module from Agilo Fashion starter into Medusa backend
Product ingestion (clean-slate re-inventory):
- Build ISBN scanner app (phone camera + USB barcode scanner support)
- Enrichment pipeline: scan ISBN → query Ingram Web Service (fallback: Open Library) → create draft product in Medusa with metadata, cover image, price
- Staff reviews and publishes drafts. Non-ISBN items entered manually.
- This IS the Ingram enrichment tool — used for initial inventory and ongoing product ingestion.
Payload setup:
- Define collections: staffPicks, blogPosts, readingLists, events, announcements, pages
- Create initial content: About page, Shipping page, 1-2 staff picks, 1-2 events
- Verify API access from Next.js
Milestone: Medusa running with MeiliSearch indexing. ISBN scanner app functional. Payload running with initial content. Stripe+PayPal checkout functional. Ready for in-store re-inventory scan.
Phase 2: Storefront (Weeks 3-5)
Fork and adapt Solace:
- Fork Solace Medusa starter into storefront/
- Replace Strapi CMS integration with Payload API calls (~1.5-2 days)
- Port MeiliSearch SearchField component from Agilo Fashion starter (~0.5-1 day)
- Reskin with “Indie Publisher” brand (typography, colors, layout) — 2-4 days
- Remove irrelevant components, add bookstore-specific rendering (ISBN, author, cover aspect ratios)
Build custom pages:
- Homepage (editorial-first layout, blended Medusa + Payload data)
- Staff Picks page and detail (Payload + Medusa product data)
- Reading Lists page and detail
- Events listing and detail
- Wishlist page (account section)
- Gift card purchase page
Note: Blog, About, FAQ, Terms, Privacy pages already exist in Solace (from Strapi integration). They just need Strapi→Payload data source swap, which happens in the initial fork step.
Integrate:
- Email templates (port Resend module from Agilo)
- Newsletter signup (footer + checkout opt-in)
- JSON-LD structured data on product and event pages
Milestone: Full storefront functional with commerce + editorial pages. Checkout works end-to-end with Stripe+PayPal. Customer accounts working. Search autocomplete working.
Phase 3: Square Sync (Week 6)
Build Square sync module (in-process):
- SquareSyncService (Square API client, ISBN-based product lookup)
- Square webhook receiver API route
order.placedsubscriber → decrement Square inventory- Sync loop prevention (Redis TTL)
- Nightly reconciliation scheduled job
- Dead letter queue (sync_log table)
- Testing
Testing:
- Simulate POS sale → verify Medusa inventory decrements
- Place online order → verify Square inventory decrements
- Kill sync for 1 hour → run reconciliation → verify correction
- Test edge cases: simultaneous POS + online sale, out-of-stock race condition
Milestone: Bidirectional inventory sync operational. Reconciliation job running nightly.
Phase 4: Polish and Launch (Weeks 7-8)
QA and polish:
- Cross-browser testing (Chrome, Firefox, Safari, mobile Safari, Chrome Android)
- Mobile responsive verification on all pages
- Accessibility audit (keyboard nav, screen reader, contrast ratios)
- Performance audit (Core Web Vitals, image optimization, caching)
- SEO verification (JSON-LD, OG tags, sitemap, robots.txt)
- Email template review
- Checkout flow end-to-end testing with real Stripe test cards
- Gift card purchase and redemption flow
- Wishlist add/remove/view flow
- Customer account CRUD
Content migration:
- Migrate existing events data
- Write initial staff picks for launch month
- Create 2-3 reading lists
- Write 1-2 blog posts (launch announcement, staff introductions)
- Populate store announcements
DNS cutover:
- Point dungeonbooks.com to new storefront
- Redirect beta.dungeonbooks.com → dungeonbooks.com
- Verify SSL, CDN caching, error pages
- Monitor error rates and checkout completion for 48 hours post-launch
Milestone: Live on dungeonbooks.com. Monitoring active. Existing customers can browse, buy, create accounts.
Phase 5: Post-MVP
Ingram integration (fulfillment — enrichment already built in Phase 1):
- Ingram Web Service credentials ($89/mo, 100K hits) — already used by ISBN scanner
- Search fallback: MeiliSearch returns few results → query Ingram → display “Available to Order” results → staff approval queue
- Ingram fulfillment workflow: order routing, PO submission, tracking
- “Ingram” virtual stock location
- Mixed cart checkout (in-store stock + Ingram drop-ship)
- All runs in-process — no additional services
Other post-MVP:
- Bookshop.org / Libro.fm deeper integration
- Square gift card validation workflow hook (redeem physical gift cards online)
- Public wishlists with sharing
- Back-in-stock notifications
- Custom bookstore module (typed Author, Publisher, Series models with admin widgets)
- Reviews / ratings
- hi.events integration (if event complexity warrants it)
12. Infrastructure: Railway Services
| Service | Type | Resources | Estimated Cost |
|---|---|---|---|
| Medusa Server | Web service | 1GB RAM, 1 vCPU | ~$10/mo |
| Next.js Frontend | Web service | 1GB RAM, 1 vCPU | ~$10/mo |
| Payload CMS | Web service | 1GB RAM, 1 vCPU | ~$10/mo |
| MeiliSearch | Web service | 512MB RAM | ~$5/mo |
| PostgreSQL | Database | 2GB RAM | ~$20/mo |
| Redis | Database | 512MB | ~$5/mo |
| Total | ~$60/mo |
Plus:
- Cloudflare R2 (media storage): ~$5/mo
- Resend (email): ~$10/mo (free tier likely sufficient initially)
- Stripe fees: 2.9% + $0.30/transaction
- Domain: existing
**Total infrastructure: ~540/yr less than Saleor path)
Note: No separate services for inventory sync, payments, or integrations. All custom logic runs in-process with Medusa.
13. Risks and Mitigations
| Risk | Severity | Likelihood | Mitigation |
|---|---|---|---|
| Inventory sync drift causing oversells | Medium | Medium | Nightly reconciliation job. Alert on discrepancies. Low volume reduces impact. |
| Medusa v2 upgrade breaks something | Medium | Low | Pin to specific version. Upgrade deliberately. Medusa v2 has stable API surface. |
| Solace starter diverges from Medusa v2 changes | Medium | Low | Fork is a snapshot. Track releases for useful updates but don’t auto-merge. |
| Strapi→Payload replacement introduces bugs | Low | Medium | Well-scoped: data fetching layer only. Components/layouts unchanged. |
| MeiliSearch index falls out of sync | Low | Low | Subscribers auto-index on product CRUD. Full reindex available as admin operation. |
| Payload adds operational overhead | Low | Low | Payload is lightweight. Same Postgres instance. Minimal maintenance. |
| Square changes webhook behavior | Low | Low | Reconciliation job catches drift regardless of cause. |
| Gift card split (Square physical + Medusa digital) confuses customers | Medium | Medium | Clear messaging at checkout. Manual process for Square gift cards. Unify post-MVP. |
| Railway outage takes down the store | Medium | Low | Railway has good uptime. No multi-region needed for this scale. Accept the risk. |
14. Success Criteria
Launch:
- All scanned/published products browsable with images, prices, and correct inventory
- MeiliSearch autocomplete returning relevant results for title, author, ISBN queries
- End-to-end checkout with Stripe + PayPal (guest + authenticated)
- Customer accounts functional (register, login, order history, addresses)
- Staff picks and at least one reading list live with commerce data
- Blog with at least 2 posts
- Events page populated with current/upcoming events
- Inventory sync operational with <5 minute latency
- Nightly reconciliation running and alerting
- Newsletter signup functional
- Gift card purchase functional
- Wishlist functional
- All transactional emails sending correctly (Resend + React Email)
- Dark mode working
- Core Web Vitals passing (LCP < 2.5s, CLS < 0.1, INP < 200ms)
- Mobile experience fully functional
Post-launch (30 days):
- Zero inventory-related customer complaints
- Checkout completion rate measurable via Stripe dashboard
- Staff comfortable creating content in Payload (staff picks, events, blog posts)
- MeiliSearch index staying in sync with product catalog
- No manual intervention needed for day-to-day operations