This README documents the Express backend for SubjectSwap (currently deployed on:
https://subjectswap-frontend.onrender.com). It explains API endpoints grouped by category, data models, socket/live-chat design (including encrypted tunneling), folder structure, and notable design decisions.
For general evaluation repositoryhttps://github.com/SubjectSwap/SubjectSwap/
For frontend repositoryhttps://github.com/SubjectSwap/FRONTEND/
- Quick overview
- API endpoints (by category)
- Socket / Live chat (encrypted tunneling)
- Data models & storage patterns
- Matchmaking algorithm (how matches are made)
- Account creation, login, and caches
- Denormalization & soft-delete patterns
- Folder structure & key files (roles)
- Design decisions (summary)
- How to run?
- References in Repo
SubjectSwap backend:
- NodeJS + Express + MongoDB.
- Uses JWT for auth and for protected routes (cookie + token-in-body patterns across endpoints).
- Stores chat conversations as documents that each hold up to 1000 messages (so a logical conversation is represented by a chain of
Conversationdocuments — new doc after every 1000 messages). This reduces write amplification and makes conversation objects indexable/fast to read.
-
POST
/create-accountRequest body:{ username, email, password }Creates a temporary registration entry and sends a frontend powered verification link to the email (the link contains a UUID). -
POST
/verify-account/:uuidSent by the frontend page when accessed. Can work exactly once. Accepts the UUID from the email link; if found intempUsers, creates a persistentUserin MongoDB and deletes the temp entry. -
POST
/loginBody:{ email, password }Verifies password (bcrypt), returns user object (with sensitive properties removed) and a JWT token; sets cookieSubjectSwapLoginJWTwithhttpOnlyandSameSite=None(intended for cross-site flows). -
POST
/verify-userBody:{ token }— verifies a JWT and returns the user info. -
PUT
/edit-profileA protected route. Body must include a valid token (token is decoded to obtain user id). Supports updating username, languages, learningSubjects, and teachingSubjects, profile-pic update.
- POST
/matchmaking/matchBody:{ token, wantSubject, mySubjects }Runs an pipeline over users to compute a customized list of candidates and returns sorted matches. The algorithm is discussed later.
-
POST
/search/personBody:{ query }Uses MongoDB Atlas Search ($searchwith an autocomplete index) to return matching users (username autocomplete + score ordering). -
POST
/search/user/:uuidFetch detailed public profile for a user by id (only for active users).
-
POST
/chat/previous_chatsBody:{ token, to? }(token must be valid) — returns the conversation partner list (basic info) or conversation metadata for a chat pair. -
POST
/chat/get_user_infoBody:{ token, uuid }— fetches a user's public info for chat preview (id, username, profilePic).
-
POST
/rating_routes/personalityBody:{ token, to, rating }— rate a user's personality.tracks previous ratings by the rater and updates the target's aggregated personality rating accordingly. -
POST
/rating_routes/subjectBody:{ token, to, subjectName, rating }— rate another user's subject teaching ability. The endpoint checks whether the rater had already rated that(to, subjectName)pair (and updates totals accordingly);
Namespace: /private_chat (Socket.IO)
Authentication:
- Socket handshake requires
auth.token(the JWT). The server verifies the JWT and setssocket.user.
Main flow & events (high-level):
-
join_conversation{ to, publicKey }- The client asks to open/join a peer conversation. The server checks the
touser id validity, computes a deterministic room idusersString(sorted pair:smallerId_biggerId), callssocket.join(usersString). - If client sent a
publicKey, server stores it in an in-memoryuserPublicKeysmap for that session (so the server knows how to encrypt messages for that client).
- The client asks to open/join a peer conversation. The server checks the
-
previous_chats{ to }- The server fetches
Conversationdocuments forparticipantId = usersString. Because conversations are stored in chunks (max ~1000 messages per Conversation document), the server chooses the most recent one or merges the two most recent when needed and returns messages. - The server returns
server_public_key(RSA public key generated server-side), thearchivedflag, and thechats(messages). Messages are encrypted with the client's public key before sending.
- The server fetches
-
message_sent{ to, content, type, filedata? }- Decryption on server: The server first attempts to
privateDecryptthe incomingcontentusing the server private key (the client send text message encrypted using the server public key). If not encrypted, it will just take content as-is. - Files: if
type === 'file'andfiledatapresent, the file buffer is uploaded to Cloudinary and the stored URL becomesmessage.content. - Persistence: the server saves the message plain-text into the DB inside the appropriate
Conversationdocument. If the latestConversationdoc already has>= 1000messages (the chunk threshold), the server creates a newConversationdoc and starts an appended chunk. This is how chunking/archiving is implemented. - Outgoing encryption: Before emitting the message to the sender and receiver sockets, the server encrypts the message content with each recipient's public key (if available) using RSA OAEP and sends base64-encoded ciphertext. The server emits
message_receivedto the sender (withbyMe: true) and to the receiver(s) in the room (withbyMe: false).
- Decryption on server: The server first attempts to
-
Other events:
offline— leaves the room (keeps key until disconnect).disconnect— server removes the user's public key from the in-memory map.
Crypto & key handling details:
-
The server generates a server RSA key-pair at process start (
2048bits). -
Clients are expected to send their public RSA key to the server when joining a conversation.
-
For message sending:
- Client may encrypt with server public key → server decrypts to get plaintext to persist.
- Server re-encrypts message plaintext for each recipient with their public key and emits.
-
All ciphertexts are base64 encoded across the wire.
-
The server stores only plaintext in the DB (so stored chat content is in plain text in the DB—encryption is used for transport between server and client). The advantage is that persisted data is optimized for indexing and fast reads while the tunnel ensures the network transport between server and client is not cleartext for the active socket connections.
-
Schema highlights:
participantId: String— canonical pair id string likesmallerId_biggerId.messages: [{ type, timestamp, content, from, id }]— messages array (mixed types:'text' | 'file' | 'deleted').noOfMessages: Number— a useful counter.
-
Chunking: At runtime the server keeps messages in arrays; once a conversation record reaches ~1000 messages the writing code starts a new
Conversationdocument for the sameparticipantId. This reduces the number of writes per document and enables indexing/fast reads of the most recent chunk(s).
-
Important fields:
-
username,email,passwordHash,profilePicUrl,description,languages(array). -
teachingSubjects: [{ subjectVector, subjectName, selfRating, noOfRatings, totalReceivedRatings, active }]subjectVector— precomputed unit vector for the subject (fromconstants/vectorEmbeddings.js).active— boolean to indicate if the subject is currently enabled (ratings are retained even whenactiveis false).
-
learningSubjects: [String] -
personalityRating: { average, totalRatings }— aggregated personality rating values. Describes how fellow users rateda user's attitude. -
active: { type:Boolean, default:true }— user soft-delete / disable flag such that they can no longer be operated on all the while maintaining their legacy data. -
peopleIRated: [{ type: 'personality'|'subject', rating, to, subjectName? }]— denormalized local history of which users (and which subject of theirs) this user has rated. This array is used to implement idempotent rating updates and to rollback/take-back rating operations.
-
High-level idea: compute a totalScore per candidate user that combines:
- similarity between the requested subject vector and candidate teaching subject vectors (dot product with precomputed unit vectors),
- candidate’s self-rating and the community's ratings for that subject,
- penalties/rewards based on rating distribution,
- small multiplicative reward if the candidate learns subjects that intersect with
mySubjects.
Concrete steps (as implemented):
-
Input:
wantSubject— the subject the searching user wants (mapped to a unit vector).mySubjects— an array of user’s own subjects used for learning-subject overlap bonus.
-
Per candidate
teachingSubject(onlyactiveones with a validsubjectVectorof the same length):-
Compute dot product
dotScore = dot(subjectVector, wantVector)(this is done inside MongoDB aggregation using$reduceover the dimension). -
Compute
avg_ratingfor that teaching subject:avg_rating = totalReceivedRatings / noOfRatings(or0ifnoOfRatings === 0). -
Compute
baseScore = dotScore * ( selfRating/2 + avg_rating_if_exists ). -
Apply penalties/rewards:
- If
noOfRatings > 100andtotalReceivedRatings < 4→ penalty -4 (flags a poorly rated subject despite many ratings). - Else if
noOfRatings > 100andtotalReceivedRatings > 7→ reward +3 (trusted/consistently rated teachers). - If
noOfRatings > 0and|selfRating - avg_rating| > 5→ penalty -4 (selfRating wildly different from community rating).
- If
-
Sum per-subject contributions for a user to produce a partial score.
-
-
Add cross-subject bonus:
size(setIntersection(user.learningSubjects, mySubjects)) * 3— users who want to learn what you teach (mutual benefit) get a massive multiplication bonus.
-
Keep only users with
totalScore > 0, sort bytotalScoredescending, and return selected fields.
Implementation detail:
- All of the above is implemented inside a MongoDB aggregation pipeline (using
$match,$addFields,$map,$reduce,$let,$cond, etc.). Doing the math inside the DB reduces data transfer and leverages Atlas' aggregation optimization. Vector constants are stored inconstants/vectorEmbeddings.js(unit-normalized vectors).
-
When a user POSTs to
/create-account:- The server validates inputs and checks
tempUsers.checkMail(email)to ensure the same email isn't in an active temp session. - If OK, it hashes the password (bcrypt), generates a
uuid, stores{ username, email, passwordHash }in the in-memory cachetempUserskeyed by the uuid, and sends a verification email with link containing the uuid. After verification the/verify-account/:uuidendpoint moves the temp entry into the persistentUsercollection and deletes the temp cache entry.
- The server validates inputs and checks
-
cache/tempUsers.jsdefines a smallCacheclass that wraps aMapplus TTL semantics:- Methods:
set,get,has,delete,clearTimeout,checkMail. - Two instances exported:
tempUsersandpermanentUserswith different timeouts (configured inconstants/cronJobTimers.js). TheclearTimeout()method will iterate and remove expired entries — this is invoked regularly by a cron job. This wrapper makes it straightforward to later replace the backingMapwith Redis or another store with minimal changes.
- Methods:
- Index (
index.js) registers a cron schedule that callsclearUserCache()at a period derived fromconstants/cronJobTimers(the smallest configured interval).clearUserCache()simply callstempUsers.clearTimeout()so stale temp registrations get removed automatically.
-
Each
Userdocument containspeopleIRatedrecords: when a user rates another (personality or subject), an entry is appended (or updated) inpeopleIRated. This enables:- Efficient updates (to detect if a rater is changing a previous rating).
- Ability to traverse a user's outward rating activity if needed during deletion/cleanup or audits.
-
Rating update endpoints use
peopleIRatedto determine whether to increment aggregate counters or replace old ratings (adjusting sum totals accordingly).
-
Users and teaching subjects have
activebooleans:User.active(defaulttrue) indicates whether the user is "active". In many queries the code filters by{ active: true }so deactivated users stop showing up but historical records and references remain in place.teachingSubjects[].activeallows disabling a subject without removing its rating totals (so past ratings persist).
-
This design avoids deleting documents outright because users/comments/chats/ratings may refer to historical data and removing DB documents would complicate referential integrity. Instead, the system prefers traversing denormalized lists (e.g.,
peopleIRated) to update related aggregates and then setactive: falseto remove the entity from active lists while preserving the historical footprint.
(only top-level folders and files present in the repository)
/cache
tempUsers.js # in-memory Cache class (tempUsers, permanentUsers)
/constants
vectorEmbeddings.js # subject vector embeddings (normalized unit vectors)
cronJobTimers.js # configured timeouts (unregistered/registered users, minTime)
/errors
chat_related_errors.js # custom errors (e.g., ConversationUsersOverloaded)
incorrect_profilepic_file_type_error.js
/models
User.js # Mongoose User schema (teachingSubjects, peopleIRated, active flags)
Conversation.js # Mongoose Conversation schema (participantId, messages, noOfMessages)
/routes
auth.js # create-account, verify-account, login, verify-user, edit-profile
chat_routes.js # /previous_chats, /get_user_info
matchmaking.js # /match -> aggregation + ranking
search.js # /person (username search), /user/:uuid
rating_routes.js # rating endpoints (personality, subject, take-back)
/sockets
chats.js # Socket.IO namespace /private_chat, RSA key flow, message handling
/utils
conversationHelpers.js # helpers (getUserOrder, getFromBoolean, getActualSender)
encryptionHelpers.js # (deprecated xor helper)
sendEmail.js # nodemailer wrapper
clearCache.js # wrapper to clear caches used by cron
index.js # app bootstrap: express, cors, mongoose connect, start server, attach sockets, start cron
package.json
.env example
vercel.json
Most files referenced above appear in the repo and are the primary places to look for implementation details (models, routes, sockets, caches and constants). The code organizes heavy computation inside MongoDB aggregation pipelines and uses utilities for deterministic conversation ordering and crypto helpers.
-
Conversation chunking (1,000 message chunks) Each
Conversationdocument stores up to ~1000 messages before a new document is created. This reduces per-document write pressure and enables indexing/searching of the most recent chunk(s) while keeping archival chunks intact. Indexing theparticipantIdand returning / merging only the latest chunk(s) yields fast reads for active conversations. -
Transport encryption (end-to-end-ish) + server-side storage Messages are protected during transport by RSA-based encryption between clients and server: clients share public keys with server; the server provides its public key for client -> server encryption; server decrypts, persists plaintext, then encrypts for each recipient with their public key. This ensures on-the-wire confidentiality while enabling server-side features (indexing, search).
-
Denormalization for ratings Ratings are tracked both inside the
teachingSubjectsaggregated counters and insidepeopleIRatedentries on the rater document. This enables idempotent updates (change/undo ratings) and allows the system to traverse outward rating relationships when doing deletions or data cleanup. It favors read performance for ranking and search. -
Soft-delete (
active: false) Users and subject entries can be marked inactive to preserve historical references (messages, comments, ratings) while removing them from active matching/search results. -
Aggregation-based matchmaking Match scoring runs inside MongoDB aggregation to leverage the DB cluster’s compute and indexing (vector dot-product style calculation + rating heuristics), reducing data movement and making ranking scale better.
-
Cache abstraction
cache/tempUsers.jswraps aMapwith TTL semantics. Because it's a simple class wrapper, migrating the same small API to Redis later is straightforward — the rest of the code depends on the wrapper methods, not the underlying implementation. -
Minimal persistent writes for chat By grouping messages into chunks, writes happen fewer times per conversation lifecycle, improving DB write amplification and allowing efficient chunk-level archival or TTL strategies later.
-
Clone repo and install dependencies:
npm install -
Provide environment variables (see
.env.example). You would have to request us for actual keys. -
Start:
npm run dev # uses nodemon in development npm start # production
- Socket + encryption:
sockets/chats.js. - Conversation chunking & schema:
models/Conversation.js. - Matchmaking aggregation + scoring:
routes/matchmaking.js. - Vector definitions:
constants/vectorEmbeddings.js) - Auth + create-account + verify flow:
routes/auth.js. - Temp cache & TTL:
cache/tempUsers.jsand cron scheduling inindex.js. - User model & denormalization:
models/User.jsand rating updates inroutes/rating_routes.js.