All projects

Case study · 5 min read

LUMES

A storefront that takes payment safely and an admin one person can run.

LUMES storefront home page with the headline "A premium wardrobe shaped by grace, ease, and intention"
My role
Designed and built end to end: storefront, admin dashboard, payments and email.
Timeline
Mar 2026
Stack
Next.js (App Router), TypeScript, MongoDB, NextAuth, Stripe, Brevo, TipTap, Zustand, TanStack Query, Tailwind CSS

LUMES is a modest-fashion store (abayas, dresses, kaftans, hijabs) with a custom admin dashboard. Customers browse, pay with Stripe in the currency of their location and get order updates by email. The owner manages products, orders, customers, coupons and even the storefront copy without touching code.

The problem

A small fashion brand needs two things that are usually separate projects: a storefront customers enjoy, and a back office the owner can run alone. Getting money handling wrong is the costly mistake: charging for stock that is gone, or losing track of a paid order.

I built LUMES to see how complete a store I could make on my own while keeping the payment path trustworthy.

Constraints

  • Payments must be right. Orders and stock have to survive retries, closed tabs and missed webhooks.
  • A non-technical owner. Everything that changes (products, copy, coupons, shipping fees) has to be editable in the admin.
  • Customers in several countries. Prices have to be shown in a currency that makes sense for each visitor.
  • One developer, about two weeks.

Approach

The storefront and the admin live in one Next.js App Router project. Data sits in MongoDB (users, products with variants, categories, orders with a timeline, coupons, notifications and the storefront content). NextAuth handles sign-in, and middleware protects the admin API, checkout and account pages. Stripe Checkout takes payment, a webhook confirms it, and Brevo sends branded order emails.

The money path is deliberately short and checked at each step: quote, pending order, Stripe session, signed webhook, paid order.

The payment path

Each step checks the one before it; the reconcile route covers a missed webhook.

In the app

The shop.
A product page with variant selection.
The storefront on a phone.

Key decisions

01Trust the webhook, but keep a way back

Context
Payment confirmation can fail in two ways: Stripe retries an event, or the customer closes the tab and the event is never seen.
What I chose
The webhook verifies Stripe's signature on the raw body and records each processed event id, so a repeated event is ignored. A separate reconcile route asks Stripe about the session directly and runs the same "mark as paid" logic, so a missed webhook can be recovered.
What I passed on
Mark the order paid when the customer lands on the success page. Easy, and easy to fool or miss.
Trade-off
Idempotency is check-then-record, not atomic, so two simultaneous deliveries could both pass the check. A unique insert before processing would be stronger, and it is on my list.

02Check stock before taking money

Context
Selling an item that is gone is the worst moment for a customer.
What I chose
The quote step rejects a cart if a variant has less stock than requested, before any order or Stripe session exists. Stock is decremented after payment, in the webhook. If an item sold out in between, the order is flagged for admin review instead of failing silently.
What I passed on
Reserve stock at checkout with a timer. Stronger guarantees, and much more machinery for a store this size.
Trade-off
A rare oversell is possible and is handled by a person, not by the system.

03Currency by location, with the rate saved on the order

Context
A visitor in one country should not have to convert prices in their head, and an order must not change value later.
What I chose
Prices are stored in pounds. The default currency comes from the visitor's locale and a geo lookup, with nine currencies available. The exchange rate is fetched at checkout and saved on the order, and if the rate service fails the order falls back to pounds.
What I passed on
A fixed daily rate table, or a single currency.
Trade-off
One extra network call at checkout, in return for correct, reproducible totals.

04An admin the owner can run alone

Context
If every change needs a developer, the store is not really the owner's.
What I chose
The admin covers a dashboard with low-stock warnings; analytics (revenue, status breakdown, top products, categories and customers, coupon performance); products with duplication, trash and restore, rich text and multi-image upload; categories; orders; customers with presets like top spenders and no orders; coupons with per-user limits; settings for low-stock threshold, shipping fees and currency; and a no-code editor for the home page, About, Contact, FAQ, policies and footer. Admin sign-in needs an emailed one-time code.
What I passed on
Edit content in code and redeploy, or buy an off-the-shelf admin.
Trade-off
A lot of surface area to build and secure for one store.

05Orders have a story, and customers hear it

Context
Order status is only useful if everyone sees the same history.
What I chose
Every order keeps a timeline. When an admin marks an order shipped (with courier and tracking), delivered, cancelled or refunded, the customer gets an in-app notification and an email. Refunds are admin-managed: the admin marks the order refunded and returns the money through the Stripe Dashboard.
What I passed on
Call Stripe's refund API from the admin button.
Trade-off
Two manual steps for a refund today. Automating it is the first item under what I would do next.
What the admin covers

Ten areas, all reachable without a deploy.

Challenges

  • Getting the money path right. The reconcile route exists because a webhook-only design has a failure mode I could not accept. Writing both paths to share the same "mark as paid" function kept them consistent.
  • Stock and payment race. Stock is checked before payment and decremented after it, so the gap needed an honest answer: admin review.
  • Currency edge cases. A failing rate service must not block checkout, so it falls back to pounds.

Results

The store works end to end: browse, choose a variant, pay, receive an email and have the order fulfilled from the admin, with nine currencies and a content editor for the owner.

It is a portfolio-scale build, so there are no sales or traffic numbers to report. I would rather say that than invent some.

What I would do next

  • Make webhook idempotency atomic with a unique insert before processing.
  • Decrement stock atomically, or reserve it with an expiry.
  • Call Stripe's refund API from the admin and sync status from refund events.
  • Send low-stock alerts by email instead of only showing them on the dashboard.
  • Add automated tests; there are none yet.

Contact

Exploring a new product idea? Looking for ways to improve your product? Send a mail to: