DEV Community

Cover image for πŸŒ€ Vortex: From a Firebase Toy to a Real Multi-Vendor Marketplace (Finished, Finally)
Ayush
Ayush

Posted on

πŸŒ€ Vortex: From a Firebase Toy to a Real Multi-Vendor Marketplace (Finished, Finally)

GitHub β€œFinish-Up-A-Thon” Challenge Submission

Post body

GitHub "Finish-Up-A-Thon" Challenge Submission

This is a submission for the GitHub Finish-Up-A-Thon Challenge.

What I Built

Vortex is a full-stack, multi-vendor e-commerce platform β€” a "mystical archive" of products from independent sellers, with an admin moderation layer, role-based access, and a real Razorpay checkout.

It's a React 19 + Vite 8 + Tailwind 4 frontend talking to Firebase (Auth + Firestore) for data and a small Express 5 backend for payments. It started life as a sleepy single-admin Firebase CRUD app and grew into something I'd actually feel comfortable showing a recruiter.

πŸ”— Live: https://vortex-vyp8.onrender.com/
πŸ”— Repository: https://github.com/hotokeAtlast/Vortex
πŸ”— Backend health check: GET /api/health β†’ { "status": "active", "message": "The Vortex is awake." }

What's actually in the box

  • Three-tier role system β€” Buyer, Seller, Admin β€” with a real users/{uid} profile document, an isAdmin() claim derived from email, and an isActiveSeller() lookup helper used in Firestore rules.
  • Seller Portal β€” full product CRUD, stock tracking, image upload to Cloudinary, gated by an "active" sellerStatus so pending sellers can't dump products into the marketplace.
  • Admin Moderation Panel β€” a single dashboard with stats (pending sellers, review queue, active listings), approve/reject flows for both sellers and products, and content deletion.
  • Razorpay checkout with server-side verification β€” /api/create-order issues the order, /api/verify-payment re-derives the HMAC-SHA256 signature on the Express side and only marks the Firestore order as Paid when the signatures match.
  • 114 lines of Firestore security rules β€” seller-owned product enforcement, admin moderation authority, public read for approved products only, user privacy, and order history isolation.
  • UI/UX polish β€” en-IN currency formatting (β‚Ή1,23,456), proper empty states with CTAs, sticky order summary, dark/light theme, mobile-first responsive grid, scroll-to-top, toast notifications.
  • Single-service deploy β€” the same Express process serves the Vite-built dist/ and the /api/* routes, so one Render Web Service runs the whole thing.

Stack at a glance

Layer Tech
Frontend React 19, Vite 8, Tailwind 4, React Router 7, FontAwesome
Data Firebase Auth, Cloud Firestore, Cloudinary (image upload)
Payments Razorpay (orders + HMAC signature verification)
Backend Node 18+, Express 5, CORS, dotenv
Tooling ESLint 9, Vite Rolldown, concurrently
Deploy Render (single Web Service hosts the whole monorepo)

Demo

πŸ”— Live: https://vortex-vyp8.onrender.com/
πŸ”— Repo: https://github.com/hotokeAtlast/Vortex

Try the three roles in order:

  1. Buyer β€” sign up with a normal email, browse, add to cart, hit the Razorpay test checkout (use the test card 4111 1111 1111 1111).
  2. Seller β€” sign up, pick "Seller" during registration, submit a couple of products. They show up in your SellerPortal immediately as pending.
  3. Admin β€” sign in with the admin email configured in firestore.rules (hotoke.atlast@gmail.com) and use the moderation panel to approve your own products.

Repo quick links

  • server/server.cjs β€” Express API + static frontend host
  • firestore.rules β€” role-based access enforcement
  • src/pages/SellerPortal.jsx β€” seller product management
  • src/pages/Admin.jsx β€” moderation panel
  • src/pages/Checkout.jsx β€” Razorpay integration
  • render.yaml β€” single-service deploy config

The Comeback Story

Where it started

A few months ago, Vortex was a Vite + Firebase project with a single hardcoded admin who could add, edit, and delete products. That was the whole feature set. One user role ("user or the one specific admin email"), a half-finished cart, a checkout that didn't actually charge anyone, no seller concept, no moderation, and a basic Tailwind UI I'd gotten tired of looking at.

I left it because the obvious next step β€” letting other people post products β€” opened a can of worms. Sellers need their own role, their own dashboard, their own approval flow, and the database needs rules that keep one seller from editing another seller's product. Admin needs a moderation queue. Payments need a server. Deploy needs to actually work. None of it was a Saturday afternoon.

So the project sat. Hosted, but rotting.

What I changed

Area Before After
Roles user vs admin (one hardcoded email) buyer / seller / admin with `sellerStatus: pending\
Marketplace Admin posts products directly Sellers apply β†’ admin approves β†’ sellers post β†’ admin approves β†’ buyers see it
Auth flow Sign up β†’ you're in Sign up β†’ pick a role β†’ sellers land in {% raw %}pending state, gated by rules
Checkout "Place order" button that did nothing Razorpay test mode + /api/create-order + HMAC-SHA256 verification on /api/verify-payment
Data integrity No server, all rules imagined 114 lines of firestore.rules with helper functions, role checks, and ownership
UI Light theme, no real empty states, broken currency Dark-first theme toggle, β‚Ή formatting with en-IN locale, proper loading + empty states
Deploy Frontend on Vercel, backend half-deployed to Render Single Render Web Service: Vite build β†’ Express serves both dist/ and /api/*

The deploy line is the one I'm most embarrassed about, because the original render.yaml was technically there but completely broken β€” it had startCommand: npm start && npm run dev and there was no start script in the root. The build never even reached the server startup phase. I just kept ignoring the failed deploy logs.

The finishing touches (this session)

When I finally decided to "finish" Vortex for this challenge, I expected to spend an evening polishing the UI. Instead I spent half of it chasing a deployment that wouldn't boot:

  1. render.yaml had a typo that crashed every deploy β€” startCommand: npm start && npm run dev because I never wrote a start script. Fixed with npm start + a real start entry that resolves the right entry point.
  2. ESM-vs-CommonJS collision β€” the root package.json was "type": "module", but the server file used require(). Renamed server/server.js β†’ server/server.cjs so it stays CommonJS regardless of the parent's type, and updated the root start command to point at it.
  3. Build was missing devDependencies β€” Vite and @vitejs/plugin-react lived in devDependencies, but Render's NODE_ENV=production made npm install skip them. Added --include=dev to the install command and NPM_CONFIG_PRODUCTION=false to the env vars as a belt-and-braces fix.
  4. Razorpay would crash the process if env vars were missing β€” moved instantiation to lazy, added a startup warning and a 503 response when keys aren't configured. Now a fresh Render deploy without Razorpay secrets still boots cleanly.
  5. /api/health was the only thing that worked β€” refactored the Express server to also serve the built Vite dist/ folder and act as an SPA catch-all (regex route that returns index.html for any non-/api/* path) so a single Render service hosts the whole thing.
  6. Frontend checkout was hardcoded to someone else's Render URL β€” https://vortex-api-6wk1.onrender.com/api/... from an earlier experiment. Replaced with a VITE_API_BASE_URL env var, empty by default, so same-origin requests just work.
  7. The page wouldn't scroll β€” leftover Vite starter CSS had #root { width: 1126px; min-height: 100svh; display: flex; flex-direction: column; } which broke the scroll chain. Replaced it with a 4-line reset (html, body, #root { height: 100%; } body { margin: 0; }) and let Tailwind handle the rest.

After that, npm run build && npm start boots in 3 seconds, the health check pings green, and the Vite bundle is served from the same Express process. The site finally works in production β€” which is, in retrospect, the most basic requirement of "finished."


My Experience with GitHub Copilot

Copilot was a co-author on basically every part of this project β€” but the way I used it changed shape across the two phases.

Phase 1: shipping features (the original sessions)

I used Copilot heavily in agent mode to design the role-based auth flow. The thing I kept getting wrong on my own was the invariants of the role transition: a new user should land as buyer, a buyer should be able to apply to become a seller (which sets role: 'seller' and sellerStatus: 'pending' simultaneously), but a pending seller should not be able to flip themselves to active and start posting. That's a tiny state machine with a security guarantee attached to it, and I was over-engineering it.

What worked was describing the state machine to Copilot in plain English and asking it to write the Firestore rule helpers first, then the React context code second. The helpers β€” isAdmin(), isSelf(), isActiveSeller(), userRequestsSellerRoleChange() β€” came out almost unchanged from its first draft, and they made the rest of the system obviously correct. I wasn't learning "how to use Firestore rules" β€” I was learning "the shape of a security-relevant state machine" by watching one get drawn.

Same story on the seller portal UI. I had a vague "sellers should be able to manage their own products" idea. Copilot turned that into a <SellerPortal /> page with a form, an inventory list, an image upload to Cloudinary, and a "pending review" badge β€” all in a single iteration. I edited maybe 20% of it (mostly the date/time formatting and the stock number input), but the shape was right on the first pass.

On the Razorpay side, the lesson was: the integration is short, but the trust boundary is the part you have to be careful about. The whole "create order" endpoint is 20 lines. The verify endpoint is 30. The reason I shipped it confidently is that Copilot and I went over the signature verification together: it wrote the crypto.createHmac('sha256', key).update(sign).digest('hex') snippet, I asked "what if the order id is missing," and it added the input check. That's not autocomplete β€” that's a code review partner.

Phase 2: finishing the deploy (this week)

The deployment debugging was where Copilot's value flipped. Most of this work was not writing new code β€” it was reading logs, forming hypotheses, and making tiny targeted changes. Copilot was great at the "here's a thing I broke, explain why" loop:

  • "Why would npm start && npm run dev fail on Render?" β†’ It explained the && short-circuit and the missing start script in one sentence, then suggested the right start script body. I would've eventually figured this out from the deploy log, but Copilot got me there in seconds.
  • "What does Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@vitejs/plugin-react' mean in this context?" β†’ Instantly: "Vite is in devDependencies and Render's NODE_ENV=production is hiding them. Use --include=dev or set NPM_CONFIG_PRODUCTION=false." That was the exact fix I needed.
  • "Show me an Express middleware that serves dist/ for a Vite SPA and falls back to index.html for any non-/api/* route." β†’ Drop-in snippet, I pasted it, it worked, I moved on.

The thing I appreciated most was the discipline to not over-use it. When the page wouldn't scroll and I suspected the leftover #root { width: 1126px; } from the Vite starter, I could've asked Copilot to rewrite the whole stylesheet. Instead I asked "is this specific CSS rule the problem?" β€” Copilot said yes, I deleted 100 lines and wrote 4, and the scrollbar came back. Sometimes the right Copilot prompt is the short one.

What I still pushed back on

  • Copilot's first draft of the Firestore create rule on users/{userId} was permissive about role changes. I tightened it: only admins can set role: 'admin', and the seller-role upgrade has to come with sellerStatus: 'pending'. Verified by writing a failing test case (a non-admin trying to set role: 'admin') and watching the rule correctly reject it.
  • The first version of the Express static-file handler was app.get('*', ...) which would have intercepted /api/* requests too. I caught it and switched to the ^(?!api/).* regex.
  • I didn't let Copilot touch firestore.rules "for me" β€” security rules are the one place I want my name on every line. It helped me think about the rule shape, but I wrote and tested them by hand against the Firebase emulator.

What's next

Vortex is in a state I'm actually happy with. Things I might still do, in priority order:

  1. Replace the hardcoded admin email in isAdmin() with a real Firebase Auth custom claim, so admin status is provable and revocable from the Firebase console.
  2. Add a "withdraw product" flow so sellers can delist items without admin intervention.
  3. Wire up a proper email receipt via the existing SMTP path.
  4. Add Playwright e2e tests for the three role flows (buyer checkout, seller CRUD, admin moderation).

None of those are blockers for calling this finished. The thing this challenge gave me was the deadline pressure to actually ship what I had, instead of letting the next-feature itch keep the project in permanent limbo.

Thanks for reading β€” and if you've got an abandoned side project, this is your sign: open the deploy logs, fix the typo, push the button, and call it done.

Top comments (0)