Zynk is a classroom and live-meeting platform that combines role-based academic workflows with real-time communication. The stack covers classroom management, scheduled and ad hoc meetings, persistent classroom chat, in-meeting chat and polls, file sharing, and a Mediasoup-based SFU for WebRTC video.
- Backend: Node.js, Express 5, MongoDB with Mongoose, Socket.IO 4, Mediasoup 3, Redis via
ioredis, Cloudinary - Frontend: React 19, Vite, Tailwind CSS 4,
mediasoup-client - Auth and security: JWT auth, bcrypt password hashing, CryptoJS AES for classroom chat payload encryption
- Teacher and student authentication with role-aware profile flows
- Classroom creation, enrollment, resources, announcements, and persistent chat
- Instant and scheduled meetings tied to classrooms
- Real-time meeting admission, permissions, hand raise, chat, and polls
- SFU-based multiparty video using Mediasoup
- Cloudinary-backed resource uploads and backend-proxied downloads
apps/
├── backend/
│ ├── index.js
│ ├── server.js
│ ├── db.connect.js
│ ├── config/
│ ├── middleware/
│ ├── models/
│ ├── controllers/
│ ├── routes/
│ ├── sfu/
│ ├── sockets/
│ └── utils/
└── frontend/
├── public/
└── src/
├── components/
├── context/
├── hooks/
├── pages/
└── utils/
apps/backend/index.js is the main entrypoint:
- Loads environment variables.
- Connects to MongoDB.
- Starts the Express server.
- Creates an HTTP or HTTPS server depending on TLS env vars.
- Attaches Socket.IO with shared CORS configuration.
- Applies socket authentication middleware to every handshake.
- Registers REST middleware and route modules.
- Spawns Mediasoup workers and activates the SFU layer.
- Express handles REST APIs for auth, profiles, classrooms, announcements, meetings, resources, and classroom chat history.
- Socket.IO handles live collaboration events on the default namespace.
- Mediasoup handles media routing for audio, video, and screen share.
- MongoDB stores durable domain data.
- Redis mirrors room and peer presence but is not the system of record.
- Cloudinary stores uploaded classroom files.
Main identity record with email, fullName, password, role, institution, profileCompleted, and isVerified.
Institution is derived from the email domain during signup.
Teacher profile linked one-to-one with User.
Student profile is auto-created from structured email input and stores programme, branch, semester, and batch year metadata.
Represents an academic group with teacher assignments, student membership, demographic eligibility filters, announcements, resources, invite code, chat toggle state, and soft-delete via isActive.
Stores classroom-scoped announcement content authored by a user.
Persistent classroom chat message with AES-encrypted content, replies, mentions, and reactions.
Uploaded classroom asset stored in Cloudinary and linked to a classroom.
Tracks room metadata, host, schedule, participants, blacklist, and meeting lifecycle.
Stores live poll configuration, votes, correct answer, status, and timer metadata.
Base path: /api/auth
POST /signup: Create user, and auto-create student profile when applicablePOST /login: Return access and refresh JWTsGET /validate: Validate current access token
Base path: /api/profiles
POST /: Create teacher profileGET /: Fetch current user profilePATCH /: Update profile and sync key fields back toUser
Base path: /api/classrooms
GET /: List classrooms visible to the authenticated userPOST /: Create classroom as teacherGET /:id: Get one classroom with role-aware access controlPATCH /:id: Update classroom metadataDELETE /:id: Soft-delete classroomPOST /:id/enroll: Enroll eligible studentPOST /:id/resources: Upload resourceDELETE /:id/resources/:resourceId: Delete resource and Cloudinary assetGET /:id/resources/:resourceId/download: Proxy-download a resourceGET /:id/messages: Fetch paginated classroom chat historyPOST /:id/messages: Create classroom chat messagePOST /:id/messages/:msgId/react: Toggle reaction on messagePATCH /:id/chat/toggle: Enable or disable classroom chat
Base path: /api/announcements
GET /:classroomId: List announcementsPOST /:classroomId: Create announcement as teacher
Base path: /api/meets
POST /create: Create instant meetingPOST /schedule: Schedule classroom meetingPATCH /schedule/:id: Update scheduled meetingDELETE /schedule/:id: Cancel scheduled meetingGET /upcoming/all: List upcoming meetings for the current userGET /upcoming/:classroomId: List classroom-specific upcoming meetingsGET /history: List past meetingsGET /:roomId: Fetch public room metadata for joiningGET /:roomId/end: End meeting as host
- JWT payload includes
id,email, androle. - Access tokens expire in 1 day.
- Refresh tokens expire in 7 days.
requireAuthvalidates bearer tokens and attaches the authenticated user.isTeacherrestricts teacher-only routes.- Classroom-level authorization is rechecked inside controllers.
- Socket.IO uses a dedicated handshake auth middleware to validate the same JWT.
The backend invalidates tokens issued before the current server boot time. This forces users to log in again after every backend restart.
All Socket.IO handlers are mounted on the default namespace.
apps/backend/sockets/sfu.socket.js handles:
- room join and leave
- transport creation and DTLS connection
- producer creation, pause, resume, and close
- consumer creation and resume
- meeting end and participant removal
- hand raise state
The server emits router RTP capabilities, existing producer lists, producer lifecycle events, participant updates, and meeting lifecycle events.
apps/backend/sockets/room.socket.js handles:
- lobby join requests
- host approval and rejection
- mic, camera, and screen-share permission grants and revocations
apps/backend/sockets/chat.socket.js provides ephemeral room chat kept only in memory.
apps/backend/sockets/poll.socket.js handles active poll state, vote submission, host-triggered end, and timer-based auto-end.
apps/backend/sockets/classroomChat.socket.js only manages Socket.IO room membership. Message persistence lives in REST controllers, which then broadcast updates back to subscribed clients.
- One Mediasoup worker is created per CPU core.
- Workers receive dedicated RTC port ranges.
- Worker selection is round-robin.
- Dead workers are automatically restarted.
Each meeting room gets one Mediasoup router. The configured codecs are:
- audio: Opus, 48 kHz, 2 channels
- video: VP8, 90 kHz
Simulcast and SVC are not enabled in the current design.
Room state is maintained in memory inside the room manager:
- router
- peers
- chat enabled flag
- in-memory meeting chat messages
- active poll id
Peer state tracks transports, producers, consumers, join time, hand raise state, and media permissions.
Redis mirrors room and peer presence:
room:{roomID}room:{roomID}:peerspeer:{socketId}
Redis is not the source of truth, and no TTL is currently applied to these keys.
Persistent classroom chat uses client-side AES encryption:
- plaintext is encrypted in the frontend before submission
- ciphertext is stored in MongoDB
- decryption happens in the frontend when messages are rendered
The effective key is derived from VITE_CHAT_SECRET and the classroom id.
This is not strong end-to-end encryption in the cryptographic sense because the frontend secret is bundled into the client application. It protects stored messages from being plain text on the server, but it does not protect against a client that can inspect the shipped bundle.
Cloudinary handles classroom resource storage.
- Client submits multipart form data.
- Multer with Cloudinary storage uploads the file.
- Backend stores the resulting secure URL in the
Resourcedocument.
Supported types include PDF, JPG, JPEG, PNG, and PPTX.
Downloads are proxied through the backend so authorization is enforced before the file is streamed to the client with an attachment filename.
Resource deletion also attempts Cloudinary cleanup, including a fallback retry using the asset path with extension when needed.
The frontend uses:
AuthContextfor auth state and socket lifecycle- local component state for feature data
useSFUfor Mediasoup client state and media orchestration
There is no Redux or Zustand layer.
Main route structure:
/redirects to/home/home/login/signup/dashboard/profile-setup/profile/classroom/:id/room/:roomId/logout/unauthorized
Protected routes validate the token with the backend and force logout if validation fails.
The frontend creates a shared Socket.IO client singleton with autoConnect: false. The socket connects when auth state gains a token and disconnects when auth is cleared.
An axios interceptor injects Authorization: Bearer <token> from local storage into outgoing API requests.
- Students are auto-profiled during signup.
- Teachers complete a separate profile setup flow after initial registration.
- Load room metadata.
- Acquire local media.
- Enter directly if host or returning participant, otherwise wait in lobby.
- On approval, join the SFU room.
- Load router RTP capabilities into the Mediasoup device.
- Create send transport and publish local tracks.
- Consume existing remote producers.
The classroom page loads classroom metadata and presents tabs for:
- stream
- resources
- announcements
- people
- chat
- Join the classroom Socket.IO room.
- Fetch encrypted history over REST.
- Decrypt locally for display.
- Encrypt outbound messages before POST.
- Receive real-time broadcasts and decrypt them client-side.
The backend requires the following configuration:
| Variable | Purpose |
|---|---|
ATLAS_URI |
MongoDB connection string |
JWT_SECRET |
JWT signing secret |
APP_PORT |
Server listen port |
REDIS_HOST |
Redis host |
REDIS_PORT |
Redis port |
FRONTEND_URL |
Frontend origin for CORS |
HTTPS_CERT_FILE |
Optional TLS cert path |
HTTPS_KEY_FILE |
Optional TLS key path |
CLOUDINARY_URL or CLOUDINARY_* |
Cloudinary credentials |
BASE_PORT |
Base Mediasoup RTC port |
PORT_RANGE_SIZE |
Port block size per worker |
ANNOUNCED_IP |
ICE candidate announced IP |
The frontend also depends on a classroom chat secret:
| Variable | Purpose |
|---|---|
VITE_CHAT_SECRET |
Base secret used in classroom message encryption |
This repo is organized as separate frontend and backend apps. At a minimum you need:
- Node.js
- MongoDB or MongoDB Atlas
- Redis
- Cloudinary credentials
- Optional TLS certs for local secure WebRTC testing
Typical development flow:
- Install dependencies for each app.
- Configure backend and frontend environment variables.
- Start Redis.
- Start the backend.
- Start the frontend Vite dev server.
If you plan to test WebRTC on non-localhost environments, configure HTTPS and ANNOUNCED_IP correctly.