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, anisAdmin()claim derived from email, and anisActiveSeller()lookup helper used in Firestore rules. -
Seller Portal β full product CRUD, stock tracking, image upload to Cloudinary, gated by an "active"
sellerStatusso 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-orderissues the order,/api/verify-paymentre-derives the HMAC-SHA256 signature on the Express side and only marks the Firestore order asPaidwhen 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-INcurrency 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:
-
Buyer β sign up with a normal email, browse, add to cart, hit the Razorpay test checkout (use the test card
4111 1111 1111 1111). -
Seller β sign up, pick "Seller" during registration, submit a couple of products. They show up in your
SellerPortalimmediately aspending. -
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:
-
render.yamlhad a typo that crashed every deploy βstartCommand: npm start && npm run devbecause I never wrote astartscript. Fixed withnpm start+ a realstartentry that resolves the right entry point. -
ESM-vs-CommonJS collision β the root
package.jsonwas"type": "module", but the server file usedrequire(). Renamedserver/server.jsβserver/server.cjsso it stays CommonJS regardless of the parent's type, and updated the root start command to point at it. -
Build was missing devDependencies β Vite and
@vitejs/plugin-reactlived indevDependencies, but Render'sNODE_ENV=productionmadenpm installskip them. Added--include=devto the install command andNPM_CONFIG_PRODUCTION=falseto the env vars as a belt-and-braces fix. -
Razorpay would crash the process if env vars were missing β moved instantiation to lazy, added a startup warning and a
503response when keys aren't configured. Now a fresh Render deploy without Razorpay secrets still boots cleanly. -
/api/healthwas the only thing that worked β refactored the Express server to also serve the built Vitedist/folder and act as an SPA catch-all (regex route that returnsindex.htmlfor any non-/api/*path) so a single Render service hosts the whole thing. -
Frontend checkout was hardcoded to someone else's Render URL β
https://vortex-api-6wk1.onrender.com/api/...from an earlier experiment. Replaced with aVITE_API_BASE_URLenv var, empty by default, so same-origin requests just work. -
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 devfail on Render?" β It explained the&&short-circuit and the missingstartscript in one sentence, then suggested the rightstartscript 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 indevDependenciesand Render'sNODE_ENV=productionis hiding them. Use--include=devor setNPM_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 toindex.htmlfor 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
createrule onusers/{userId}was permissive aboutrolechanges. I tightened it: only admins can setrole: 'admin', and the seller-role upgrade has to come withsellerStatus: 'pending'. Verified by writing a failing test case (a non-admin trying to setrole: '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:
- 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. - Add a "withdraw product" flow so sellers can delist items without admin intervention.
- Wire up a proper email receipt via the existing SMTP path.
- 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)