VibeCart: Building a Full-Stack E-Commerce System
How the VibeCart monorepo connects React, Express, Prisma, PostgreSQL, auth, email, CI/CD, and production observability.
What this project is
VibeCart is a full-stack e-commerce platform built as a monorepo. The project is not just a product catalog demo. It includes a React frontend, an Express API, a Prisma data layer, authentication, email flows, admin-facing routes, deployment wiring, logging, and error tracking.
The main goal was to build the kind of application that has real operational shape: public browsing, protected checkout and account areas, staff-only admin routes, persistent sessions, validation, and a database schema large enough to support more than a toy storefront.
Architecture
The repo is split into apps/api, apps/web, and shared packages. The API uses Node.js, TypeScript, Express, Prisma, PostgreSQL, JWT authentication, Pino logging, Sentry, and Resend. The frontend uses Vite, React, TypeScript, Tailwind CSS, shadcn/ui, React Router, React Hook Form, and Zod.
That separation lets the backend own business rules while the frontend owns interface behavior. API routes are grouped around controllers, services, validators, middleware, and config. The web app keeps route definitions, auth context, pages, UI components, and frontend services separate so the application can grow without every workflow living in one file.
Backend decisions
The backend follows a controller to service pattern. Controllers handle HTTP concerns such as request parsing, status codes, and response shape. Services handle business logic. Validators sit close to route boundaries so invalid input is rejected before it reaches deeper code.
Prisma is used instead of raw SQL because this project benefits from typed models, migrations, relation handling, and a clear generated client. PostgreSQL through Supabase gives the app a realistic hosted database target, while the Prisma schema gives the project an explicit data model instead of relying on ad hoc JSON.
Authentication uses JWT access tokens and refresh tokens persisted in the database. That matters because access tokens can stay stateless while refresh sessions remain revocable. Logout can invalidate a session instead of depending only on client-side state clearing.
User workflows
The public side includes home, shop, product detail, cart, registration, login, forgot password, reset password, and email verification flows. Protected areas include checkout, wishlist, account, and staff-only admin pages.
On the API side, the auth surface includes registration, login, logout, current-user lookup, email verification, resend verification, forgot password, and password reset. A health endpoint reports system status and database connectivity.
Security and reliability
The project uses bcrypt password hashing, JWT expiry, refresh token revocation, one-time password reset tokens, expiring email verification tokens, CORS, Helmet, input validation, audit logging, and role-based access control.
The key security choice is consistency. Inputs are validated with Zod, auth events are logged, routes are protected on both frontend and backend boundaries, and staff areas are treated as separate authorization cases rather than just hidden navigation links.
Deployment shape
The API is designed for Render, the frontend for Vercel, and CI/CD runs through GitHub Actions. That split mirrors the app architecture: the web client can ship independently while the API remains an isolated service with its own environment variables and database connection.
For a portfolio project, this is more valuable than a single local-only demo because it shows the production concerns around environment config, deployment targets, hosted database connectivity, and operational visibility.
What I would improve next
The next step would be adding Redis-backed caching and stronger product/order workflows. I would also expand automated tests around checkout, admin authorization, token rotation, and email flows. The structure is already set up for that kind of growth because routes, services, validators, and shared types are not collapsed into one layer.