Vaarta: The Language Exchange App That Outsources the Hard Parts

A clean look at how a niche social network uses MongoDB for identity and relationships, Stream for real-time chat and video, and onboarding rules to keep the match quality high.

8 min read • View on GitHub • More from HarshSharma225

A wide black-ink illustration of a language exchange marketplace split from a live communication relay. On one side are profile cards and friend requests arranged like files in a cabinet, while the other side shows a token flowing into a chat and video switchboard. It explains that Vaarta keeps identity and matching local, then hands real-time communication to a specialized service.
Vaarta’s core move is separation. MongoDB holds the social graph, while Stream handles the live layer.
Key Takeaways

Why Vaarta Feels Smaller Than It Is

Vaarta looks like a chat app, but that is only the surface. The interesting choice is architectural: it keeps durable product state in MongoDB and pushes real-time chat and video into Stream. That makes the repo feel more focused than a full-stack communication product usually does.

A real-time chat application built with React, Node.js, and Socket.io.

Harsh Sharma, Project Creator and Maintainer · HarshSharma225/Vaarta on GitHub

The Product Is Really a Matching Engine

Vaarta is not trying to be Slack or Discord. It is closer to a language-exchange filter with messaging attached. Native language, learning language, and onboarding state shape who shows up, which means the social graph is already opinionated before anyone sends a message.

Vaarta separates access, product state, and live transport into three layers. The app only opens the real-time door after the local session and onboarding gates are satisfied.

LayerWhat Vaarta controlsWhat it avoids owning
Identity and discoveryMongoDB users, onboarding, friend requests, recommendation filteringGeneric chat infrastructure
Live communicationAuthentication and capability issuanceWebSocket plumbing, call signaling, media transport
Product surfaceLanguage matching and relationship rulesTrying to compete as a broad social network

How the App Decides Who You Can Meet

The routing logic is strict for a reason. If a user is not authenticated, they do not get into the app. If they are authenticated but not onboarded, they are pushed into onboarding instead of a half-finished social feed. That keeps the network from filling with empty profiles and weak matches.

The recommendation query reinforces the same idea. Vaarta filters out the current user, existing friends, and anyone who has not finished onboarding. In other words, the app is not optimizing for volume. It is optimizing for relevance.

SignalEffect on discoveryWhy it matters
isOnboardedOnly complete profiles can appearKeeps the feed from becoming noisy
nativeLanguage + learningLanguageShapes who is a good matchTurns profile data into product logic
existing friends exclusionPrevents duplicate recommendationsKeeps the graph clean
authenticated sessionBlocks anonymous access to core featuresProtects the social layer

What Stream Takes Off Vaarta’s Plate

This is the repo’s biggest leverage point. The backend does not try to become a communications engine. It validates the user, issues a Stream token, and gets out of the way. The frontend waits for that token, then initializes Stream’s chat and video SDKs.

That matters because real-time systems are expensive in hidden ways. Presence, retries, message ordering, call setup, and media transport all add complexity fast. Vaarta avoids that tax by treating live communication as a capability it can broker, not an infrastructure layer it has to own.

A close editorial illustration of a sealed token sliding from a backend ledger into a chat client and video lane. The scene shows the token arriving before the chat and call systems light up. It explains how Vaarta gates Stream initialization behind backend-issued access rather than starting the live layer first.
The app’s live layer only wakes up after the token arrives. That is the clean handoff between local state and outsourced real-time services.
const { data: token } = useQuery({
  queryKey: ['streamToken'],
  queryFn: async () => {
    const res = await axiosInstance.get('/chat/token');
    return res.data.token;
  },
  enabled: !!authUser,
});

if (!token) return null;

const chatClient = StreamChat.getInstance(STREAM_API_KEY);
await chatClient.connectUser(currentUser, token);

Why the Chat-to-Call Flow Is Clever

Vaarta does not treat a video call like a separate event that floats outside the conversation. It writes the call invite into the chat itself. That turns the call into a durable artifact, which is much better than forcing both users to remember what happened in another window.

That design also makes the product feel more asynchronous. If the other person is not ready, the invite still lives in the thread. The message history becomes the handoff point, not just a log of old text.

The Friend Request Model Keeps the Social Graph Clean

The repo uses a dedicated FriendRequest collection instead of hiding pending relationships inside user arrays. That is the right move. Pending and accepted are different states, and they deserve a separate model with a clear lifecycle.

When a request is accepted, Vaarta uses atomic updates to add each user to the other’s friends list without duplication. It is a small implementation detail, but it shows discipline. The graph stays consistent because the write path is explicit.

ApproachStrengthWeakness
FriendRequest collectionClear pending and accepted statesMore moving parts in the data model
Embedded pending arraysFewer collectionsHarder to reason about lifecycle and duplicates
Atomic $addToSet updatesPrevents duplicate friendshipsRequires careful backend discipline

What Vaarta Gets Right, and What Will Break First

Vaarta’s strongest trait is focus. It knows what kind of product it is, and it keeps the code aligned with that answer. The onboarding gate, recommendation query, and Stream handoff all reinforce the same thesis: this is a language exchange app first, a chat app second.

The obvious trade-off is dependency. Stream buys speed and simplicity, but it also creates vendor lock-in and cost sensitivity. The other likely pressure point is scale. The matching query will need pagination and more explicit performance work as the graph grows.

ChoiceWinsTrade-off
Use Stream for live featuresFaster shipping, less protocol workVendor dependence and cost
Keep MongoDB for identitySimple ownership of product stateBackend will need stronger scaling patterns later
Restrict access with onboardingBetter match qualityLess casual discovery