Back to Projects

StoreCraft

Next.jsTypeScriptReactPrismaMySQLClerkStripeCloudinaryTailwind CSSRadix UIHeadless UIZustandTanStack TableReact Hook FormZodRecharts
IMAGE — Admin dashboard overview with total revenue, sales count, and products in stock cards + revenue bar chart
IMAGE — Admin products list page with data table, search, and pagination
IMAGE — Admin product creation form with image upload (Cloudinary), price, category, size, color, featured/archived toggles
IMAGE — Admin billboards list with label and image URL columns
IMAGE — Admin categories page showing category-billboard relationship
IMAGE — Admin sizes and colors management pages
IMAGE — Admin orders page showing paid/unpaid status, phone, address
IMAGE — Admin settings page with store name edit and API URL copy
IMAGE — Store switcher dropdown (cmdk command palette) showing multiple stores
IMAGE — Store homepage with billboard hero and featured products grid
IMAGE — Store category page with size and color filter sidebar
IMAGE — Store product detail page with image gallery (tab thumbnails) and product info
IMAGE — Store product card hover state showing expand and add-to-cart icons
IMAGE — Store shopping cart page with cart items, order summary, and checkout button
IMAGE — Store mobile responsive view with mobile filter drawer (Headless UI)
IMAGE — Admin API documentation auto-generated per entity (public GET / admin POST/PATCH/DELETE)
VIDEO
VIDEO
VIDEO
VIDEO
VIDEO
VIDEO
VIDEO
VIDEO
VIDEO
Admin — Dashboard Overview1 / 25

Overview

A full-stack e-commerce platform consisting of two Next.js applications: a multi-tenant admin dashboard (CMS + API backend) and a customer-facing online store. The admin provides complete CRUD management for products, categories, sizes, colors, billboards, and orders with Cloudinary image uploads and revenue analytics. The store consumes the admin's public API to display products with filtering, an image gallery, persistent shopping cart, and Stripe checkout. Built with Next.js 15, React 19, Prisma ORM, MySQL, Clerk, Stripe, and Cloudinary.

Key Features

Admin Dashboard
  • Multi-tenant admin dashboard — create and switch between multiple stores using a cmdk command palette store switcher
  • Full CRUD management for billboards, categories, sizes, colors, and products with form validation via React Hook Form + Zod
  • Data tables with column-based search filtering and pagination using TanStack React Table for every entity
  • Admin dashboard overview with total revenue, sales count, and products in stock metric cards
  • Monthly revenue bar chart using Recharts — aggregates paid order revenue by month across the year
  • Cell actions on every data table row — copy ID, edit, and delete with confirmation modal
  • Store frontend with billboard hero banner, featured products grid, and dynamic category navigation from the API
  • Stripe Checkout integration — cart summary posts productIds to admin API, creates Stripe session, redirects to hosted checkout
  • Dark/light/system theme toggle on the admin dashboard using next-themes
Products & Catalog
  • Product management with multiple Cloudinary image uploads, price (Decimal), category/size/color selectors, and featured/archived toggles
  • Product filtering by category, size, and color using URL query parameters (query-string) with toggle-on/toggle-off behavior
  • Product detail page with Headless UI TabGroup image gallery (thumbnail tabs + main image panel) and related products
  • Quick preview modal — expand icon on product cards opens a Headless UI Dialog with gallery and product info without navigating away
  • Stripe webhook processes checkout.session.completed — marks order as paid, captures address/phone, and auto-archives purchased products
  • Mobile responsive filter drawer using Headless UI Dialog on the store category page
Shopping & Checkout
  • Persistent shopping cart using Zustand with localStorage persistence — add, remove, duplicate detection with toast notifications
Auth & Infrastructure
  • Authentication using Clerk with store ownership verification on every mutating API route

Architecture Overview

How the system is structured from frontend to external services.

01

Admin Frontend

Next.js 15 (App Router)React 19Tailwind CSSRadix UI / ShadcnTanStack React TableRecharts

Multi-tenant dashboard scoped by [storeId] route parameter. Server Components for data fetching, Client Components for forms (React Hook Form + Zod), data tables, modals, and the store switcher (cmdk). Dark/light/system theme toggle via next-themes. Zustand manages the store creation modal state.

02

API Backend (within Admin)

Next.js API RoutesPrisma ORMClerkStripe

RESTful API routes serving both the admin dashboard and the store frontend. GET endpoints are public (no auth) for the storefront to consume. POST/PATCH/DELETE endpoints require Clerk authentication and verify store ownership. Checkout endpoint creates Stripe sessions, webhook endpoint processes payments and archives products.

03

Store Frontend

Next.js 15 (App Router)React 19Tailwind CSSHeadless UIZustand

Customer-facing storefront with no database of its own — all data fetched from the admin API via server-side fetch() calls in action files. Zustand with localStorage persistence manages the shopping cart. Headless UI provides the product gallery (TabGroup), preview modal (Dialog with transitions), and mobile filter drawer.

04

Database & Services

MySQL (PlanetScale)Prisma ORMCloudinaryStripe

Nine Prisma models with relationMode: prisma for PlanetScale compatibility. Cloudinary handles product and billboard image uploads via next-cloudinary widget. Stripe processes payments with hosted checkout and webhook-driven order finalization. All entity IDs are UUIDs.

Data Model

Core entities and how they relate to each other.

Store

Has many Billboards, Categories, Sizes, Colors, Products, Orders

Multi-tenant store — each user can create multiple stores, and all content is scoped to a store via storeId foreign keys

id (uuid, PK)nameuserId (from Clerk)createdAtupdatedAt

Billboard

Belongs to Store. Has many Categories (a category displays its billboard)

Hero banner images displayed at the top of the store — each billboard has a label and a Cloudinary image URL

id (uuid, PK)storeIdlabelimageUrl@@index([storeId])

Category

Belongs to Store and Billboard. Has many Products

Product categories linked to a billboard for visual display — categories drive the store's navigation menu

id (uuid, PK)storeIdbillboardIdname@@index([storeId])@@index([billboardId])

Size

Belongs to Store. Has many Products

Product sizes (e.g., S, M, L, XL) — each size has a name and display value

id (uuid, PK)storeIdnamevalue@@index([storeId])

Color

Belongs to Store. Has many Products

Product colors with hex values — displayed as colored circles on the store frontend and as filter buttons

id (uuid, PK)storeIdnamevalue (hex string)@@index([storeId])

Product

Belongs to Store, Category, Size, Color. Has many Images (cascade delete) and OrderItems

Main product entity with price, featured/archived flags, and links to category, size, and color. Archived products are hidden from the store and auto-archived after purchase

id (uuid, PK)storeIdcategoryIdsizeIdcolorIdnameprice (Decimal)isFeatured (default: false)isArchived (default: false)

Image

Belongs to Product (cascade delete on product removal)

Product images uploaded via Cloudinary — supports multiple images per product for the gallery view

id (uuid, PK)productIdurl@@index([productId])

Order

Belongs to Store. Has many OrderItems

Customer order record — created during Stripe checkout, updated to paid with address/phone after webhook confirms payment

id (uuid, PK)storeIdisPaid (default: false)phone (default: '')address (default: '')@@index([storeId])

OrderItem

Belongs to Order and Product

Individual product within an order — links an order to a product for the order details

id (uuid, PK)orderIdproductId@@index([orderId])@@index([productId])

Key Flows

Step-by-step walkthrough of the main user and system flows.

Product Management (Admin)

  1. 1Admin authenticates via Clerk and selects a store from the store switcher (or creates a new one)
  2. 2Navigates to Products page — data table shows all products with search, pagination, and cell actions
  3. 3Clicks 'Add New' — form renders with React Hook Form + Zod validation
  4. 4Fills in name, price, category (dropdown), size, color, featured/archived toggles
  5. 5Uploads one or more images via Cloudinary widget (next-cloudinary CldUploadWidget)
  6. 6On submit: POST /api/:storeId/products creates product with nested createMany for images
  7. 7On edit: PATCH first deletes all existing images (deleteMany), then creates new ones (createMany)

Store Browsing & Filtering

  1. 1Customer visits the store homepage — server-side fetch gets featured products and billboard from admin API
  2. 2Navbar dynamically renders category links fetched from GET /api/:storeId/categories
  3. 3Customer clicks a category — category page fetches products with categoryId filter
  4. 4Size and color filters rendered as toggle buttons from GET /api/:storeId/sizes and /colors
  5. 5Clicking a filter updates URL search params via query-string — page re-fetches products with the new filters
  6. 6Product cards show image, name, category, price — hover reveals expand (preview modal) and cart icons
  7. 7Product detail page shows Headless UI TabGroup image gallery with thumbnail tabs + main panel, plus related products from same category

Shopping Cart & Stripe Checkout

  1. 1Customer clicks 'Add to Cart' on a product card or detail page
  2. 2Zustand cart store (persisted to localStorage) adds the product — duplicate detection shows toast if already in cart
  3. 3Cart page displays items with image, name, color, size, price, and remove button
  4. 4Summary component calculates total price by reducing over cart items
  5. 5Customer clicks 'Checkout' — Summary posts productIds to POST /api/:storeId/checkout
  6. 6Admin API creates an Order + OrderItems in the database, then creates a Stripe Checkout session with line items
  7. 7Customer is redirected to Stripe's hosted checkout page for payment
  8. 8After payment, Stripe sends checkout.session.completed webhook to /api/webhook
  9. 9Webhook updates order (isPaid: true, address, phone) and auto-archives all purchased products (isArchived: true)
  10. 10Customer is redirected back to /cart?success=true — cart is cleared and success toast shown

Multi-Store Management

  1. 1User signs in via Clerk — root layout checks for existing stores
  2. 2If no stores exist: store creation modal auto-opens with name field
  3. 3On submit: POST /api/stores creates the store, then window.location.assign redirects to the new store's dashboard
  4. 4Store switcher in the navbar uses cmdk command palette — lists all stores the user owns
  5. 5Selecting a different store navigates to /:newStoreId — all dashboard routes are scoped by [storeId]
  6. 6Every mutating API route verifies the authenticated user owns the store via store.findFirst({where: {id, userId}})

Challenges I Solved

01

Dual-App Architecture (Admin API + Store Frontend)

The Problem

The store frontend needs product data but shouldn't have its own database or duplicate the admin's data models. Both apps need to run independently and communicate reliably.

How I Solved It

The admin serves a dual role: CMS dashboard AND public API backend. GET endpoints are unauthenticated so the store can fetch data server-side. The store's action files are thin fetch() wrappers pointing to NEXT_PUBLIC_API_URL. This means one source of truth for data, and the store can be deployed anywhere as a static/SSR frontend.

02

Product Image Update Strategy

The Problem

When editing a product, the user might add, remove, or reorder images. Tracking individual image adds/removes against the database is complex and error-prone.

How I Solved It

Used a delete-all-then-recreate pattern: the PATCH endpoint first runs images.deleteMany({}) to remove all existing images, then images.createMany({data: newImages}) to insert the new set. This is simpler than diffing and works because Image cascade-deletes are handled by Prisma. The trade-off is slightly higher write volume, but it eliminates sync bugs.

03

Store Ownership Verification on Every Mutation

The Problem

Multi-tenant apps must ensure users can only modify their own stores. A simple userId check on the request isn't enough — the user might craft a request to a storeId they don't own.

How I Solved It

Every mutating API route performs a two-step auth check: (1) Clerk auth() extracts the userId from the JWT, (2) prismaDB.store.findFirst({where: {id: storeId, userId}}) confirms the user owns the specific store. If either check fails, the route returns 401/403. This prevents cross-tenant data manipulation.

04

Stripe Checkout with Order Lifecycle

The Problem

Orders need to be created before payment (to track what was purchased), but they shouldn't be marked as paid until Stripe confirms payment. Products should also become unavailable after purchase to prevent overselling.

How I Solved It

The checkout endpoint creates an Order with isPaid: false and OrderItems linking to each product. Stripe's checkout.session.completed webhook then updates the order (isPaid: true, captures address/phone from the session). The webhook also auto-archives all purchased products (isArchived: true), which hides them from the store's product listing since the API always filters isArchived: false.

Summary

StoreCraft demonstrates a complete e-commerce platform with a dual-app architecture: a multi-tenant admin CMS that doubles as an API backend, and a separate storefront that consumes the public API. The project showcases multi-store management with Clerk authentication and ownership verification, Cloudinary image uploads, Stripe checkout with webhook-driven order lifecycle, persistent cart state with Zustand and localStorage, and a consistent CRUD pattern across all entities using TanStack React Table, React Hook Form, and Zod validation.