After a year of development: Personal Project — Velora (E-Commerce)
I can say with certainty and confidence:
Today’s Velora consists of 296 files, 115 folders, and approximately 12,000–14,000 lines of hand-written code (scaling to 35,000+ lines including package configurations and dependency lockfiles).
- Architected a modular Next.js frontend using a feature-based folder structure (
features/containing localized hooks, services, and components), streamlining code maintenance and scalability across 7 core domains. - Designed a MongoDB/Mongoose database layer with a reusable BaseService query abstraction, eliminating redundant query code across 12 distinct backend services.
- Implemented secure role-based access control (RBAC) on the backend via custom JWT extraction and authorization middleware, protecting sensitive endpoints for both Customers and Sellers.
- Developed a dual-role workflow featuring a customer-facing marketplace (cart, checkout, product review system) and a dedicated Seller Panel (
app/seller) with store onboarding and product management dashboards. - Integrated Stripe payments and automated webhook processing in the Express backend, ensuring secure checkout flows and real-time database state synchronization.
- Optimized SEO indexability and metadata across the Next.js App Router, utilizing dynamic OpenGraph (
opengraph-image.js), Twitter cards, robots, and automated sitemaps. - Created a centralized validation middleware architecture using schema-based validations across 10+ core models, preventing corrupt or malformed inputs from reaching the database.
- Managed global client state (Auth, Basket, and User profile) using Redux Toolkit slices, ensuring predictable state transitions and eliminating prop-drilling across the Next.js App Router.
- Engineered a custom API client wrapper with interceptors for header injection and token refreshing, facilitating clean, authenticated backend communication for the frontend app.
- Established an automated testing suite using Jest, covering crucial integration scenarios such as auth, controller actions, mailer behaviors, and database connection setups.
Velora uses a clean, decoupled monorepo architecture separating a high-performance Next.js App Router Frontend from a robust Node.js/Express REST API Backend.
High-Level System Design
graph TD
subgraph Frontend [Next.js App Router]
UI[React Components / Pages] <--> Hooks[Custom Hooks]
Hooks <--> Redux[Redux Toolkit Store]
Hooks <--> API_Client[API Client Wrapper client.js]
end
subgraph Backend [Express REST API]
Router[Router Layer /routes] --> Mid[Middleware Layer]
Mid --> Validation[Joi / Custom Validation]
Mid --> Auth[JWT & RBAC Guards]
Validation & Auth --> Controller[Controllers]
Controller --> Service[Services / BaseService]
Service --> Model[Mongoose Models]
Model <--> MongoDB[(MongoDB Database)]
end
API_Client <-->|HTTP REST / JSON| Router
1. The Backend (Express REST API)
The backend follows a strict layered controller-service-model pipeline to maintain a clean separation of concerns:
- Entry (Server.js): Boots the application, configures global middleware (CORS, body parsers, logging), connects to MongoDB (
database/MongoDB.js), and mounts the API routes. - Routing (
routes/versionOne/): Maps paths to middleware chains (e.g.,/api/v1/products). - Middleware (
middleware/): Automatically intercepts requests for rate-limiting, JWT signature extraction (token/verification), role-based checking (auth/system/), and Joi payload validation (validation/). - Controllers (
controller/): Act as the adapters. They extract parameters and payload details, call the service layer, and map the return data into HTTP status codes and JSON responses. - Services (
services/): Where all business logic lives (Stripe calculations, password hashing, mailer notifications). Database services inherit from a BaseService which acts as a reusable repository layer. - Models (
model/): Defines database collections, indexes, and Mongoose schemas.
2. The Frontend (Next.js 14)
The frontend relies on a Feature-Folder design pattern to prevent layout coupling:
- Features (
src/app/features/): All files are grouped under functional domains (e.g., auth, catalog, seller, order). Each domain packages its own localized UI components, state management hooks (e.g.,use-checkout-form.js), API service triggers, and helpers. - Global State (
redux/): Shared client state (user login session, active basket, UI popup states) is managed using Redux Toolkit slices. - API Client (
src/api/client.js): A unified API interface layer configuring endpoint routing, custom headers injection, and handling standardized error interceptors.
Here is the lifecycle of a typical request when a Seller adds a new product to their store:
sequenceDiagram
autonumber
actor Seller as Seller User
participant Page as SellerProductForm.jsx
participant Hook as use-seller-product-form.js
participant API as Product_API.js
participant Router as Post_products.js (Router)
participant Auth as Seller.js (RBAC Middleware)
participant Val as ProductValidation.js (Validation)
participant Ctrl as ProductController.js
participant Svc as ProductService.js
participant DB as MongoDB (Mongoose Model)
Seller->>Page: Fill form & click "Create"
Page->>Hook: Submit handler triggered
Hook->>API: Call createProduct(payload)
API->>Router: POST /api/v1/products (with JWT Auth header)
rect rgb(240, 240, 240)
note right of Router: Backend Processing Pipeline
Router->>Auth: Extract token & verify user is a Seller
Auth->>Val: Check input payload (name, price, stock, description)
Val->>Ctrl: Pass validated data
Ctrl->>Svc: Call productService.createProduct()
Svc->>DB: MongoDB insert query
DB-->>Ctrl: Saved Product Document
end
Ctrl-->>API: HTTP 201 Created (JSON Response)
API-->>Hook: Update state & show success toast
Hook-->>Seller: Redirect to dashboard / show new product
One year of deliberate architecture, separation of concerns, and shipping features that actually work end-to-end.
Velora is the result.
Last updated: 18 July 2026
This document tracks every outstanding task across the Backend and Frontend.
Work through each section top-to-bottom. Check items off as they are completed.
- Backend — Store API
- Backend — Database Schema Fixes
- Backend — Business Rule: Mandatory Store Before Product
- Backend — Validation, Auth & Token Audit
- Frontend — Search Bar Fix
- Frontend — Product Card: Show Store Name
- Frontend — Add-to-Basket Popup
- Frontend — Mandatory Auth Popup (Cart & Buy)
- Frontend — Seller Panel Full Redesign
- Frontend — Better Customer Account Page
- Frontend — Optional Login Popup on Site Open
- Rebrand / Copyright
Implemented seller store endpoints for /store:
| Method | Route | Status |
|---|---|---|
POST |
/store |
✅ Done |
GET |
/store |
✅ Done — returns the logged-in owner's own store |
PATCH |
/store/:id |
✅ Done (seller only) |
DELETE |
/store/:id |
✅ Done (seller only) |
- Add
getAllStoreshandler inStoreController.js - Add
getAllStores()method inStoreService.js(usethis.model.find({}).sort({ createdAt: -1 })) - Create route file
Backend/routes/versionOne/store/Get_all_stores.js- No auth required — public endpoint
- Register the route in
Backend/routes/versionOne/store/index.js
- Add
getStoreByIdhandler inStoreController.js- Validate
:idis a valid ObjectId, return 400 if not - Return 404 if store not found
- Validate
- Add
getStoreById(id)method inStoreService.js(usethis.findById(id)) - Add route in
Get_store.js(or a dedicatedGet_store_by_id.js)router.get("/:id", validateStoreId, getStoreById)— no auth required
- Add
validateStoreIdmiddleware inStoreValidation.js
- Add
patchStoreByIdhandler inStoreController.js(partial: implemented aspatchStoreData, but explicit 403 ownership error is still missing)- Verify
req.user.id === store.ownerOfStore— 403 if not the owner - Only update fields that are present in
req.body(partial update)
- Verify
- Add
updateStore(id, patch)method inStoreService.js(implemented aspatchStoreData) - Create route
PATCH /store/:idwithrequireSeller+validatePatchStoremiddleware - Add
validatePatchStoremiddleware inStoreValidation.js(all fields optional, same rules as create)
- Add
deleteStoreByIdhandler inStoreController.js(partial: implemented asdeleteStore, but explicit 403 ownership error is still missing)- Verify
req.user.id === store.ownerOfStore— 403 if not the owner - Decide: hard delete or soft delete (add
isDeletedflag toStoreschema). Soft delete is recommended. - If soft delete → add
isDeleted: { type: Boolean, default: false }toStoreSchema
- Verify
- Add
deleteStore(id)/softDeleteStore(id)method inStoreService.js(currently hard delete) - Create route
DELETE /store/:idwithrequireSeller - Consider cascading: when a store is deleted, should its products be soft-deleted too? Decision needed.
-
StoreController.js—getStoreDatadoes not guard againstreq.userbeing undefined (fixed: route now usesrequireSeller). -
StoreService.js—getStoreDatacallsfindOne({ ownerOfStore: ownerId }). If no store exists yet it returnsnull. The controller returns{ data: null }— the frontend should handle this gracefully; add a proper 404 or empty-state response. (updated to list-based empty-state behavior) - Test all four new endpoints with the Postman collection (
Velora.postman_collection.json).
Several models reference User or mix up Customer vs Seller references.
The intended relationship is:
| Concept | Model | ObjectId field should reference |
|---|---|---|
| CustomerId | Review, Account (CustomerDetails), Address, Order, Cart, Payment |
Customer model |
| StoreOwnerID | Store.ownerOfStore |
Seller model ✅ already correct |
| StoreId | Product |
Store model ❌ currently references Seller via storeOwnerId |
-
userIdcurrently refs"User"→ change ref to"Customer"// Before userId: { type: ObjectId, ref: "User" } // After userId: { type: ObjectId, ref: "Customer" }
-
userIdcurrently refs"User"→ change ref to"Customer"
-
userIdcurrently refs"User"→ change ref to"Customer"
-
storeOwnerIdreferencesSeller— this is wrong per the intended data model. (resolved by moving tostoreIdin schema) - Add a
storeIdfield that references theStoremodel:storeId: { type: mongoose.Schema.Types.ObjectId, ref: "Store", required: true, // enforce after store creation guard is in place index: true, }
- Keep
storeOwnerIdfor ownership checks (or derive it via the Store document — decide which approach). - Update
ProductService.createProduct()to accept and savestoreId. - Update
createSellerProductinProductController.jsto look up the seller's store, get its_id, and pass it asstoreId. - Update
listProductsandgetProductByIdto populatestoreId(store name at minimum) so the frontend can display it.
- Check
Backend/model/User.js— clarify whether this is a shared base model or a legacy artifact. If unused, remove it to avoid confusion. (noUsermodel file exists) - Search entire codebase for
ref: "User"and confirm each one is intentional or needs changing to"Customer".
A seller can call POST /seller/products without having a store. createSellerProduct in ProductController.js does not check for an existing store.
- Create a middleware
requireSellerHasStoreinBackend/middleware/(or insideStoreService.js):// Pseudocode const store = await storeService.getStoreData(req.user.id); if (!store) throw createHttpError(403, "You must create a store before adding products."); req.sellerStore = store; // pass store downstream
- Apply
requireSellerHasStoreto thePOST /seller/productsroute inBackend/routes/versionOne/products/Post_products.js. - In
createSellerProduct, usereq.sellerStore._idas thestoreIdfor the new product (ties in with task 2.4). - Frontend guard — In
SellerPanelShell.jsx/SellerProductsOverview, before showing "Add Product" CTA, check if the seller has a store. If not, redirect to/seller/store/:idwith an informative message like "Create your store first before listing products."
Last updated: 24 June 2026
This document is the working plan for stabilizing the backend, unifying the product domain, and turning the seller/customer experience into a scalable platform.
Work top to bottom. Each phase depends on the previous one.
- Fix the critical backend risks first.
- Unify the product domain into one canonical model and one write path.
- Harden auth, ownership, webhook, and order flows.
- Upgrade the frontend to match the canonical backend behavior.
- Add tests and rollout safeguards before removing deprecated routes.
If you are asking, "where do I change what?" use the file references in each phase. If you want line-accurate anchors, check the current source files first because line numbers shift as code changes.
Velora already has a good foundation:
- backend layers are mostly separated into routes, controllers, services, middleware, models, and utils
BaseServicegives the project a consistent CRUD backbone- the frontend already centralizes Axios and auth refresh behavior
- integration tests exist, so this can be improved safely instead of rewritten blindly
The main problem is product-domain split and a few correctness gaps:
- public catalog products and seller-owned products are both treated as separate concepts even though they should be one product domain
- product delete is destructive
- seller-facing frontend API wiring has at least two broken URLs
- review storage is duplicated between
ProductandReview - order and webhook flows need stronger invariants
- File: Backend/routes/versionOne/products/Post_products.js
- File: Backend/controller/ProductController.js
What to do:
- Keep only one canonical create path for seller inventory.
- Treat
POST /seller/productsas the real seller write endpoint. - Deprecate
POST /productsor convert it to a legacy alias that forwards to the same service path. - Remove any logic that allows product creation without seller ownership context.
How:
- route layer should only attach auth + validation
- controller should inject
req.user.idand resolved store context - service should persist
storeOwnerIdandstoreIdonly after ownership is verified
What to do:
- Change product deletion to soft delete.
- Keep deleted products out of public lists.
- Preserve order history, cart references, and review traceability.
How:
- replace
hardDeletewithsoftDelete - keep
deletedByset to the user id - ensure
find*methods naturally exclude deleted docs viaBaseService._active
What to do:
- Fix the missing slash in the store patch route.
- Fix
deleteAnStoreto accept anidparameter and useDELETE. - Keep product routes and store routes clearly separated.
How:
patchAnStore(id, payload)should call/server/seller/store/${id}deleteAnStore(id)should callclient.delete(...)
What to do:
- Import
createHttpErrorwheresoftDeleteRecursiveuses it. - Keep the shared service layer runnable in all branches, including error paths.
How:
- add the missing require from
utils/httpError - validate with tests after the change
Target state:
- one
Productcollection - one canonical seller-owned write flow
- one public read flow for the catalog
- one ownership model: seller owns store, store owns products
What to do:
- Keep
storeOwnerIdonly if you need a direct ownership shortcut. - Add or enforce
storeIdas the real product-to-store link. - Populate store information in product reads so the frontend can display seller identity cleanly.
- Remove duplicated meaning from route names and controller methods.
How:
createSellerProductshould resolve the seller’s store firstcreateProductshould not stay as a separate competing write pathlistProductsshould return catalog-safe data onlylistSellerProductsshould return owner-scoped data only
- File: Backend/model/Product.js
- File: Backend/model/Review.js
- File: Backend/services/ReviewService.js
What to do:
- Use
Reviewas the source of truth for reviews. - Keep only summary fields on
Productif needed, such asreviewCountandratingAverage. - Stop embedding full review documents inside
Product.
How:
- migrate existing embedded reviews into the
Reviewcollection - compute aggregates from
Reviewafter save/delete - update reads to join or aggregate reviews, not duplicate them
- File: Backend/routes/versionOne/products/Post_products.js
- File: Backend/routes/versionOne/products/Patch_product.js
- File: Backend/services/ProductService.js
What to do:
- Require a store before creating a product.
- Verify that the store belongs to the authenticated seller.
- Reject updates when the product does not belong to the seller or the resolved store.
How:
- create a
requireSellerHasStoreguard - resolve store in controller or middleware, not inside the client
- keep ownership verification in the service as the final gate
- File: Backend/middleware/auth/authenticate.js
- File: Backend/routes/index.js
- File: Backend/Server.js
What to do:
- Keep
requireAuthfor authenticated customer flows. - Keep
requireSellerfor seller-only flows. - Audit every route file so the correct guard is attached consistently.
- Make startup fail loudly if JWT or Stripe secrets are missing.
How:
- public routes stay public only when they truly need to be
- seller routes should never rely on frontend protection alone
- webhook secrets should be validated at startup, not discovered at runtime
- File: Backend/services/OrderService.js
- File: Backend/controller/OrderController.js
- File: Backend/routes/versionOne/checkout/Post_checkout.js
What to do:
- Validate each ordered product before order creation.
- Block deleted or unavailable products.
- Make ownership and item consistency explicit.
- Keep
updateOrderStatusfrom becoming a weakly guarded write endpoint.
How:
- verify each product exists and is active
- preserve a snapshot for orders so later product edits do not break history
- apply separate customer vs seller fulfillment permissions if fulfillment is introduced
What to do:
- Keep signature verification.
- Verify that the referenced order and payment intent exist before updating status.
- Make webhook updates idempotent.
How:
- check order existence before writing
- ignore repeated success/failure events safely
- ensure payment amount and metadata match the stored order total
What to do:
- Add page and limit support to public and seller product lists.
- Return metadata for total pages and current page.
How:
- use
findAllWithPaginationfromBaseServicewhere possible - expose page params in frontend API calls later
What to do:
- Replace regex-only search with a more scalable strategy.
- Add indexes or text search where appropriate.
How:
- keep simple regex only as a fallback
- prefer indexed search when the catalog grows
- File: Frontend/src/api/product/Product_API.js
- File:
Frontend/src/app/features/catalog/components/ProductSearchBar.jsx - File:
Frontend/src/app/features/catalog/CatalogPage.jsx - File:
Frontend/src/app/features/catalog/CatalogSidebar.jsx
What to do:
- Make search update as the user types.
- Debounce requests so the UI stays responsive.
- Keep desktop and mobile search behavior identical.
- File:
Frontend/src/app/features/catalog/components/ProductCard.jsx - File:
Frontend/src/app/features/catalog/ProductDetailPage.jsx
What to do:
- Show store name or seller identity on product cards and detail pages.
- Link the store name to the store page when that page is ready.
How:
- consume populated
storeIdor a flattenedstoreNamefield from the backend - keep the UI wording consistent: "Sold by"
- File:
Frontend/src/app/features/order/ProductDetailPage.jsx - File:
Frontend/src/app/components/ui/
What to do:
- Show a toast when an item is added to basket.
- Block add-to-basket and buy-now actions when no customer session exists.
- Reuse one auth modal instead of scattered prompts.
How:
- the toast should be lightweight and dismiss automatically
- the auth modal should preserve browsing but stop checkout actions
- File:
Frontend/src/app/features/seller/ - File:
Frontend/src/app/seller/
What to do:
- Turn the seller panel into a multi-section dashboard.
- Add Store, Products, Orders, Analytics, and Settings sections.
- Show live store state and product state, not placeholder text.
How:
- keep the seller sidebar navigation consistent
- add cards, empty states, and responsive layouts
- block the product creation screen until store setup is complete
- File:
Frontend/src/app/features/account/
What to do:
- Make the account page a real hub for profile, addresses, payment methods, and order history.
- Keep inline edits and loading states clean.
What to do:
- Add tests for seller product ownership.
- Add tests for soft delete behavior.
- Add tests for webhook order updates.
- Add tests for order creation with invalid products.
What to do:
- Run a dry-run migration first.
- Compare legacy and canonical reads before cutover.
- Keep a rollback path for write-path changes.
How:
- feature flags for canonical product writes
- migration logs with checkpoints
- staging rehearsal before production data changes
- Backend/controller/ProductController.js
- Backend/services/ProductService.js
- Backend/model/Product.js
- Backend/routes/versionOne/products/Post_products.js
- Backend/routes/versionOne/products/Patch_product.js
- Backend/routes/versionOne/products/Delete_product.js
- Backend/middleware/auth/authenticate.js
- Backend/controller/WebhookController.js
- Backend/services/OrderService.js
- Backend/services/BaseService/index.js
- Frontend/src/api/Store/Store_API.js
- Frontend/src/api/product/Product_API.js
Frontend/src/app/features/catalog/components/ProductSearchBar.jsxFrontend/src/app/features/catalog/CatalogPage.jsxFrontend/src/app/features/catalog/CatalogSidebar.jsxFrontend/src/app/features/catalog/components/ProductCard.jsxFrontend/src/app/features/catalog/ProductDetailPage.jsxFrontend/src/app/features/seller/Frontend/src/app/features/account/
- Fix the broken seller API URLs and BaseService import bug.
- Convert product delete to soft delete.
- Decide the canonical product write path and enforce seller store ownership.
- Remove review duplication by making Review the source of truth.
- Harden order and webhook updates.
- Add pagination and better search.
- Update the frontend to reflect the canonical backend model.
- Expand tests.
- Remove deprecated product write aliases once the new path is stable.
The target state is simple:
- one product domain
- one seller store ownership model
- one canonical write path for sellers
- one public catalog path for shoppers
- one review source of truth
- soft deletes everywhere important
- tests that protect ownership and payment integrity
- a frontend that feels like a real marketplace, not two partial apps stitched together
That is the version worth building toward.
{ "id": ObjectId, "storeId": ObjectId, "sku": "P001-2026", "slug": "apple-vision-pro", "name": "Person Test Product...", "description": "...", "brand": "Apple",
"basePrice": 129.99, "currentPrice": 99.99, "discountPercentage": 23,
"category": "electronics", "subCategory": "audio", "tags": ["wireless", "premium"],
"images": [ { "url": "...", "isMain": true }, ... ],
"variants": [ /* array of variant objects */ ],
"stock": 150, // total or calculated from variants "lowStockThreshold": 20,
"highlights": ["Feature 1", "Feature 2"], "specifications": { // flexible key-value "weight": "0.6kg", "battery": "18 hours" },
"status": "published", // draft | published | archived "isFeatured": false,
"createdAt": Date, "updatedAt": Date, "publishedAt": Date,
// Soft delete "isDeleted": false, "deletedAt": null, "deletedBy": null }