Dungeon Books Rebuild: Task Briefs

Platform pivot (2026-03-12): Briefs rewritten for Medusa v2 + Solace starter. Original briefs were Saleor + Paper. See plans/platform-eval-comparison.md for rationale.

Structure: Monorepo. Existing structure from Briefs 1.1-1.3 (Saleor) is reused — docker-compose, scripts, workspace config updated in place.

dungeon-books/
├── docker-compose.yml        # Local dev: Medusa, Payload, Postgres, Redis, MeiliSearch
├── storefront/               # Fork of Solace Medusa starter (Next.js)
├── medusa/                   # Medusa v2 backend (custom modules, subscribers, API routes)
├── payload/                  # Payload CMS instance
├── scripts/                  # Migration, setup, utility scripts
└── docs/                     # Specs, ADRs, runbooks

Note: sync-app/ is removed — Square sync is an in-process Medusa module, not a standalone service.


Phase 1: Foundation

Brief 1.1 — Docker Compose and Monorepo Update for Medusa

CONTEXT:
The existing monorepo has Docker Compose for Saleor (API, Celery worker, Dashboard,
Postgres, Redis). You are pivoting to Medusa v2. The monorepo structure, Postgres,
Redis, and workspace config are reusable. Saleor services are replaced with Medusa
and MeiliSearch.

Existing assets to keep:
- pnpm-workspace.yaml (update package list)
- .gitignore, .env.example (update vars)
- docker/postgres/init-databases.sh (change "saleor" → "medusa")
- justfile (update commands)
- scripts/ directory (adapt scripts in later briefs)

TASK:
Update the monorepo for Medusa v2 local development.

1. Update docker-compose.yml — replace Saleor services with:

   medusa:
     - Build from ./medusa directory (Dockerfile)
     - Port 9000
     - Depends on: postgres, redis
     - Environment: DATABASE_URL, REDIS_URL, MEDUSA_ADMIN_CORS,
       STORE_CORS, COOKIE_SECRET, JWT_SECRET
     - Runs: npx medusa develop

   postgres:
     - Keep postgres:16-alpine
     - Update init script: create "medusa" and "payload" databases
     - Volume for persistence

   redis:
     - Keep redis:7-alpine

   meilisearch:
     - Image: getmeili/meilisearch:latest
     - Port 7700
     - Environment: MEILI_MASTER_KEY (for auth)
     - Volume for index persistence

   payload:
     - Keep from original (Brief 1.4 will build this out)
     - Port 3001

   Remove: saleor-api, saleor-worker, saleor-dashboard

2. Initialize Medusa v2 backend in medusa/ directory:
   - npx create-medusa-app@latest (or manual setup)
   - Configure: PostgreSQL, Redis, S3 file storage (local for dev)
   - Verify admin panel loads at http://localhost:9000/app
   - Create admin user

3. Update pnpm-workspace.yaml:
   - Replace sync-app with medusa
   - Packages: storefront, medusa, payload, scripts

4. Update .env.example:
   - Remove Saleor vars (SALEOR_API_URL, SALEOR_ADMIN_TOKEN, etc.)
   - Add Medusa vars (DATABASE_URL, REDIS_URL, COOKIE_SECRET, JWT_SECRET,
     MEDUSA_ADMIN_CORS, STORE_CORS)
   - Add MeiliSearch vars (MEILISEARCH_HOST, MEILISEARCH_API_KEY)
   - Keep Square vars, Payload vars, Stripe vars

5. Update justfile:
   - Replace Saleor-specific commands with Medusa equivalents
   - just up, just down, just logs, just ps
   - just medusa-migrate — run Medusa DB migrations
   - just setup-medusa — run setup script
   - just scan-inventory — start ISBN scanner app

ACCEPTANCE CRITERIA:
- `docker compose up` starts Medusa, Postgres, Redis, MeiliSearch without errors
- Medusa admin loads at http://localhost:9000/app
- Medusa store API responds at http://localhost:9000/store/products
- MeiliSearch dashboard loads at http://localhost:7700
- Postgres has both medusa and payload databases
- Redis is accessible

VERIFY BEFORE MOVING ON:
- Log into Medusa admin panel
- Create a test product via admin — verify it appears in store API
- Confirm MeiliSearch is reachable from Medusa container

Brief 1.2 — Medusa Product Types, Categories, and Region Setup

CONTEXT:
You have a running Medusa instance (from Brief 1.1). Dungeon Books sells books,
RPG products, comics/graphic novels, merchandise, and gift cards. The data
definitions from the original setup-saleor.ts (670 lines) are reusable — same
product types, categories, attributes. The API calls change from Saleor GraphQL
to Medusa admin REST API.

TASK:
Adapt scripts/setup-saleor.ts → scripts/setup-medusa.ts. Same data, different API.

1. Product Types — create via POST /admin/product-types:
   - Book
   - RPG Product
   - Comic / Graphic Novel
   - Merchandise
   - Gift Card

2. Product Categories — create via POST /admin/product-categories:
   Same hierarchy as original:
   Books (Science Fiction, Fantasy, Horror, Literary Fiction, Non-Fiction)
   RPG (Dungeons & Dragons, Pathfinder, OSR, Other Systems, Accessories)
   Comics & Graphic Novels (Comics, Manga, Graphic Novels)
   Stationery & Gifts
   Zines
   Board Games

3. Stock Location — create via POST /admin/stock-locations:
   - Name: "Jersey City Store"
   - Address: 115 Brunswick St, Jersey City, NJ 07302

4. Region — create via POST /admin/regions:
   - Name: "United States"
   - Currency: USD
   - Countries: ["us"]

5. Sales Channel — create (or use default):
   - Name: "Online Store"
   - Associate with stock location and region

6. Shipping:
   - Shipping Profile: "Default"
   - Fulfillment Provider: manual (built-in)
   - Shipping Options:
     - "Standard Shipping" — flat rate $5.00
     - "Free Shipping" — conditional, orders over $50
     - "In-Store Pickup" — free

The script should:
- Be written in TypeScript (scripts/setup-medusa.ts)
- Use fetch() to call Medusa's Admin REST API
- Authenticate with admin API token or session
- Be idempotent — safe to run multiple times
- Reuse data structures from the old setup-saleor.ts where possible
- Log each step clearly

ACCEPTANCE CRITERIA:
- All 5 product types visible in Medusa admin
- Category tree visible in admin with correct hierarchy
- "Jersey City Store" stock location exists
- "United States" region with USD exists
- Shipping options configured
- Script runs cleanly a second time without creating duplicates

VERIFY BEFORE MOVING ON:
- Create one test product of type "Book" in admin
- Confirm product type assigned, category selectable
- Confirm it's in the "Online Store" sales channel
- Confirm inventory can be tracked at "Jersey City Store"
- Delete the test product

Brief 1.3 — ISBN Scanner App + Ingram/OpenLibrary Enrichment Tool

Strategy change (2026-03-12): Instead of importing the Square catalog (~1,700 items, many stale), Medusa starts clean via a physical re-inventory of the store. This tool is used for initial inventory AND ongoing product ingestion — build it once, use it forever. See plans/re-inventory-strategy.md.

CONTEXT:
The Medusa catalog starts empty. Products are added by scanning physical
inventory — staff walks the store with a phone or USB barcode scanner, scans
each book's ISBN, and the app creates a draft product in Medusa enriched with
metadata from Ingram Web Service or Open Library.

Non-ISBN items (dice, minis, totes, stickers) are entered manually via
Medusa admin.

This replaces the original Square→Medusa migration script. The Square catalog
stays as-is for POS — no cleanup required. The sync layer (Phase 3) only maps
ISBNs that exist in both systems, which after re-inventory is exactly the
active physical inventory.

STARTING POINT:
Fork Agilo's Medusa POS Starter (https://github.com/Agilo/medusa-pos-starter).
It's an Expo + React Native app (SDK 54, TypeScript, NativeWind) that already
has camera-based barcode scanning, Medusa v2 Admin API auth, product search,
and a setup wizard (region, sales channel, stock location). MIT licensed.

Strip: cart, checkout, order flow, payment processing, order history.
Keep: barcode scanner, Medusa API integration, auth, setup wizard, app scaffold.
Add: enrichment pipeline, draft product creation, scan session reporting.

TASK:
Fork the POS starter and adapt it into the ISBN scanner + enrichment app.

1. Scanner interface (fork of POS starter's scanning):
   - Keep camera-based barcode scanning (already implemented)
   - Add manual ISBN entry fallback (text field)
   - Scanned ISBN displayed with enrichment status (loading → success/error)
   - Quick actions: create draft, skip, flag for manual review
   - Scan session view: list of scanned ISBNs with status

2. Enrichment pipeline (server-side):
   - Query Ingram Web Service by ISBN for metadata:
     - title, author, publisher, cover image URL, description, list price,
       format (hardcover/paperback/etc.), page count, publication date,
       series, language
   - Fallback to Open Library API if Ingram unavailable or no result
   - Cache responses (.cache/enrichment.json or Redis) to avoid re-hitting APIs
   - Rate limiting for both APIs

3. Product creation in Medusa:
   - Create product as draft via POST /admin/products:
     - title, handle, description, status ("draft")
     - type_id (Book, RPG Product, etc.)
     - metadata: { author, publisher, page_count, publication_date,
       series, series_number, language }
     - options: [{ title: "Format", values: [detected format] }]
     - variants: [{ title, sku (ISBN), prices: [{ amount, currency_code }],
       options: { Format: "Hardcover" }, manage_inventory: true }]
   - Download and upload cover image via POST /admin/uploads
   - Set inventory at "Jersey City Store" stock location:
     POST /admin/inventory-items/{id}/location-levels with stocked_quantity=1
     (staff adjusts count during review)
   - Idempotency: check existing SKUs before creating (skip if ISBN exists)

4. Staff review workflow:
   - Scanned products land in Medusa admin as drafts
   - Staff reviews: verify metadata, adjust quantity, assign categories,
     set price (override Ingram list price if needed)
   - Staff publishes when satisfied

5. Reporting:
   - Session report: ISBNs scanned, products created, duplicates skipped,
     enrichment failures
   - Export as JSON for audit trail

ACCEPTANCE CRITERIA:
- Expo app builds and runs on iOS/Android (or Expo Go for dev)
- Manual ISBN entry creates a draft product with enriched metadata
- Enrichment populates title, author, publisher, cover image, price, format
  (where available from Ingram/Open Library)
- Duplicate ISBN scan skips gracefully with notification
- Created products appear in Medusa admin as drafts
- Staff can review, edit, and publish drafts
- Session report shows scan summary
- Non-ISBN items can be entered manually via Medusa admin (no tooling needed)

VERIFY BEFORE MOVING ON:
- Scan 10 ISBNs — verify draft products created with correct metadata
- Verify cover images display in Medusa admin
- Verify a duplicate scan is handled gracefully
- Verify enrichment fallback (Ingram → Open Library) works
- Verify staff can publish a draft and it appears via store API: GET /store/products

Brief 1.4 — Payload CMS Setup

CONTEXT:
You are setting up Payload CMS as the editorial content layer for Dungeon Books.
Payload manages blog posts, staff picks, reading lists, events, store
announcements, and static pages. It does NOT sync product data from Medusa —
editorial content references products by handle or ISBN.

Payload runs alongside Medusa in the Docker Compose environment (Brief 1.1
reserved a Postgres database and port for it).

TASK:
Set up the Payload CMS instance in the payload/ directory.

1. Initialize Payload:
   - npx create-payload-app@latest in the payload/ directory
   - Use the blank template
   - Database: postgres (connection string from .env: PAYLOAD_DATABASE_URI)
   - TypeScript

2. Create a Dockerfile for the payload/ directory:
   - Node 20 alpine base
   - Install dependencies, build, start
   - Expose port 3001

3. Update docker-compose.yml (from Brief 1.1):
   - Add the payload service building from ./payload
   - Depends on postgres
   - Port 3001
   - Environment: PAYLOAD_DATABASE_URI, PAYLOAD_SECRET, PAYLOAD_PUBLIC_SERVER_URL

4. Define collections (see rebuild-spec.md section 5 for full field definitions):
   - Staff Picks (staffPicks)
   - Blog Posts (blogPosts)
   - Reading Lists (readingLists)
   - Events (events)
   - Announcements (announcements)
   - Pages (pages)
   - Media (media) — upload collection

5. Configure access control:
   - Admin users: full CRUD on all collections
   - Public (API): read-only on published content
   - Lock down the admin panel with PAYLOAD_SECRET

6. Seed initial content (scripts/seed-payload.ts):
   - About page, Shipping page, FAQ page with placeholder content
   - One sample staff picks entry (use real product handles from Medusa if available)
   - One sample event
   - One active announcement: "Welcome to the new Dungeon Books website!"

ACCEPTANCE CRITERIA:
- Payload admin panel loads at http://localhost:3001/admin
- All 7 collections visible in the admin sidebar
- Can create, edit, and publish content in each collection
- Public API returns published content:
  GET http://localhost:3001/api/staff-picks?where[status][equals]=published
- Media uploads work
- Seed script populates initial content
- Unpublished content is NOT returned by the public API

VERIFY BEFORE MOVING ON:
- Create a staff pick with a real productHandle, verify API returns it
- Create a blog post with a featured image, verify image URL works
- Create an event, verify date handling
- Verify unpublished content is NOT returned

Brief 1.5 — MeiliSearch Integration (Port from Agilo Fashion Starter)

CONTEXT:
MeiliSearch is running in Docker (Brief 1.1). The Agilo Fashion Medusa starter
has a production-ready MeiliSearch integration: a custom Medusa module that
indexes products on CRUD events, plus a 177-line SearchField component with
autocomplete. Port the backend module into the Medusa backend.

The Agilo Fashion repo: https://github.com/Agilo/fashion-starter
Relevant paths:
- medusa/src/modules/meilisearch/ — MeiliSearch module
- medusa/src/subscribers/ — product indexing subscribers
- storefront/src/components/SearchField.tsx — autocomplete component (ported in Brief 2.2)

TASK:
Port the MeiliSearch backend integration into the Medusa backend.

1. Copy the MeiliSearch module from Agilo into medusa/src/modules/meilisearch/:
   - Service: MeiliSearch client, index management, product indexing
   - Configuration: searchable attributes, ranking rules

2. Configure searchable attributes for a bookstore:
   - title (highest weight)
   - subtitle
   - description
   - metadata.author
   - metadata.publisher
   - metadata.series
   - categories (names)
   - tags
   - variants.sku (ISBN — allows searching by ISBN)
   - type (product type name)

3. Copy/adapt product indexing subscribers:
   - product.created → index product in MeiliSearch
   - product.updated → update index
   - product.deleted → remove from index
   - Batch index on startup or via admin command

4. Add a full reindex script or admin API endpoint:
   - Fetch all products from Medusa
   - Batch index into MeiliSearch
   - Useful for initial setup and recovery

5. Configure MeiliSearch connection:
   - MEILISEARCH_HOST and MEILISEARCH_API_KEY from .env
   - Register module in medusa-config.ts

6. Run full reindex after initial inventory scan (Brief 1.3):
   - All scanned products should be searchable
   - Verify autocomplete returns relevant results for title, author, ISBN queries

ACCEPTANCE CRITERIA:
- MeiliSearch module registered in Medusa
- Product indexing subscribers fire on product CRUD
- All published products indexed (verify count matches)
- Search returns relevant results:
  - "Ursula Le Guin" → finds her books
  - "978-0-..." (ISBN prefix) → finds the specific book
  - "dungeons" → finds D&D products
  - Typo tolerance: "urslua le guin" still returns results
- Full reindex completes in <60 seconds for the catalog

VERIFY BEFORE MOVING ON:
- Query MeiliSearch directly: curl http://localhost:7700/indexes/products/search?q=fantasy
- Verify results include product data with thumbnails and prices
- Create a new product in Medusa admin → verify it appears in MeiliSearch within seconds
- Delete a product → verify it disappears from MeiliSearch

Brief 1.6 — Stripe and PayPal Checkout Verification

CONTEXT:
Solace ships with working Stripe + PayPal checkout. Before proceeding with the
storefront fork, verify payments work end-to-end against the Medusa backend.

This is a verification brief, not a build brief. The payment infrastructure
already exists in Solace and Medusa.

TASK:

1. Configure Stripe in Medusa:
   - Install @medusajs/payment-stripe (if not already included)
   - Set STRIPE_API_KEY in .env (test mode key)
   - Enable Stripe payment provider for the "United States" region

2. Configure PayPal in Medusa (optional for MVP, Stripe is primary):
   - Set PAYPAL_CLIENT_ID, PAYPAL_SECRET
   - Enable PayPal payment provider

3. Verify checkout via Medusa admin or API:
   - Create a cart: POST /store/carts
   - Add item: POST /store/carts/{id}/line-items
   - Set shipping address and method
   - Create payment session: POST /store/carts/{id}/payment-sessions
   - Complete: POST /store/carts/{id}/complete
   - Verify payment in Stripe Dashboard (test mode)

4. Configure email (Resend):
   - Port Resend notification module from Agilo Fashion starter
   - Set RESEND_API_KEY in .env
   - Verify order confirmation email sends after checkout

ACCEPTANCE CRITERIA:
- Full checkout flow works via API
- Payment appears in Stripe Dashboard
- Order created in Medusa admin
- Order confirmation email sent via Resend
- Gift card creation works (Medusa built-in)

VERIFY BEFORE MOVING ON:
- Complete 2 test orders with different products
- Verify orders in Medusa admin
- Verify Stripe webhook events received
- Verify email delivery

Brief 1.7 — Railway Staging Deployment

CONTEXT:
Local development is working (Briefs 1.1-1.6). Deploy a staging environment
on Railway. Simpler than the original Saleor plan — 4 services instead of 6+.

TASK:
Deploy all services to Railway staging environment.

1. Railway project setup:
   - Create "dungeon-books-staging" project
   - Add services from the monorepo

2. Deploy services:
   - Medusa Server: from ./medusa, port 9000
   - Next.js Frontend: from ./storefront, port 3000 (Brief 2.1 will populate this)
   - Payload CMS: from ./payload, port 3001
   - MeiliSearch: Docker image getmeili/meilisearch
   - PostgreSQL: Railway plugin (two databases: medusa, payload)
   - Redis: Railway plugin

3. Configure environment:
   - Medusa: DATABASE_URL, REDIS_URL, COOKIE_SECRET, JWT_SECRET,
     STRIPE_API_KEY, MEILISEARCH_HOST, MEILISEARCH_API_KEY,
     STORE_CORS, MEDUSA_ADMIN_CORS
   - Payload: PAYLOAD_DATABASE_URI, PAYLOAD_SECRET, PAYLOAD_PUBLIC_SERVER_URL
   - Storefront: NEXT_PUBLIC_MEDUSA_BACKEND_URL, NEXT_PUBLIC_MEILISEARCH_HOST

4. Run setup on staging:
   - Run scripts/setup-medusa.ts against staging
   - Run scripts/seed-payload.ts
   - Trigger MeiliSearch full reindex
   - Deploy ISBN scanner app (Brief 1.3) — initial inventory scan happens in-store

5. Document deployment in docs/deployment.md

ACCEPTANCE CRITERIA:
- Medusa admin loads at staging URL
- Store API responds
- MeiliSearch returns search results
- Payload admin loads
- Scanner app accessible from staging URL
- Stripe checkout works on staging

VERIFY BEFORE MOVING ON:
- Complete a full checkout on staging via API
- Verify search works
- Verify Payload content accessible
- Check Railway logs for errors

Phase 2: Storefront

Brief 2.1 — Fork Solace and Replace Strapi with Payload

CONTEXT:
Solace is the Medusa v2 starter being used as the storefront foundation. It has
deep Strapi CMS integration (homepage sections, blog, about, FAQ, terms, privacy,
webhook revalidation). You need to replace all Strapi data sources with Payload.

Solace repo: https://github.com/rigby-sh/solace-medusa-starter

Solace's Strapi touchpoints:
- Data fetching functions in src/lib/ (~15-20 fetch calls)
- Content components in src/modules/content/
- Blog system in src/modules/blog/
- Homepage sections pulling from CMS
- /api/strapi-revalidate webhook endpoint
- Type definitions for Strapi responses

TASK:

1. Fork Solace into storefront/:
   - Clone the repo into storefront/
   - Remove .git directory
   - Install dependencies with pnpm
   - Configure .env with local Medusa backend URL
   - Remove [countryCode] routing prefix — Dungeon Books is US-only.
     Move all routes from /[countryCode]/... to /... (flatten the
     App Router directory structure). Hardcode the US region in the
     Medusa SDK client config instead of deriving it from the URL.
   - Verify it builds (will have Strapi errors — expected)

2. Replace Strapi data layer with Payload:

   Create src/lib/payload.ts — typed fetch wrapper:
   - Base URL from env: NEXT_PUBLIC_PAYLOAD_API_URL
   - Methods: getStaffPicks(), getBlogPosts(), getEvents(), getPage(), etc.
   - Handle Payload's response format (simpler than Strapi's nested attributes)

   Replace each Strapi fetch call:
   - Homepage hero/banner → Payload announcements or featured content
   - Homepage collections imagery → keep from Medusa (collections already work)
   - About Us page → Payload pages collection (slug: "about-us")
   - FAQ page → Payload pages collection (slug: "faq")
   - Privacy Policy → Payload pages collection
   - Terms & Conditions → Payload pages collection
   - Blog listing → Payload blogPosts collection
   - Blog detail → Payload blogPosts by slug
   - Blog categories → Payload tags (or custom taxonomy)

3. Replace /api/strapi-revalidate with /api/payload-revalidate:
   - Payload sends webhooks on content publish
   - Revalidate relevant Next.js cache tags

4. Update type definitions:
   - Remove Strapi types
   - Add Payload collection types matching the collections from Brief 1.4

5. Remove Strapi dependencies:
   - Remove any @strapi packages from package.json
   - Clean up unused imports

6. Add to docker-compose.yml:
   - storefront service building from ./storefront
   - Port 3000
   - Depends on: medusa
   - Environment: NEXT_PUBLIC_MEDUSA_BACKEND_URL, NEXT_PUBLIC_PAYLOAD_API_URL,
     NEXT_PUBLIC_MEILISEARCH_HOST, NEXT_PUBLIC_STRIPE_KEY

ACCEPTANCE CRITERIA:
- Storefront loads at http://localhost:3000 with products from Medusa
- Blog listing page renders posts from Payload (not Strapi)
- About Us, FAQ, Privacy, Terms pages render from Payload
- Homepage loads without Strapi errors
- No Strapi references remain in the codebase
- Payload webhook revalidation works (edit content → page updates)

VERIFY BEFORE MOVING ON:
- Browse products — verify cards, PDP, cart, checkout all work
- Visit blog page — verify posts from Payload render
- Visit about page — verify content from Payload renders
- Edit a blog post in Payload admin → verify storefront updates
- Verify no console errors related to Strapi

Brief 2.2 — Port MeiliSearch SearchField from Agilo

CONTEXT:
MeiliSearch backend module is running and indexing products (Brief 1.5). Solace
has basic Medusa search (submit-based, no autocomplete). Agilo Fashion has a
177-line SearchField component with autocomplete (React Aria ComboBox, async
search, product thumbnails and prices in dropdown).

Port Agilo's search UI into the Solace storefront.

TASK:

1. Install meilisearch client in storefront:
   - npm install meilisearch

2. Port SearchField from Agilo:
   - Copy src/components/SearchField.tsx from Agilo Fashion
   - Adapt styling to Solace's design tokens and dark mode
   - Adapt product data mapping (Agilo may use different field names)

3. Replace Solace's search components:
   - Replace src/modules/search/components/search-box/ with SearchField
   - Replace or augment search-dropdown with MeiliSearch results
   - Keep mobile search dialog structure, update to use MeiliSearch

4. Update search results page:
   - /[countryCode]/results/[query] should query MeiliSearch
   - Display results with existing product grid components
   - Maintain filters and sorting from Solace's store template

5. Configure MeiliSearch client:
   - NEXT_PUBLIC_MEILISEARCH_HOST from .env
   - NEXT_PUBLIC_MEILISEARCH_API_KEY (search-only key)

ACCEPTANCE CRITERIA:
- Typing in search bar shows autocomplete dropdown with product thumbnails and prices
- Autocomplete appears after 2+ characters, updates as you type
- Clicking a result navigates to the PDP
- Pressing Enter navigates to full search results page
- Search results page shows full product grid with MeiliSearch results
- Typo tolerance works ("urslua" still finds "Ursula")
- Mobile search dialog uses MeiliSearch
- Dark mode styling correct

VERIFY BEFORE MOVING ON:
- Search for a book by title — verify autocomplete shows it
- Search for an author — verify their books appear
- Search by ISBN — verify the specific book appears
- Search with a typo — verify tolerance works
- Test on mobile — verify search dialog works
- Test with dark mode — verify styling

Brief 2.3 — Reskin with “Indie Publisher” Brand

CONTEXT:
The storefront is functional with Medusa data, Payload content, and MeiliSearch.
Now apply the Dungeon Books visual identity. See rebuild-spec.md section 3 for
full design direction.

Solace uses a custom Tailwind preset (preset/plugins/colors.js) with CSS
custom properties for light/dark mode. next-themes handles dark mode toggle.

TASK:

1. Typography:
   - Replace Solace's default font with:
     - Display/headings: Choose one serif (Playfair Display, Fraunces,
       Instrument Serif, or Gambetta)
     - Body/UI: Choose one humanist sans-serif (Satoshi, General Sans,
       Switzer, or Cabinet Grotesk)
     - Monospace: JetBrains Mono or IBM Plex Mono (prices, ISBNs only)
   - Install via next/font/google or @fontsource
   - Update Tailwind config fontFamily: sans, serif, mono
   - Apply serif to all headings, sans to body, mono to data fields

2. Color palette — update preset/plugins/colors.js:
   - Light mode and dark mode tokens per the spec's color palette
   - Map Solace's semantic tokens (--bg-primary, --content-action-primary, etc.)
     to the Dungeon Books palette

3. Layout:
   - Set max content width to 1280px
   - Set border radius to 6px (not aggressively rounded)
   - Remove heavy shadows except on hover states and drawers
   - Product grid: 4 columns desktop, 2 mobile, 24-32px gap

4. Logo and branding:
   - Replace Solace logo with Dungeon Books logo/wordmark
   - Update site name, meta title: "Dungeon Books | Sci-fi, Fantasy & RPG Bookstore"
   - Update favicon
   - Update footer: store address, hours, social links

5. Bookstore-specific adjustments:
   - Product cards: book cover images at natural aspect ratio (no forced squares)
   - PDP: prominent ISBN display (mono font), author below title, publisher in metadata
   - Remove fashion-specific UI (Solace is fashion-neutral but check for any)
   - Navigation: update menu categories to match Dungeon Books structure

6. Remove Solace branding:
   - Replace "Solace" references in copy, meta tags, README
   - Update social links, copyright

ACCEPTANCE CRITERIA:
- Typography: serif headings, sans body, mono prices/ISBNs
- Color palette matches spec (warm off-white background, burnt orange accent)
- Dark mode works with specified dark palette
- Product cards show book covers at natural aspect ratio
- PDP shows ISBN, author, publisher
- Navigation reflects Dungeon Books categories
- Mobile responsive (375px, 768px, 1024px, 1440px)
- No Solace branding visible

VERIFY BEFORE MOVING ON:
- Browse all category pages — verify products display correctly
- Click through to 5+ PDPs across different product types
- Add items to cart, verify cart works
- Toggle dark mode — verify all colors correct
- Test on mobile viewport

Brief 2.4 — Homepage (Blended Commerce + Editorial)

CONTEXT:
The storefront has the Dungeon Books brand (Brief 2.3). The homepage needs to be
editorial-first — blending Medusa commerce data with Payload editorial content.

TASK:
Build a custom homepage.

1. Data fetching (server component, parallel queries):

   From Medusa (SDK):
   - "New Arrivals" collection: 8 most recently added products
   - Featured product: single product for hero (collection "Homepage Hero" or
     hardcode a handle initially)

   From Payload (REST API):
   - Current staff picks (most recent published staffPicks entry)
   - Upcoming events (next 3 published, ordered by date asc)
   - Active announcements (where active=true, date range valid)

2. Layout sections (top to bottom):

   a. Announcement bar (if active announcements)
   b. Hero section (featured product or editorial pick)
   c. Staff Picks ("Staff Picks — {month} {year}")
   d. New Arrivals (8 product cards)
   e. Upcoming Events (3 event cards)
   f. Newsletter signup

3. Staff Picks data merging:
   - Payload returns picks with productHandle
   - Fetch each product from Medusa by handle
   - Merge: editorial (staffName, review, pullQuote) + commerce (image, price)
   - Handle missing products gracefully

4. Create shared helper:
   async function getProductsByHandles(handles: string[])
   - Batch fetch from Medusa, return Map<handle, Product>
   - Used by staff picks, reading lists, blog posts

ACCEPTANCE CRITERIA:
- Homepage loads with all sections populated
- Staff picks merge Payload editorial + Medusa commerce data
- New arrivals from Medusa collection
- Events from Payload
- Newsletter signup form
- Responsive (mobile stacks, staff picks horizontal scroll)
- Loads in <2 seconds

VERIFY BEFORE MOVING ON:
- All product links navigate to correct PDPs
- Staff picks show attribution and pull quotes
- Toggle dark mode — verify all sections
- Test on mobile
- Edit a staff pick in Payload with a non-existent handle — page still loads

Brief 2.5 — Editorial Pages (Staff Picks, Reading Lists)

CONTEXT:
The homepage blends data (Brief 2.4). Blog pages already exist from the Solace
fork (via the Strapi→Payload swap in Brief 2.1). Now build the staff picks and
reading lists pages — the two editorial page types that don't come from Solace.

TASK:

1. Staff Picks — /staff-picks and /staff-picks/[slug]:

   Listing: fetch all published staffPicks, display title/date/intro, link to detail.

   Detail: for each pick, fetch product from Medusa by handle. Render product
   image + title + price alongside staff name + review + pull quote. Include
   "Add to Cart" button per pick.

2. Reading Lists — /reading-lists and /reading-lists/[slug]:

   Listing: fetch all published readingLists, display title/description/image/count.

   Detail: for each item (ordered by position), fetch product from Medusa.
   Render position number + product card + editorial note. Include "Add to Cart".

3. Use the getProductsByHandles() helper from Brief 2.4 for data merging.

4. Create a Payload API client module:
   src/lib/payload.ts with typed methods for each collection.

ACCEPTANCE CRITERIA:
- All 4 routes render with merged Payload + Medusa data
- Product images, prices, availability are live from Medusa
- Editorial content renders from Payload
- Add to Cart works on all product cards
- Missing product handles don't break pages
- Responsive, dark mode works

VERIFY BEFORE MOVING ON:
- Create 2 staff picks entries with real product handles
- Create a reading list with 5+ items, verify ordering
- Test Add to Cart from staff picks page
- Test broken handle — graceful handling

Brief 2.6 — Events Pages

CONTEXT:
Events are managed in Payload CMS (from Brief 1.4). Build the display pages.
Same as the original Brief 2.4 — the content is platform-agnostic.

TASK:
Build /events and /events/[slug]. See original Brief 2.4 for full spec.

Key points:
- Split into Upcoming and Past sections on listing
- Detail page: full content, ticket CTA, related products from Medusa
- Cancelled events show cancelled banner
- JSON-LD Event structured data on detail pages

ACCEPTANCE CRITERIA:
- Listing shows upcoming prominently, past collapsed
- Detail renders full content with ticket CTA
- Related products display with live Medusa data
- Cancelled events show cancelled state
- JSON-LD present on detail pages
- Mobile responsive

Brief 2.7 — Static Pages, Navigation, and SEO

CONTEXT:
The storefront has commerce + editorial pages. Now: static pages from Payload,
site navigation, and SEO improvements. Solace already has a mega menu nav,
footer, and basic SEO — adapt these.

TASK:

1. Static pages from Payload — catch-all or explicit routes:
   - /about-us, /faq, /shipping, /contact, /privacy-policy, /terms-and-conditions
   - Fetch from Payload pages collection by slug
   - Render rich text in a centered, readable layout

2. Navigation update:
   - Solace has mega menu — update with Dungeon Books categories:
     Shop (dropdown): Books, RPG, Comics & GN, Stationery, Zines, Board Games
     Events, Blog, About
   - Mobile: hamburger → slide-out, same links nested
   - Keep search, account, cart icons from Solace

3. Footer update:
   - Store info: Dungeon Books, 115 Brunswick St, Tues-Sun 12-8pm
   - Shop links, Info links, Social links
   - Newsletter signup (compact inline form)
   - Bookshop.org and Libro.fm links

4. SEO:
   - Add JSON-LD Product structured data on PDP (Solace doesn't have this)
   - Add JSON-LD Event on event detail pages (from Brief 2.6)
   - Verify sitemap includes all page types
   - Verify OG tags on all pages
   - Custom 404 page with Dungeon Books branding

ACCEPTANCE CRITERIA:
- Navigation works on desktop and mobile with correct categories
- All static pages render from Payload
- Footer on all pages with correct info
- JSON-LD on product and event pages
- Sitemap at /sitemap.xml
- 404 page branded
- All internal links work

VERIFY BEFORE MOVING ON:
- Click every nav link desktop and mobile
- Verify static pages render
- Check JSON-LD via Google Rich Results Test
- Check /sitemap.xml

Brief 2.8 — Wishlist

CONTEXT:
Medusa v2 has no built-in wishlist. Use customer metadata (same pattern as
the original Saleor approach, adapted for Medusa's API).

TASK:

1. Backend: store wishlist in customer metadata
   - key: "wishlist", value: JSON array of product IDs

2. Frontend:
   - useWishlist() hook: add, remove, check, optimistic updates
   - Heart icon toggle on ProductCard and PDP
   - /account/wishlist page: grid of wishlisted products with Remove and Add to Cart
   - Login required (redirect if not authenticated)

ACCEPTANCE CRITERIA:
- Heart icon on product cards and PDP
- Toggle adds/removes from wishlist (logged in)
- /account/wishlist shows all wishlisted products
- Persists across sessions (stored in Medusa customer metadata)
- Redirect to login when not authenticated

VERIFY BEFORE MOVING ON:
- Add 5 products from different pages
- Verify all appear on wishlist page
- Remove 2, verify removal
- Log out, log in — verify persistence
- Add to cart from wishlist

Brief 2.9 — Gift Cards, Newsletter, and Email Templates

CONTEXT:
Three remaining features bundled into one brief — each is small.

TASK:

1. Gift card page (/gift-cards):
   - Display Medusa gift card product with denomination selector
   - Add to Cart, purchase through normal checkout
   - Gift card code delivered via email
   - Balance check: enter code, query Medusa gift card API
   - Note in checkout: "Have a physical Dungeon Books gift card?
     Email orders@dungeonbooks.com to apply it."

2. Newsletter signup:
   - Choose platform (Buttondown or Resend Audiences)
   - API route: POST /api/newsletter with { email }
   - Wire homepage signup form (from Brief 2.4)
   - Wire footer signup form
   - Checkout opt-in checkbox: subscribe on order completion

3. Email templates (port from Agilo if not done in Brief 1.6):
   - Verify Resend module sends branded emails
   - Update React Email templates with Dungeon Books logo, colors, footer
   - Templates: welcome, order confirmation, shipping notification,
     password reset, gift card issued

ACCEPTANCE CRITERIA:
- Gift card purchasable and redeemable
- Newsletter signup works from homepage, footer, and checkout
- Email templates branded and sending correctly

VERIFY BEFORE MOVING ON:
- Purchase a $25 gift card, verify email with code
- Apply code to new order, verify credit applied
- Sign up for newsletter via all 3 paths
- Verify email templates render correctly

Phase 3: Square POS Sync

Brief 3.1 — Square Sync Module: Square → Medusa

CONTEXT:
Square POS sync runs as an in-process Medusa module — not a standalone service.
This brief implements the Square → Medusa direction and the module scaffolding.

TASK:

1. Create the Square sync module:
   medusa/src/modules/square-sync/
   ├── service.ts           # SquareSyncService
   └── models/sync-log.ts   # Sync event tracking

   SquareSyncService:
   - Square API client (using @square/square SDK)
   - ISBN → (Square variation ID, Medusa variant ID) mapping
   - Cache mapping in memory, refresh hourly
   - Sync event logging to sync_log table

2. Create Square webhook receiver:
   medusa/src/api/webhooks/square/route.ts
   - POST endpoint for inventory.count.updated
   - Verify Square webhook HMAC-SHA256 signature
   - Return 200 immediately, process async

3. Implement Square → Medusa sync:
   - Parse webhook: extract variation ID, location ID, new quantity
   - Filter: only process events for SQUARE_LOCATION_ID
   - Look up ISBN → Medusa variant via SquareSyncService
   - Update Medusa inventory via InventoryModuleService (set absolute count)
   - Log event to sync_log

4. Error handling:
   - Unknown ISBN: log warning, store in sync_log with error status
   - Medusa API failure: store in sync_log for retry during reconciliation
   - Don't retry inline

5. Health/status endpoint:
   medusa/src/api/admin/sync-status/route.ts
   - GET: return last sync event, queue depth, error count

6. Register Square webhook:
   - Via Square Developer Dashboard
   - Subscribe to inventory.count.updated
   - For local dev: use ngrok to expose medusa port

ACCEPTANCE CRITERIA:
- Adjusting Square inventory triggers webhook that updates Medusa within 10 seconds
- Products without ISBNs are logged, not crashed on
- Medusa API failures are logged in sync_log
- Health endpoint returns sync status

VERIFY BEFORE MOVING ON:
- Adjust Square inventory for 3 products via Square Dashboard
- Verify Medusa inventory matches within 10 seconds
- Adjust inventory for a product with no ISBN — verify it's logged

Brief 3.2 — Medusa → Square Sync

CONTEXT:
Square → Medusa sync works (Brief 3.1). Implement the reverse: online order
decrements Square POS inventory.

TASK:

1. Create order.placed subscriber:
   medusa/src/subscribers/order-placed.ts
   - Listen for order.placed event
   - For each line item: look up ISBN, look up Square variation ID
   - Skip items with no mapping (might be Ingram-only in future)

2. Update Square inventory:
   - Call Square inventory.batchCreateChanges() with ADJUSTMENT type
   - Decrement by quantity sold (negative adjustment)
   - Use ADJUSTMENT (relative), not SET (absolute)

3. Sync loop prevention:
   - After Medusa→Square update, store (ISBN, timestamp) in Redis (TTL 30s)
   - Square→Medusa handler checks Redis — skip if recently synced FROM Medusa

4. Error handling:
   - Square API failures: log to sync_log, continue processing other line items
   - Dead letter for retry during reconciliation

ACCEPTANCE CRITERIA:
- Online order decrements Square inventory for each line item
- Products without ISBNs skipped gracefully
- No infinite sync loop (loop prevention working)
- Multiple line items in one order processed correctly

VERIFY BEFORE MOVING ON:
- Place an order with 2 items on staging
- Verify Square inventory decremented for both
- Verify no duplicate sync events in logs
- Place order with 1 book + 1 non-ISBN item — book syncs, other skipped

Brief 3.3 — Nightly Reconciliation

CONTEXT:
Bidirectional sync works (Briefs 3.1, 3.2). Nightly reconciliation catches
missed events and retries failures.

TASK:

1. Reconciliation scheduled job:
   medusa/src/jobs/nightly-reconcile.ts
   - Runs at 3 AM ET daily (Medusa scheduled jobs)
   - Fetch all inventory from Square (batched by 100)
   - Fetch all inventory from Medusa "Jersey City Store"
   - Compare by ISBN/SKU
   - Auto-correct discrepancy ≤1 (Saleor matches Square — Square is source of truth)
   - Alert on discrepancy >1 (email via Resend)
   - Log all corrections

2. Dead letter retry:
   - Before reconciliation, retry failed sync_log events (retries < 5)
   - On success: mark processed
   - On failure: increment retries
   - Events with retries ≥5: mark permanently failed, include in alert

3. Expose manual trigger:
   - POST /admin/reconcile endpoint (auth required)

4. Reconciliation report:
   - Store in sync_reconciliation table
   - Items compared, in sync, corrected, large discrepancies, orphans

ACCEPTANCE CRITERIA:
- Reconciliation runs on schedule and manually
- Auto-corrects small discrepancies
- Alerts on large discrepancies
- Dead letter events retried
- Full run completes in <5 minutes for the catalog

VERIFY BEFORE MOVING ON:
- Manually set a wrong inventory value in Medusa
- Run reconciliation via POST /admin/reconcile
- Verify corrected to match Square
- Add a failed sync_log event, verify it's retried

Phase 4: Polish and Launch

Brief 4.1 — QA and Cross-Browser Testing

CONTEXT:
All features are built. Structured QA pass before launch.

TASK:
Systematically test every user flow on staging. Same scope as the original
Brief 4.1 — commerce flows, editorial flows, cross-browser, performance,
SEO, accessibility, error states.

Additional items for Medusa path:
- MeiliSearch autocomplete works across browsers
- Stripe + PayPal checkout both work
- Dark mode toggle works
- Lightbox gallery on PDP works
- Blog with MDX rendering works

Document all bugs in docs/qa-bugs.md with priorities P0/P1/P2.

ACCEPTANCE CRITERIA:
- All flows tested and documented
- Zero P0 bugs
- All P1 bugs fixed
- P2 bugs documented for post-launch

Brief 4.2 — Content Population

CONTEXT:
Same as original Brief 4.2 — populate Payload with real editorial content.

TASK:
Populate in Payload:
1. Staff Picks: current month (3-5 books with real reviews)
2. Reading Lists: at least 2 for launch
3. Blog: launch post + 1 additional
4. Events: all currently scheduled
5. Static pages: About, Shipping, FAQ, Contact with real content
6. Announcements: launch announcement
7. Homepage hero: choose featured product

ACCEPTANCE CRITERIA:
- Homepage loads with real content in all sections
- No placeholder text visible anywhere

Brief 4.3 — Production Deployment and DNS Cutover

CONTEXT:
Staging is QA'd and content populated. Deploy production and cut over DNS.
Simpler than the original — 4 services, no standalone sync app.

TASK:

1. Production Railway project:
   - Deploy: Medusa, Storefront, Payload, MeiliSearch, PostgreSQL, Redis
   - Production Stripe keys, production Resend
   - Strong secrets for COOKIE_SECRET, JWT_SECRET, PAYLOAD_SECRET

2. Production data:
   - Run setup-medusa.ts and migrate-products.ts against production
   - Seed Payload content (or copy from staging)
   - Full MeiliSearch reindex
   - Verify product count

3. Square webhook registration:
   - Point Square webhooks to production Medusa URL
   - Run initial reconciliation

4. DNS cutover:
   - dungeonbooks.com → production storefront (Cloudflare)
   - api.dungeonbooks.com → Medusa
   - cms.dungeonbooks.com → Payload admin
   - Redirects: beta → main, www → main

5. Post-launch monitoring (48 hours):
   - Railway logs, Stripe Dashboard, sync health, reconciliation job
   - Google Search Console, Cloudflare analytics

6. Rollback plan:
   - Point DNS back to old Square site if critical issues
   - Document in docs/runbook.md

ACCEPTANCE CRITERIA:
- Production live at dungeonbooks.com
- All services healthy
- Stripe processing real payments
- Square sync processing real inventory events
- Email delivery working
- SSL and redirects working

VERIFY BEFORE MOVING ON:
- Place a real test order (refund after)
- Verify in Medusa admin, Stripe Dashboard, email
- Verify Square inventory decremented
- Verify reconciliation runs overnight
- Check from different device/network

Summary: All Briefs

#BriefPhaseEst. Duration
1.1Docker Compose + Monorepo UpdateFoundation0.5 day
1.2Medusa Product Types + CategoriesFoundation0.5 day
1.3ISBN Scanner App + Enrichment ToolFoundation2-3 days
1.4Payload CMS SetupFoundation1-2 days
1.5MeiliSearch IntegrationFoundation1 day
1.6Stripe/PayPal Checkout VerificationFoundation0.5 day
1.7Railway Staging DeploymentFoundation0.5-1 day
2.1Fork Solace + Strapi→PayloadStorefront1.5-2 days
2.2Port MeiliSearch SearchFieldStorefront0.5-1 day
2.3Reskin with “Indie Publisher” BrandStorefront2-4 days
2.4Homepage (Blended Data)Storefront2-3 days
2.5Editorial Pages (Staff Picks, Reading Lists)Storefront2-3 days
2.6Events PagesStorefront1-2 days
2.7Static Pages + Navigation + SEOStorefront1.5-2 days
2.8WishlistStorefront1-2 days
2.9Gift Cards + Newsletter + EmailStorefront1.5-2 days
3.1Square Sync Module: Square → MedusaSync2-3 days
3.2Medusa → Square SyncSync1.5-2 days
3.3Nightly ReconciliationSync1-1.5 days
4.1QA + Cross-Browser TestingLaunch3-5 days
4.2Content PopulationLaunch2-3 days
4.3Production Deploy + DNSLaunch1-2 days
Total~27-40 days

Comparison to Original (Saleor) Estimates

MetricSaleor PathMedusa PathDelta
Total briefs2122+1 (MeiliSearch)
Phase 1 (Foundation)6-8 days5.5-7 days-1 day
Phase 2 (Storefront)17-25 days13-21 days-4 days (Solace has blog, CMS, payments)
Phase 3 (Sync)7-11 days5-6.5 days-3 days (in-process, no standalone app)
Phase 4 (Launch)6-10 days6-10 dayssame
Total35-50 days27-40 days~8-10 days saved
Railway services6+4-2+
Standalone integration services30-3
Monthly infrastructure~$110~$75-$35/mo