"Live classes and community pages are great, but imagine how much more engaging and collaborative the user experience would be with live chat. It would take things to a whole new level. Can we make it happen? Also, let's aim to roll it out in 4 weeks."
- Matt, our CEO
As a lean team of 4 engineers, including our CTO, we set out to build and integrate a realtime chat application with our existing codebase in just 4 weeks. We would spend 3 weeks working on development, and 1 week for testing.
However, building a realtime chat application comes with its own set of challenges.
The Problem
When users engage in online communication (texting or messaging), they typically do so through their devices, such as smartphones or computers, which act as clients in a client-server architecture.
However, these clients cannot communicate with each other directly . Instead, each client connects to a chat service, which receives messages from clients and routes them to the intended recipients.
Therefore, we are faced with the challenge of building a service that enables clients to communicate with each other in real-time.
Client-Server Communication Fundamentals
To better understand the challenges involved, let's briefly go over the fundamentals of client-server communication in the context of a chat application.
When a client wants to start a chat, it initiates a connection to the chat service using one or more network protocols. The most common protocol used for this purpose is HTTP (Hypertext Transfer Protocol). HTTP allows clients to establish connections and send requests to the server, making it suitable for the sender side of the chat application.
Using HTTP, the client can send a message to the receiver via the chat service. The client opens an HTTP connection with the chat service and sends the message, instructing the service to deliver it to the intended recipient.
However, the receiver side presents a more complex challenge. Since HTTP is a client-initiated protocol, it is not trivial to send messages from the server.
Exploring solutions
To enable the server to push messages to clients in real-time, we need to explore various solutions that simulate server-initiated connections. These solutions overcome the limitations of traditional HTTP communication and provide ways for the server to send messages to clients without relying on client-initiated requests.
Here we explore the potential solutions for building a realtime chat application:
-
Polling: Polling is a technique where clients periodically send requests to the server to check for new messages. The server responds with any available messages or an empty response if there are no new messages. While polling is simple to implement, it can be inefficient and introduce unnecessary latency, as clients need to continuously send requests even when there are no new messages.
-
Long Polling: Clients send requests to the server and wait for a response. The server keeps the request open until new data is available or a timeout occurs. Long polling simulates realtime communication by allowing the server to push messages to clients without the need for repeated requests. However, it may still introduce some latency and requires the server to maintain open connections.
-
WebSocket: A protocol that enables full-duplex communication over a single TCP connection. WebSocket allows clients and the server to send messages to each other independently, providing true realtime communication. It eliminates the need for polling and offers lower latency compared to other techniques.
-
Third-Party Services: Leveraging platforms like Pusher or Socket.IO for realtime communication can simplify the implementation process. These services provide APIs and libraries that abstract the underlying communication protocols, making it easier to integrate realtime features into our application. They also offer scalability and security out of the box.
-
Realtime Databases: Platforms like Firebase Realtime Database, Supabase Realtime, and Convex provide built-in realtime capabilities. These databases allow clients to subscribe to specific data channels and receive updates in realtime whenever data changes. They offer a seamless way to integrate realtime features, but may require careful data modeling and management to ensure efficient synchronization and avoid excessive data transfer.
Choosing the Right Solution
When selecting the appropriate solution for our realtime chat application, we considered several key factors:
-
Performance: We aimed to choose a solution that offers optimal performance, ensuring low latency and efficient message delivery. Minimizing delays and providing a seamless user experience were top priorities.
-
Integration: Integrating the chosen solution with our existing Next.js codebase was a critical factor. We assessed the compatibility and level of effort required to incorporate the solution seamlessly, considering factors such as library support, documentation, and community resources. Our goal was to find a solution that would work harmoniously with our technology stack and minimize integration challenges.
-
Development Speed: We evaluated solutions based on the speed of implementation and the overall developer experience. Given our time constraints, we sought a solution that could be quickly integrated into our existing codebase without extensive modifications, allowing us to rapidly develop and deploy our chat application.
-
JavaScript: We're JavaScript devs, of course we're gonna use a third-party solution. One does not simply build things from scratch in JavaScript... 😅
Polling & Long Polling
When evaluating the potential solutions, we quickly discarded short polling and long polling as viable options. Although these techniques can simulate realtime communication, they introduce unnecessary latency and overhead due to the need for repeated requests and open connections.
Web Sockets & Third Party Services
WebSocket emerged as a more efficient and performant alternative, providing true realtime communication with minimal latency. WebSocket enables full-duplex communication over a single TCP connection, allowing both the client and server to send messages independently. This bidirectional communication model aligns perfectly with the requirements of a realtime chat application.
Socket.io is arguably the most popular WebSocket wrapper for Node.js. However, we quickly realized that serverless functions on Vercel do not support WebSockets. This limitation is a significant drawback for those deploying their Next.js applications on Vercel.
To address this limitation, Vercel recommends using third-party solutions for real-time communication when deploying to their platform. They even provide a guideline for integrating Pusher, a well-known third-party service for real-time functionality.
Despite the potential workarounds, we decided against using WebSocket with Next.js due to the deployment limitations and the need for additional third-party services. We wanted a solution that would seamlessly integrate with our Next.js application and provide real-time capabilities without relying on external dependencies.
Realtime databases
This led us to explore alternative approaches, such as realtime databases, which offer built-in real-time functionality and can be easily integrated into our Next.js application without the need for separate WebSocket servers or third-party services.
By leveraging a realtime database, we can simplify our architecture and reduce the complexity of managing WebSocket connections and message routing. These databases provide a seamless way to store and synchronize data in realtime, allowing clients to subscribe to specific data channels and receive updates instantly.
Choosing the right database
We chose Convex for a variety of reasons, ranging from pricing to integration with our current tech stack (authentication, TypeScript support, etc).
While the main idea of this post is to approach this problem from a system design perspective rather than talk about the implementation of a specific technology, let's glance over some cool features that Convex offers!
The main idea behind Convex is to:
- Define a schema with TypeScript
- Create queries to fetch data from our DB
- Wrap your application in a Context Provider
- Use their useQuery hook to execute queries and retrieve data
What sets Convex apart is its realtime capabilities. As mentioned in their documentation:
"Convex's realtime database tracks all dependencies for every query function. Whenever any dependency changes, including any database rows, Convex reruns the query function and triggers an update to any active subscription. Convex automatically updates the query cache so that the next time you try the same query it'll return instantly."
Now let's go over why Convex was a really attractive option for us particularly:
-
Convex & Clerk: Convex integrates really well with our authentication provider, Clerk. They provide comprehensive documentation on setting up Convex & Clerk, making the process straightforward and easy. This integration enables us to access the context (ctx) on the server-side, similar to how we handle it with tRPC, allowing us to handle authentication directly in our queries instead of using
useAuth()
hooks on the frontend and passing it down as arguments. -
TypeScript Support: Convex offers excellent TypeScript support. By writing your Convex functions using the .ts extension, Convex can infer the types of your function arguments automatically. You can then use this on the frontend (see writing frontend code in TypeScript) to make your entire project type-safe.
-
Convex & Vercel: As we host and deploy our application on Vercel, it's really convenient that we can host and deploy Convex on Vercel as well.
The Convex team has done an outstanding job in providing clear and comprehensive documentation, making it easier for developers to get started and leverage the platform's features effectively.
Conclusion
In conclusion, when embarking on a project with tight deadlines and specific requirements, it's essential to carefully assess the system design, identify potential constraints, and select tools and technologies that align with your needs. By considering these factors and making informed decisions, you can overcome challenges and create applications that provide value to your users.
In our case, Convex proved to be the ideal choice for our realtime chat application. Its seamless integration with our existing technology stack, excellent TypeScript support, and comprehensive documentation allowed us to focus on building our application rather than struggling with complex setup and integration processes. We are grateful to the Convex team for creating a powerful and developer-friendly realtime database that played a crucial role in our success.