A full-stack event platform and merch store for an annual summer festival — public site, e-commerce shop, and admin CMS in a single MERN application.
JUNEFEST powers a recurring June celebration (bar crawls, golf outings, happy hours) with everything the event needs in one place: a public site with a live countdown and event RSVPs, a merchandise shop with real PayPal checkout, and a full admin panel for managing every piece of content — no code deploys required to update the site.
Live: frontend on Netlify · API on Render · data on MongoDB Atlas · images on Cloudinary · payments via PayPal
| What it is | Event site + e-commerce shop + admin CMS, unified |
| Frontend | React 18 · Redux Toolkit / RTK Query · React Router v6 · React Bootstrap |
| Backend | Node.js · Express · Mongoose (MongoDB Atlas) |
| Auth | JWT (bcrypt-hashed passwords) with role-based access control |
| Payments | PayPal Orders API, verified server-side |
| Media | Cloudinary upload pipeline via Multer |
| Deploy | Netlify (SPA) + Render (API) |
flowchart LR
subgraph Client["Browser — React SPA (Netlify)"]
UI["React 18 + React Bootstrap<br/>public site · shop · admin panel"]
RTK["Redux Toolkit / RTK Query<br/>7 API slices, tag-based cache invalidation"]
UI --> RTK
end
subgraph Server["Express API (Render)"]
MW["Middleware<br/>JWT auth · admin gate · error handler"]
ROUTES["REST routes<br/>/api/products · /api/orders · /api/users<br/>/api/events · /api/bars · /api/carousel<br/>/api/settings · /api/upload"]
CTRL["Controllers<br/>server-side pricing · stock control<br/>payment verification"]
MW --> ROUTES --> CTRL
end
DB[("MongoDB Atlas<br/>7 Mongoose models")]
PAYPAL["PayPal Orders API<br/>server-verified payments"]
CLOUD["Cloudinary<br/>image hosting"]
RTK -- "REST / Bearer JWT" --> MW
CTRL --> DB
CTRL -- "verify: status + amount + replay check" --> PAYPAL
CTRL -- "Multer + CloudinaryStorage" --> CLOUD
The flow in one sentence: the React SPA talks to the Express API over REST with a Bearer JWT; the API recomputes anything money- or stock-related on the server, verifies payments directly with PayPal, and persists everything in MongoDB.
sequenceDiagram
participant U as User (React)
participant API as Express API
participant DB as MongoDB
participant PP as PayPal
U->>API: POST /api/orders (cart items)
API->>DB: Validate stock, recompute prices server-side
API->>DB: Atomically decrement inventory ($inc)
API-->>U: Order created
U->>PP: Pay via PayPal buttons
U->>API: PUT /api/orders/:id/pay (transaction id)
API->>PP: Verify: status COMPLETED?
API->>PP: Verify: amount matches order total?
API->>DB: Verify: transaction id never used before?
API->>DB: Mark paid
API-->>U: Receipt
Things in this codebase that go beyond the standard tutorial-grade MERN store:
- Server-verified payments with anti-fraud checks — the backend never trusts the client's word that an order was paid. It fetches its own PayPal OAuth token and confirms three things before marking an order paid: the transaction status is
COMPLETED, the captured amount exactly matches the order total, and the transaction ID has never been used on another order (replay protection). (backend/utils/paypal.js) - Trusted server-side pricing — item prices, 15% tax, and free-shipping-over-$100 are all recomputed from the database at order time; client-submitted prices are ignored. (
backend/utils/calcPrices.js) - Inventory integrity — stock is validated and decremented atomically (
$inc) when an order is created, and restored when an order is cancelled, with guards that prevent cancelling delivered or already-cancelled orders. - Role-based access control, end to end —
protect(JWT) andadminmiddleware on the API, mirrored byPrivateRoute/AdminRouteguards in the router. - Event RSVP system — authenticated RSVP / un-RSVP per event with duplicate prevention, plus an admin roster view of attendees.
- Admin CMS across six content types — products, orders, users, events, bars, and carousel images, with soft-delete (
isActive) toggles and display ordering, so the whole site is content-managed. - Modern data layer — RTK Query with one injected API slice per resource and tag-based cache invalidation; cart and auth state persisted to
localStorage. - Cloudinary media pipeline — admin image uploads flow through Multer with double (MIME + extension) validation into a dedicated Cloudinary folder.
- Admin-editable countdown — the homepage countdown reads a
Settingssingleton, so the event date and label are changed from the admin panel, not a deploy.
| Area | What's there |
|---|---|
| Public site | Homepage (countdown, photo carousel, team), bar-crawl listings, event pages with RSVP |
| Shop | Paginated + searchable catalog, product reviews & ratings, cart, multi-step checkout, PayPal payment, order history |
| Account | Register / login, profile management, my orders, my RSVPs |
| Admin | Dashboard + management screens for carousel, bars, events (with photos & RSVP rosters), products, orders (pay / deliver / cancel), and users |
| Path | Description |
|---|---|
/ |
Homepage — countdown, carousel, team |
/bars · /events · /events/:id |
Listings + event detail with RSVP |
/shop · /product/:id · /cart |
Merch catalog through checkout |
/profile |
Orders + RSVPs |
/admin/* |
Dashboard, carousel, bars, events, products, orders, users |
Seven Mongoose models, all timestamped:
| Model | Purpose |
|---|---|
User |
Auth identity — bcrypt-hashed password (pre('save') hook), isAdmin role flag |
Product |
Catalog item with embedded reviews subdocuments, rating aggregates, stock count |
Order |
Price-snapshot line items, shipping, payment result, paid / delivered / cancelled lifecycle |
Event |
Typed events (bar-crawl / golf / happy-hour) with embedded photos and RSVPs |
Bar |
Bar-crawl venue — featured drink, website, display order |
CarouselImage |
Homepage carousel entries with ordering and active flag |
Settings |
Singleton config driving the countdown date/label |
junefest-main/
├── backend/
│ ├── server.js Express app: CORS, routers, static, error handling
│ ├── seeder.js DB seed / destroy scripts
│ ├── config/ Mongo connection
│ ├── controllers/ One per resource (products, orders, users, events, bars, carousel, settings)
│ ├── middleware/ JWT auth, admin gate, asyncHandler, ObjectId check, error handler
│ ├── models/ 7 Mongoose schemas
│ ├── routes/ REST routers, incl. Cloudinary upload
│ └── utils/ calcPrices · generateToken · paypal (server-side verification)
├── frontend/
│ └── src/
│ ├── screens/ Public, checkout, and Admin/ screens
│ ├── components/ CountdownTimer, carousels, Rating, Paginate, route guards…
│ ├── slices/ RTK Query API slices per resource + auth/cart slices
│ └── assets/ Custom Bootstrap theme
└── netlify.toml SPA build + redirect config
# 1. Install
npm install
npm install --prefix frontend
# 2. Configure — copy example.env to .env and fill in:
# NODE_ENV, PORT, MONGO_URI, JWT_SECRET, PAGINATION_LIMIT
# PAYPAL_CLIENT_ID, PAYPAL_APP_SECRET, PAYPAL_API_URL
# CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET
# 3. Seed the database (bars, events, products, users)
npm run data:import # npm run data:destroy to clear
# Default admin: admin@email.com / 123456
# 4. Run both servers
npm run dev # backend :8000 + frontend :3000, concurrentlyOther scripts: npm run server (API only, nodemon) · npm run client (SPA only) · npm run build (production build) · npm run debug (Node inspector).
Netlify (frontend) — build settings live in netlify.toml (base frontend, SPA catch-all redirect). Set REACT_APP_API_URL to the backend URL.
Render (backend) — build npm install, start node backend/server.js, with all .env vars configured (including the three CLOUDINARY_* vars).
Images — uploads go straight to Cloudinary in every environment via multer-storage-cloudinary; nothing is stored on the app server's disk.