A React + TypeScript storefront with a persistent client-side cart, Stripe checkout, an Express backend, MongoDB order storage, and a product customization flow for stamp-based designs.
The project currently supports two shopping modes:
- Standard catalog products loaded from local JSON data
- Customizable products with canvas-based mockups, preset PNG stamp designs, user PNG uploads, and saved customization metadata in the cart
This repository is split into two main parts:
src/: the Vite/React frontend deployed to Vercelserver/: the Express backend intended for Railway deployment
The frontend is responsible for:
- Routing and page rendering
- Product browsing
- Cart state and persistence in local storage
- Product customization UI
- Uploading customer PNG files for fulfillment
- Redirecting the user into Stripe Checkout
The backend is responsible for:
- Creating Stripe checkout sessions
- Receiving Stripe webhooks
- Persisting completed order metadata in MongoDB
- Uploading customer PNG files to Cloudflare R2 and returning a public URL
- Homepage with:
- ready-to-buy catalog items
- a customizable product section for stampable products
- Product detail page for standard store items
- About page
- Payment success and failed pages
- Cart state stored in React context
- Cart contents persisted in local storage
- Quantity increase/decrease/remove controls
- Support for both standard products and customized products
- Custom items are treated as distinct cart lines based on their customization payload, not just item id/size
- Dedicated routes for:
/customize/tshirt/customize/hoodie/customize/sweater/customize/glasscup/customize/hat/customize/apron/customize/totebag
- Canvas mockup preview with:
- draggable design overlay
- scale controls
- rotation controls
- reset positioning
- Preset PNG design picker
- Multiple user PNG uploads retained in the same session
- Uploads stored in Cloudflare R2 through the backend
- Customization metadata saved into the cart, including:
- product type
- color
- size
- material
- design source and URL
- transform position, scale, and rotation
- Stripe Checkout session creation on the backend
- Stripe webhook handling
- Order persistence to MongoDB after successful payment
- Checkout metadata includes cart item information for downstream order handling
- Server-side cart quoting for the review page before payment
- Stripe Checkout remains the final source of truth for automatic tax and final shipping address collection
Verify these settings before production checkout goes live:
- Stripe Tax is enabled for the account
- Checkout is allowed to calculate tax automatically
- Line items are treated as tax-exclusive
The backend explicitly sends
tax_behavior: "exclusive"when creating Checkout Session line items. - Shipping address collection is enabled through the Checkout Session flow
- The webhook endpoint is subscribed to at least:
checkout.session.completedcheckout.session.async_payment_succeeded
- React 19
- TypeScript
- Vite
- React Router v7
- React Bootstrap
- Bootstrap 5
- React Icons
- React Helmet Async
- Express 5
- TypeScript
- Stripe SDK
- Mongoose
- Multer
- Cloudflare R2 via AWS S3-compatible SDK
- CORS
- dotenv
From the root package.json:
{
"dependencies": {
"bootstrap": "^5.3.7",
"mongoose": "^9.4.1",
"react": "^19.1.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.1.0",
"react-helmet-async": "^3.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.6.3",
"stripe": "^22.0.0"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.5.2",
"eslint": "^9.29.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.2.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.34.1",
"vite": "^7.0.0"
}
}Notes:
mongooseandstripeappear in the root frontend package, although the backend owns the runtime usage.- The frontend is built with Vite and uses a dev proxy for
/api,/checkout, and/webhook.
From server/package.json:
{
"dependencies": {
"@aws-sdk/client-s3": "^3.1030.0",
"@aws-sdk/lib-storage": "^3.1030.0",
"cors": "^2.8.6",
"dotenv": "^17.4.1",
"express": "^5.2.1",
"mongoose": "^9.4.1",
"multer": "^2.1.1",
"stripe": "^22.0.1"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/multer": "^2.1.0",
"@types/node": "^25.5.2",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typescript": "^6.0.2"
}
}cart-system-typescript/
├── dist/ # Frontend production build output
├── public/ # Public frontend assets
├── server/ # Express / Stripe / Mongo / R2 backend
│ ├── src/
│ │ └── lib/
│ │ └── r2Client.ts # Cloudflare R2 S3-compatible client
│ ├── index.ts # Main Express server entry
│ ├── package.json # Backend dependencies and scripts
│ ├── package-lock.json
│ └── tsconfig.json
├── src/ # Frontend source
│ ├── assets/
│ │ ├── designs/
│ │ │ ├── stampDesign1.png # Preset stamp design asset
│ │ │ └── stampDesign2.png # Preset stamp design asset
│ │ └── react.svg
│ ├── components/
│ │ ├── CartItem.tsx # Cart row renderer
│ │ ├── Footer.tsx
│ │ ├── Navbar.tsx
│ │ ├── ProductCard.tsx # Reusable product card for custom entry points
│ │ ├── ShoppingCart.tsx # Offcanvas cart and checkout trigger
│ │ └── StoreItem.tsx # Standard store item card with cart actions
│ ├── context/
│ │ └── ShoppingCartContext.tsx # Cart state, persistence, and cart item types
│ ├── data/
│ │ ├── customProducts.ts # Stampable customizable product definitions
│ │ └── items.json # Standard catalog data
│ ├── hooks/
│ │ └── useLocalStorage.ts # Local storage persistence hook
│ ├── pages/
│ │ ├── About.tsx
│ │ ├── Failed.tsx
│ │ ├── Home.tsx
│ │ ├── ProductCustomizer.tsx # Canvas customizer, upload flow, add-to-cart integration
│ │ ├── ProductPage.tsx
│ │ └── Success.tsx
│ ├── utilities/
│ │ └── formatCurrency.ts
│ ├── App.tsx # Route definitions
│ ├── index.css # Global and component-level app styling
│ ├── main.tsx # Frontend entry point
│ └── vite-env.d.ts
├── eslint.config.js
├── index.html
├── LICENSE
├── package.json # Frontend dependencies and scripts
├── package-lock.json
├── README.md
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── vercel.json # Vercel deployment config
└── vite.config.ts # Vite config and dev proxy setupDefines all current frontend routes, including the 7 customization routes.
The central cart store for the frontend. This file currently:
- stores cart state in local storage
- exposes cart operations through context
- supports standard items and customized items
- differentiates cart rows using
id,size, and serialized customization metadata
The most feature-rich frontend page. It currently handles:
- product-specific option visibility
- color, size, material, and design selection
- canvas rendering and overlay manipulation
- design uploads through
/api/upload-design - saving customization metadata to the shopping cart
Renders the offcanvas cart and posts the current cart to /checkout.
The main Express app. It currently contains:
- MongoDB connection bootstrap
- Stripe webhook route
- Cloudflare R2 upload route at
/api/upload-design - checkout session route at
/checkout - order persistence after successful Stripe webhook confirmation
Creates the S3-compatible client used for Cloudflare R2 uploads.
//about/product/:id/customize/tshirt/customize/hoodie/customize/sweater/customize/glasscup/customize/hat/customize/apron/customize/totebag/payment/success/payment/failed
GET /GET /healthzPOST /checkoutPOST /webhookPOST /api/upload-design
The cart supports a base item shape plus optional customization metadata.
Current cart items include:
type CartItem = {
id: number;
size: string;
quantity: number;
customization?: {
productType: string;
color?: string;
size?: string;
material?: string;
design: {
id: string;
label: string;
sourceType: "preset" | "upload";
imageUrl: string;
};
transform: {
x: number;
y: number;
scale: number;
rotationDeg: number;
};
};
};This is important because custom items are preserved for fulfillment with:
- the exact chosen design
- whether it came from a preset or a user upload
- the permanent design URL
- placement and transform metadata from the canvas
The current upload flow works like this:
- User chooses a PNG in
ProductCustomizer - Frontend sends
multipart/form-datatoPOST /api/upload-design - Backend validates:
- file exists
- file is PNG
- file is within the 20MB multer limit
- Backend uploads the image buffer to Cloudflare R2
- Backend returns a public URL
- Frontend stores that URL in the selected uploaded design object
- When the item is added to cart, that URL is saved in customization metadata
Preset PNGs do not go through the backend upload route. Their imageUrl is the Vite-resolved static asset URL.
In development, the frontend relies on the Vite proxy in vite.config.ts so requests to:
/api/checkout/webhook
are forwarded to http://localhost:4000.
If the Vite dev server is already running and the proxy config changes, the dev server must be restarted.
The backend expects at least the following environment variables:
PORTMONGO_URICLIENT_URLSTRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRET
R2_ENDPOINTR2_ACCESS_KEY_IDR2_SECRET_ACCESS_KEYR2_BUCKET_NAMER2_PUBLIC_URL
The current backend source documents the R2 values as:
R2_ENDPOINT—https://<accountid>.r2.cloudflarestorage.comR2_ACCESS_KEY_ID— from Cloudflare R2 API tokenR2_SECRET_ACCESS_KEY— from Cloudflare R2 API tokenR2_BUCKET_NAME— for examplestamplabprints-designsR2_PUBLIC_URL— public base URL such ashttps://pub-xxxx.r2.dev
From the repository root:
npm install
npm run devThis starts the Vite dev server on port 5173.
From server/:
npm install
npm run build
node dist/index.jsThe current server/package.json exposes build and start, but does not currently define a dedicated hot-reload dev script.
Run both processes at the same time:
- Start the backend on port
4000 - Start the frontend on port
5173 - Let Vite proxy
/api,/checkout, and/webhookto the backend during development
npm run buildThis runs TypeScript project references and then builds the Vite app.
cd server
npm run buildThis compiles server/index.ts and supporting backend source into server/dist/.
The intended deployment model appears to be:
- Frontend on Vercel
- Backend on Railway
- MongoDB for order persistence
- Stripe for checkout and webhook events
- Cloudflare R2 for customer-uploaded PNG storage
For production, make sure:
- frontend and backend origins are configured correctly
- Stripe webhook secret is set in Railway
CLIENT_URLmatches the deployed frontend URL- Cloudflare R2 credentials and public URL are valid
- MongoDB is reachable from the Railway environment
- The backend order model stores the raw
itemsarray from Stripe webhook metadata, but fulfillment-specific downstream processing is still minimal. - Root
package.jsonstill includes some backend-oriented packages that are not frontend runtime concerns. - The customizer currently supports image placement, scaling, rotation, and cart persistence, but not a final flattened preview export.
- The backend upload route only accepts PNG files.
This is no longer a simple demo cart. It is a small full-stack custom merch flow with:
- a React storefront
- a persistent browser cart
- customizable mockup pages
- PNG asset uploads to Cloudflare R2
- Stripe checkout
- Mongo-backed order capture through webhooks
The codebase is organized clearly enough to extend further in three obvious directions:
- richer product data and pricing
- stronger fulfillment tooling
- better production hardening around uploads, order schemas, and admin workflows