MessMasterKnowledge hub
Tech Guide

The Technology Behind MessMaster: A Full-Stack Architecture

MessMaster isn't a weekend project. It's a production-grade system built on a modern, battle-tested technology stack designed for speed, security, and scale.

Layered architecture of the MessMaster technology stack
Layered architecture of the MessMaster technology stack
Contents9 sections
  1. The architecture overview
  2. Frontend: React 19 and friends
  3. Backend: Express 5 and Prisma 7
  4. Database: Postgres with Supabase Auth
  5. Realtime: Socket.IO event flow
  6. Identity and security model
  7. File storage and email
  8. Background jobs and scheduling
  9. Deployment: Vercel and Render
  • Member: uses the app without thinking about the stack — it just works.
  • Meal manager: benefits from realtime updates, automatic calculations, and zero server maintenance.

Your available controls and calculations depend on your role, mess status, deployment, and current server data. This guide uses fictional examples and does not replace the values shown in the app.

Guide status: Labels, permissions, calculation rules, rates, costs, exports, and available controls can vary by deployment and mess configuration. Use the current screen as the source to check and verify saved records after important changes.

The architecture overview

MessMaster is split into two deployables from one repository: a Vite SPA (Vercel) and an Express API (Render). In production, Vercel rewrites /api/* and /socket.io/* to the Render backend and everything else to /index.html.

The five architecture layers are:

  1. React 19 UI — single-page application with React Router 7 and Zustand state management.
  2. Express 5 API — REST endpoints plus Socket.IO for realtime events.
  3. Prisma 7 ORM — type-safe database access over a pg.Pool driver adapter.
  4. Supabase Auth — JWT-based identity shared across HTTP and Socket.IO.
  5. Cloudinary — image and receipt storage with automatic cleanup.

Each layer has a clear boundary. The frontend never calls the database directly. The backend never serves the SPA in production. Identity flows through JWTs, not session cookies.

Frontend: React 19 and friends

The frontend is a modern React stack:

  • React 19 with concurrent rendering and Suspense support
  • React Router 7 for route definitions (though the actual routing is a state machine in useAppCore)
  • Zustand for lightweight state management with slices for auth, mess, payments, fines, and UI
  • Tailwind 4 for styling with a custom design system
  • Socket.IO client for realtime updates
  • Framer Motion for animations

All API calls go through a configured axios instance in services/api/config.ts. No component calls fetch directly.

State management

Zustand stores are split into slices: authStore, messStore, paymentStore, fineStore, monthTransitionStore, and uiStore. Each slice owns a specific domain — no god store.

Routing as a state machine

Real routing is currentView state in the app core (useAppCore.tsx), bidirectionally synced to the URL by useRouteSync. The router.tsx file is only a canonical route constant table — it defines no routes. This design allows fine-grained control over navigation transitions and guards.

Backend: Express 5 and Prisma 7

The backend is an Express 5 application with:

  • Prisma 7 with the @prisma/adapter-pg driver adapter over a pg.Pool
  • Zod for request validation (422 responses with field-level error details)
  • Joi as a legacy validator in a few remaining call sites
  • Socket.IO sharing the same JWT secret as HTTP for realtime auth
  • JobLock for safe cron-like background jobs

Response envelope

All responses use a standard envelope: { success, data?, error?, message?, code?, field? }. The global error handler sanitizes 5xx messages so stack traces never reach clients.

Validation layers

Two validators coexist by layer — don't mix them:

  • Zod via validate(schema) middleware validates req.body and returns 422 with field-level details
  • Joi remains for a few legacy call sites

Database: Postgres with Supabase Auth

Supabase Auth and Postgres/Prisma are parallel systems linked by UID, not by foreign keys across systems.

  • Supabase Auth owns credentials. The JWT sub claim is the identity used everywhere.
  • Prisma owns all application data. Owner.id, PortalMember.id, and MessMember.userUid are Supabase UIDs.
  • mess_code + membership_id is the app's composite identity, distinct from auth identity.

Schema management

prisma/schema.prisma is the source of truth. prisma/migrations/ is authoritative. supabase/migrations/ holds RLS policies and security hardening SQL — they are not mirrors.

Soft deletes

Messes use softDeletedAt and permanentDeleteAt for soft deletion. A cleanup service permanently purges deleted messes and drains a Cloudinary deletion queue with retry.

Realtime: Socket.IO event flow

Realtime is powered by Socket.IO, sharing the same JWT secret as HTTP. This means a single token authenticates both REST and WebSocket connections.

The data flow is:

  1. A user action triggers a Socket.IO emit.
  2. The Express handler validates and persists the change via Prisma.
  3. The server broadcasts the update to all connected clients in the same mess.
  4. The Zustand store updates and React re-renders.

Event types

  • Meal updates — tracker changes sync across all members
  • Payment status — approve/reject notifications
  • Member join — role assignment notifications
  • Month transition — balance carry updates

Identity and security model

Authentication is stateless JWT verification — requireAuth verifies the JWT locally with jwt.verify(token, SUPABASE_JWT_SECRET). It does not call Supabase.

Token invalidation is layered:

  • Logout → exact-token blacklist
  • Session revoke → revoked-before:<uid>:<ts> marker compared to token iat
  • Owner sessions → OwnerSession rows add a second expiry check

Role guards

  • requireMessManager — mess admin access
  • requireMessMember — member access
  • requireOwnerRole — platform owner access
  • requireOwnerOrManagerRole — shared admin
  • messStatusGuard — only admits status === "active"

Rate limiting

Per-purpose rate limiters: loginLimiter, registerLimiter, passwordResetLimiter, apiLimiter, emailTriggerLimiter. trust proxy is set to 1 (not true) in production so IP-keyed limiters see the real client behind Vercel/Render.

File storage and email

Cloudinary

Image and receipt uploads go to Cloudinary. A cleanup service drains a MessCleanupQueue for asset deletion with retry logic.

Email

Email sends go through a failover chain: brevo → mailersend → resend → smtp. The system falls through on provider failure or daily-cap exhaustion. Email volume is capped per provider per day.

Background jobs and scheduling

Background jobs run inside the API process using cron-like scheduling with JobLock for safety:

  • Low-balance notifications (6h)
  • Monthly statements
  • Bazar reminders (8PM Asia/Dhaka)
  • Cloudinary deletion retry (30min)
  • Tracker-stats refresh queue (5min)
  • Home/tracker stats reconciliation (6h)
  • Self-ping keep-alive (10min)

If multiple instances run, JobLock prevents duplicate execution.

Deployment: Vercel and Render

  • Frontend (Vite SPA) deploys to Vercel. vercel.json rewrites /api/* and /socket.io/* to Render.
  • Backend (Express API) deploys to Render.
  • In production, server.ts does not serve the SPA — Vercel handles static files.
  • Locally, Vite's proxy simulates this architecture.

The build pipeline runs npm run typecheck and npm test -- --run in CI. Both must pass before merge.

Important: This guide describes the architecture as implemented. Specific configurations, rate limits, email providers, and job schedules can vary by deployment. Always verify the live configuration before making changes.

Tags

  • #architecture
  • #tech
  • #full-stack
  • #messmaster