Specialty Coffee E-Commerce — A full-stack DTC platform for a premium Stockholm coffee brand.
"Rost" (roast) + "tid" (time) — crafted for those who take their coffee seriously.
- Stripe webhooks as source of truth — the checkout endpoint only creates a Stripe session;
checkout.session.completedcreates the order idempotently, decrements stock, and processes discounts, loyalty, and gift card side-effects - Atomic checkout via Prisma transaction — prices snapshotted at purchase time, order + items created, stock decremented, and cart cleared in a single all-or-nothing transaction
- JWT with silent refresh — access token lives in memory (XSS-safe), refresh token in an httpOnly cookie; an Axios interceptor queues concurrent 401s, refreshes once, then retries all queued requests transparently
- Prices stored as integers in öre — no floating-point money arithmetic anywhere in the stack (1 kr = 100 öre)
- 400+ tests with CI coverage gates — supertest integration tests against a real Postgres instance on every push; separate coverage thresholds enforced for server and client
- Manual subscription renewals — no Stripe Subscription objects; a cron-callable endpoint creates a fresh Checkout session per due subscription and advances
nextBillingDate, keeping the billing logic entirely in-app
| Layer | Tech |
|---|---|
| Frontend | React 18, TypeScript, Vite, Tailwind CSS |
| Animations | Framer Motion |
| State | Zustand, React Query v5 |
| Routing | React Router v6 (lazy-loaded, code-split) |
| Backend | Node.js, Express, TypeScript |
| ORM | Prisma |
| Database | PostgreSQL |
| Payments | Stripe Checkout |
| Auth | JWT (access token + httpOnly refresh cookie) |
| Validation | Zod |
| Charts | Recharts |
| Nodemailer | |
| Monitoring | Sentry |
| Rate limiting | express-rate-limit |
| Dev infra | Docker Compose |
- Docker Desktop (includes Compose)
make dev-build # first run — builds images, pushes schema, seeds DB, starts all services
make dev # subsequent runs
make reset # wipe the database and start freshApp runs at:
- Client:
http://localhost:5173 - API:
http://localhost:4000/api - API Docs:
http://localhost:4000/api/docs
On first boot the server automatically:
- Syncs the Prisma schema to the database (
prisma db push) - Seeds products, categories, and demo accounts
- Starts the Express API
# Terminal 1 — start Postgres
docker compose up db
# Terminal 2 — server
cd server && npm install
npx prisma db push
npx prisma db seed
npm run dev
# Terminal 3 — client
cd client && npm install && npm run devmake test # run all tests (server + client)
make test-server # server tests only
make test-client # client tests only
make seed # re-seed the database
make lint # lint both packages
make typecheck # TypeScript check both packages- Browse specialty coffees by category, roast level, and origin
- Product search and filtering with URL-synced state
- Product variants — bag sizes (250g / 500g / 1kg) and grind options per product
- Product reviews with star ratings and average score on cards
- Related products on product detail page
- Wishlist
- Stock badges — "Only N left" warning, out-of-stock overlay
- Persistent cart backed by PostgreSQL (per user)
- Stripe Checkout integration with atomic order creation
- Shipping rate selector — Standard (49 kr, free over 500 kr) and Express (99 kr)
- Discount codes — percentage and fixed-amount, with min order and expiry support
- Gift card redemption
- Loyalty points — earn 1 pt per 10 kr spent, redeem 100 pts for 10 kr off
- Optimistic cart updates with instant UI feedback and rollback on error
- Subscribe & Save — 10% off on recurring coffee deliveries
- Delivery intervals: every 14, 30, or 60 days
- Manage subscriptions: pause, resume, cancel, or change frequency
- Immediate Stripe Checkout on subscribe; renewal endpoint for cron-based processing
- Order history with itemized breakdown and status timeline
- Cancel pending orders
- Request a return on delivered orders — select items and reason
- Admin can approve returns and trigger Stripe refunds automatically
- Loyalty account auto-created on first earn
- Points balance and transaction history at
/api/loyalty - Points balance chip on profile page; earn preview in cart
- Admin can generate gift cards with custom balance and optional expiry
- JWT auth with silent refresh — no re-login on page reload
- Password reset via email link
- Profile: edit name and email, change password
- Address book: save, edit, set default, and delete shipping addresses
- Dashboard: total orders, revenue, active subscriptions, MRR, recent orders, orders-by-status donut, revenue-by-product bar chart
- Product CRUD with image, category, roast level, origin, tasting notes, weight, stock
- Variant management per product (inline table)
- Order management with status updates and CSV export
- Customer list
- Discount code management (create, delete)
- Subscription list with status and next billing date
- Return requests — approve (fires Stripe refund) or reject
- Gift card management — generate and view all cards
- Feature-first module structure (each domain: routes / controller / service / schema)
- Atomic checkout via Prisma transaction — snapshot prices, create order + items, decrement stock, clear cart
- Prices stored as integers in öre (1 kr = 100 öre) — no float money
- All routes lazy-loaded with React.lazy + Suspense
- Debounced search (300ms, URL-synced)
- Rate limiting: 10 req/15min on login/register, 5/hr on reviews
- Refresh token cleanup — expired tokens purged on startup and every 24h
- XML sitemap and robots.txt auto-generated from live product data
- Sentry error monitoring on both server and client
- PWA-ready: favicon, web manifest, Open Graph and Twitter meta tags
- Accessibility: skip-to-content, focus trap in modals, aria-labels, 44px touch targets
| Password | Role | |
|---|---|---|
| admin@rostid.se | admin123 | admin |
| customer@rostid.se | password123 | customer |
rostid/
├── server/
│ ├── src/
│ │ ├── config/ # env, prisma, stripe
│ │ ├── modules/ # auth, products, categories, cart, orders,
│ │ │ # checkout, webhooks, admin, reviews, wishlist,
│ │ │ # discounts, shipping, subscriptions, returns,
│ │ │ # loyalty, giftcards, newsletter, users
│ │ ├── middleware/ # authenticate, requireAdmin, errorHandler
│ │ └── utils/ # AppError, asyncHandler, jwt, email, tokenCleanup
│ └── prisma/ # schema, migrations, seed
├── client/
│ └── src/
│ ├── pages/ # HomePage, ProductsPage, ProductDetailPage,
│ │ # CartPage, OrdersPage, OrderDetailPage,
│ │ # ProfilePage, WishlistPage, SubscriptionsPage,
│ │ # admin/…
│ ├── components/ # ui/, layout/, products/, cart/, orders/
│ ├── hooks/ # useCart, useDebounce
│ ├── api/ # products, orders, auth, admin, checkout,
│ │ # shipping, subscriptions, loyalty, giftcards
│ ├── store/ # authStore (Zustand)
│ ├── router/ # lazy routes, ProtectedRoute, AdminRoute
│ ├── animations/ # Framer Motion variants
│ └── types/ # shared TypeScript interfaces
├── design-system/ # MASTER.md — design tokens and component guide
├── docker-compose.yml
└── .env.example
JWT with silent refresh — Access token in memory, refresh token in httpOnly cookie. Axios interceptor silently refreshes on 401 and queues concurrent requests. On page reload, App.tsx calls refresh → me to restore session before first render.
Atomic checkout — Order creation runs inside a Prisma transaction: prices are snapshotted, order and items are created, stock is decremented (variant-aware), and the cart is cleared. All-or-nothing.
Stripe webhooks — checkout.session.completed is the source of truth for order creation. The checkout service records checkout totals in session metadata, applies combined checkout discounts through a Stripe coupon, and the webhook creates the order idempotently, decrements stock, and processes discount, loyalty, and gift card side-effects.
Subscriptions (manual interval) — No Stripe Subscription objects. Each renewal is a fresh Checkout session created by the /api/subscriptions/process-due endpoint, which advances nextBillingDate by intervalDays after firing.
Loyalty points — Earned when an admin marks an order as delivered (1 pt per 10 kr). Redeemed during checkout as part of the combined Stripe checkout discount. LoyaltyAccount is upserted on first earn.
Design system — Warm Scandinavian minimalism. Espresso palette (50–950), Playfair Display serif headings, Inter body, 4pt spacing scale.




