Hana-Bi — E-Commerce Platform (Technical Architecture)
Live production e-commerce site — Stripe as the actual source of truth for product data, not just a payment processor bolted onto a CMS

- Stack
- Next.js 15 (App Router), TypeScript, hosted on Vercel
- Payments
- Stripe (Checkout Sessions + webhooks) — deliberately the source of truth for product data, not a separate CMS; product metadata (slug, status, sizes, story, etc.) lives directly on Stripe products, fetched via an autopaging
getStripeCatalog(), with a local fallback ifSTRIPE_SECRET_KEYis unset - Cart state
- Zustand, persisted client-side
- Resend, webhook-driven transactional confirmation on
checkout.session.completed - Serverless-safe verification
- checkout sessions are verified with
stripe.checkout.sessions.retrieve()against the Stripe API, not an in-process cache (the bug story is in the narrative below) - Content model
- a deliberate three-way split — Shop (current products, from Stripe), Archive (sold-out/past drops, Stripe filtered by status), Projects (personal experiments, local data only, never for sale) — an intentional architecture decision, not an accident
- Live site
- hanabiny.com
Why Stripe is the source of truth
Most e-commerce builds pair a headless CMS with Stripe bolted on just for checkout — which means two systems to keep in sync: change a price or retire a size in the CMS, and you have to remember to make the same change in Stripe, or the storefront and the actual checkout drift apart. I collapsed that into one system: product metadata (slug, status, sizes, story) lives directly on the Stripe Product object, and getStripeCatalog() autopages through Stripe's product list and treats that as the entire catalog. There's nothing else to keep in sync because there's nothing else. The tradeoff is real — Stripe's metadata fields aren't built to be a CMS, no rich content blocks, size limits — but Hana-Bi's product pages are simple enough that a real CMS would have been solving a problem I didn't have.
The serverless bug
The first version of session verification kept a Set of confirmed session IDs in memory, checked on each request. It worked in local dev and broke silently in production — a customer would complete checkout, land on the confirmation page, and get told their session wasn't verified. The bug: every serverless function invocation can spin up a fresh instance with its own memory, so the Set that recorded the session as verified in one invocation didn't exist in the next one handling the confirmation page. It wasn't a code bug in the traditional sense — the logic was correct, it just assumed a persistent process that serverless doesn't give you. The fix was to stop trying to remember anything myself: call stripe.checkout.sessions.retrieve() directly against the Stripe API on each check, so verification asks the source of truth instead of a local cache of it. The lesson generalizes: any "remember this happened" state in a serverless route needs to live in something external, never in-process.

