Why This Scope
For my graduation project, I deliberately chose something ambitious. Not a CRUD app with a login page - a full production-grade e-commerce platform. The kind where you have to think about payment security, email deliverability, image optimization, multi-role access control, and what happens when Stripe sends the same webhook three times.
The result is three applications working together:
- Customer Storefront - Next.js 14 with Material-UI for product browsing, cart management, and checkout
- Admin Dashboard - Next.js 14 with Tailwind CSS for product management, order processing, analytics, and invoice generation
- Backend API - Express.js with Sequelize, handling business logic, JWT auth, Stripe integration, and email dispatch
The Stripe Integration: Where the Details Live
Stripe's API is beautifully designed. The documentation is excellent. And yet, the implementation has traps that only surface in production.
Checkout Sessions
The flow: the storefront sends the cart items to the API. The API creates a Stripe Checkout Session with line items, success/cancel URLs, and metadata (order ID, user ID). Stripe redirects the customer to their hosted payment form. After payment, Stripe sends a webhook.
The key insight: never trust the client-side redirect. A customer could be redirected to the success URL even if their payment fails (network issues, browser crashes). The only reliable signal is the webhook.
Webhook Verification
Every incoming webhook is verified using Stripe's signing secret. The raw body and the stripe-signature header are passed to stripe.webhooks.constructEvent(). If the signature doesn't match, the request is rejected. Without this check, anyone could POST a fake checkout.session.completed event and mark their order as paid.
Idempotency
Stripe can deliver the same webhook multiple times (network retries, at-least-once delivery). The handler checks if the order's payment status has already been updated before processing. This sounds obvious, but I've seen production codebases that process the same payment event three times, sending three confirmation emails and decrementing inventory thrice.
Transactional Emails with Brevo
Order confirmations, shipping notifications, password reset links - all sent through Brevo (formerly Sendinblue). Each email type has a templated HTML file in the server's email templates directory.
The templates use inline CSS (because email clients ignore external stylesheets more often than not) and responsive tables that degrade gracefully in Outlook. Building HTML emails that look good everywhere is its own special kind of suffering.
One thing I'd change: in this project, emails are sent synchronously within the request cycle. If Brevo's API is slow or down, the user's request hangs. In a production system, I'd push email jobs onto a background queue (BullMQ, for instance) and process them asynchronously.
PDF Invoice Generation
When an admin marks an order as "shipped," the system generates a PDF invoice on-demand using PDFKit. The invoice includes:
- Company logo and contact details
- Customer name, email, and shipping address
- Line items with product name, quantity, unit price, and line total
- Subtotal, tax breakdown, and grand total
- Payment method and transaction reference
- Order date and invoice number
The PDF is generated in memory and streamed directly to the admin's browser - no file storage needed. If you need to regenerate it later (customer request, audit), the same function runs again with the order data. Stateless and predictable.
Building the PDF layout was more work than I expected. PDFKit gives you absolute positioning, which means you're manually calculating x/y coordinates for every element. Headers, columns, row heights, page breaks - all manual. I ended up building a small table renderer that handles column widths, alternating row backgrounds, and automatic page breaks when content overflows.
Security: More Than Just bcrypt
Building an e-commerce platform means handling credit card flows, personal data, and real money. The security stack:
- Bcrypt for password hashing with configurable salt rounds (12 in production)
- JWT with short-lived access tokens (15 minutes) and refresh token rotation. Each refresh token is single-use - using it issues a new pair and invalidates the old refresh token.
- Helmet.js for security headers (Content-Security-Policy, X-Frame-Options, etc.)
- Rate limiting on auth endpoints - 5 login attempts per minute, 3 password reset requests per hour
- Parameterized queries via Sequelize - all user input is parameterized, never concatenated into SQL
- CORS with strict origin whitelisting - only the storefront and admin URLs are allowed
- Input validation on every endpoint - Joi schemas validate request bodies before any business logic runs
The Database Schema
Sequelize models define a rich relational schema:
- Users with roles (customer, admin), addresses, and preferences
- Products with categories (hierarchical), materials, gemstones, and images
- Orders with line items, payment status, shipping status, and Stripe metadata
- Cart with per-user item tracking and quantity management
- Reviews with star ratings and moderation status
The hierarchical category system deserves a mention: categories have a parentId foreign key, allowing nested structures like "Jewelry > Rings > Engagement Rings." The storefront renders this as a breadcrumb navigation, and the admin panel shows it as a tree.
Two Frontends, One API
The customer storefront and admin dashboard are completely separate Next.js applications. They share the same Express API but have different authentication flows, different layouts, and different feature sets.
The storefront uses Material-UI for a polished, customer-friendly design. Product pages have image galleries, size selectors, and "Add to Cart" buttons. The checkout flow integrates with Stripe's hosted payment page.
The admin dashboard uses Tailwind CSS for a dense, data-focused layout. It features data tables with sorting and filtering, order management with status transitions, product CRUD with image uploads, and an analytics dashboard showing revenue, order volume, and popular products.
Testing Strategy
Three layers:
- Unit tests - pure function testing for price calculations, discount logic, validation rules
- Integration tests - API endpoint testing with Supertest, using a test database seeded before each suite
- E2E tests - full user flows with Selenium: browse products, add to cart, checkout, verify order creation
The integration tests were the most valuable. They caught edge cases that unit tests missed: concurrent cart operations, race conditions in inventory checks, and order creation failures that should roll back cart clearing.
What This Project Taught Me
Building an end-to-end e-commerce platform is a masterclass in the concerns that don't surface in smaller projects:
- Payment security is not just about HTTPS. Webhook verification, idempotency, and server-side session creation are all essential. Trusting client-side redirects is a recipe for fraud.
- Email is a solved problem that's never solved. HTML email rendering, deliverability, SPF/DKIM configuration, and bounce handling are each their own rabbit hole.
- PDFs are harder than they look. Without a high-level layout engine, you're doing pixel math. Worth building a small abstraction layer.
- Multi-role access is a day-one decision. Retrofitting admin-only routes onto a system designed for a single role is painful. Plan for it early.
- Test the integration points. Stripe webhooks, email dispatch, and PDF generation are where bugs hide. Unit testing business logic is necessary but not sufficient.
Source code: gitlab.com/licenta4785538/jewelry-store