Skip to content

Repository files navigation

Festigo — sponsorship on the go

A two-sided marketplace connecting college events with brand sponsors.

Colleges publish what they are running, what it costs and who attends. Brands publish what they want to back and how much they will spend. A match can start from either side, and both paths converge on the same structured sponsorship record — instead of a cold email thread that goes nowhere.

Stack: React 18 · Vite · Node.js · Express · MongoDB (Mongoose) · Socket.IO · JWT


The two matching flows

This is the core of the product. Most marketplaces only work in one direction; Festigo runs both, because sometimes the college moves first and sometimes the brand does.

Event-first

college publishes an event      POST   /api/events
brand applies with an amount    POST   /api/events/:id/interests
college reviews applicants      GET    /api/events/:id/interests
college accepts or declines     PATCH  /api/events/:id/interests/:brandId
        └── on accept → Sponsorship { source: 'event-first' } + shared thread

Brand-first

brand publishes a listing       POST   /api/listings
college sends a PDF proposal    POST   /api/proposals        (multipart)
brand reviews the decks         GET    /api/proposals/received
brand accepts or declines       PATCH  /api/proposals/:id/status
        └── on accept → Sponsorship { source: 'brand-first' } + shared thread

Both write to one Sponsorship collection tagged with the source that produced it, so a dashboard can show the whole pipeline regardless of which direction each deal came from.

State transitions are enforced server-side — a pending application can only become accepted or rejected, an accepted one can only become completed, and a decided proposal cannot be decided twice.


Running it locally

Prerequisites: Node 18+ and a MongoDB instance (local mongod, or a free MongoDB Atlas cluster).

1. API

cd backend
npm install
cp .env.example .env

Edit backend/.env — at minimum set MONGODB_URI and a real JWT_SECRET:

node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"

The server refuses to boot without them, rather than silently falling back to a hardcoded default.

npm run seed    # demo colleges, brands, events, listings and in-flight matches
npm start       # http://localhost:5050

Port 5050 rather than 5000 because macOS AirPlay Receiver occupies 5000.

2. Frontend

cd ..           # repo root
npm install
npm run dev     # http://localhost:5173

Leave VITE_API_URL empty in development. The Vite dev server proxies /api and /socket.io to the backend, so the browser stays on a single origin and there is no CORS preflight in the loop.

3. Demo accounts

All seeded accounts use the password festigo123. The login screen has one-click buttons for the two below.

Role Email Organisation
College vjti@festigo.demo VJTI Mumbai
College srcc@festigo.demo Shri Ram College of Commerce
College christ@festigo.demo Christ University
Brand stackforge@festigo.demo StackForge
Brand zephyr@festigo.demo Zephyr Energy

To see both flows immediately: log in as StackForge, apply to a VJTI event, then log in as VJTI and accept it. Or log in as VJTI, send a PDF proposal to a StackForge listing, then accept it as StackForge.


Tests

Four suites, all running against a live server — real HTTP, real sockets, a real browser. No mocks.

# terminal 1
cd backend && npm run seed && npm start

# terminal 2 — API suites
cd backend && npm run verify

# terminal 3 — browser suite (needs the frontend running too)
npm run dev          # in another terminal
npm run test:ui
Command Checks What it covers
npm run smoke 53 Both flows end to end, auth, role guards, ownership, PDF upload/download, state transitions
npm run contract 45 Every API path the frontend calls resolves — catches client/server drift
npm run realtime 9 Socket handshake, participant-gated rooms, live delivery, typing, and that outsiders receive nothing
npm run test:ui 30 Playwright walkthrough of both flows in Chromium, plus dark mode, mobile layout and console errors

npm run verify (in backend/) runs the first three. npm run test:ui (at the root) reseeds first, since the walkthrough consumes state, and drops screenshots in .artifacts/.

Two bugs the browser suite caught that the HTTP suites could not:

  • The rate limiter was mounted on the whole /api/auth router, including GET /verify — which the client calls on every page load. A user refreshing a few dozen times would have been locked out. It is now scoped to the three endpoints that actually accept a password.
  • Messages could render twice. The sender is in their own conversation room, so their message arrives over the socket and in the POST response, in either order. Both paths now go through one append that dedupes by id.

Security model

The first version of this project trusted the client in several places. Those are the bugs worth calling out, because fixing them shaped the current design.

Was Now
JWT signed with a hardcoded 'your-secret-key' Secret from the environment; the server refuses to boot without it
/api/brands/:brandId/dashboard read the id from the URL Derived from the token — there is no id to swap
Conversation routes took participant ids from the body Every route asserts the caller is one of the two participants
/api/proposals/create had no auth middleware at all Authenticated, college-only, and the brand is resolved from the listing
Proposal PDFs served from a public static directory Streamed through an authorized route, restricted to the two parties
Password hashes returned in login responses select: false on the field plus a single publicProfile() serializer

Also in place: helmet, rate limiting on the credential endpoints, bcrypt at cost 12, identical error text for unknown-email and wrong-password (so the endpoint cannot enumerate accounts), and a whitelist on profile updates so a caller cannot patch their own role.

Every row in the first table has a corresponding test in npm run smoke.


Notable implementation details

PDF storage. Proposal decks go into GridFS rather than the local disk. Hosts like Render and Fly have an ephemeral filesystem, so disk-written uploads disappear on the next deploy or restart. Multer still handles the upload — it buffers in memory and the buffer is streamed into GridFS.

Authenticated file downloads. A plain <a href> or window.open cannot attach an Authorization header, so the client fetches the PDF through axios and hands the browser a blob URL (src/lib/files.js).

One thread per subject. Conversations are unique on (college, brand, event) and (college, brand, listing) via partial indexes, so both sides always land in the same thread instead of forking a new one per action.

Realtime authorization. The Socket.IO handshake reuses the REST JWT. Joining a conversation room is gated on being a participant — otherwise any authenticated user could subscribe to anyone's thread.

Dark mode. Colours are declared as channel triplets (--brand-rgb: 99 102 241) so components can build translucent variants with rgb(var(--brand-rgb) / 0.12) without a second hardcoded value. The theme follows the OS by default, with a manual override that wins in both directions.


Deploying

Config is committed for the common hosts: render.yaml (both services), vercel.json and netlify.toml (frontend), and backend/Dockerfile.

  1. Database — create a free MongoDB Atlas cluster, add a database user, and allow network access from anywhere (0.0.0.0/0) since managed hosts do not publish fixed egress IPs on free plans. Copy the connection string.

  2. API — deploy backend/ as a Node web service. Set MONGODB_URI, JWT_SECRET, NODE_ENV=production, and CLIENT_ORIGINS to the frontend origin. Health check path is /health.

  3. Frontend — deploy the repo root as a static site. Build npm run build, publish dist, and set VITE_API_URL to the API origin (no trailing slash). The SPA fallback rewrite is already in each host's config file.

  4. Close the loop — neither URL exists until its service is created, so set CLIENT_ORIGINS and VITE_API_URL after the first deploy and redeploy both. CORS rejects unlisted origins, so a mismatch here shows up as a failed request rather than a silent misconfiguration.

VITE_API_URL is read at build time, not runtime — changing it requires a rebuild, not just a restart.


Project layout

backend/
  app.js              Express app: middleware, CORS, routes, error handling
  server.js           bootstrap: Mongo connect, HTTP + Socket.IO, shutdown
  config/env.js       validates required env vars at boot
  middleware/         auth (requireAuth, requireRole), error handling
  models/             User (+College/Brand discriminators), Event, Listing,
                      Proposal, Sponsorship, Conversation, Notification
  routes/             auth, events, listings, proposals, sponsorships,
                      brands, messages, files
  services/           conversations, notify, storage (GridFS)
  sockets/            authenticated Socket.IO server and room helpers
  scripts/            seed, smoke, contract, realtime

src/
  lib/                api client, socket, hooks, formatters, file download
  context/            Auth, Realtime, Theme, Toast
  components/ui/      design-system primitives
  components/layout/  navbar, footer
  pages/              landing, auth, browse, detail, dashboards, messages
  styles/             tokens.css, global.css, ui.css

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages