
"We need a chat feature." Five words that sound simple but hide a massive engineering project underneath. We hear this request regularly from clients who are building platforms where users need to communicate. And every time, our answer is the same: we don't build chat from scratch. Here's why.
A client comes to us with a platform idea. Users need to message each other. Maybe it's artists talking to managers, editors talking to sound designers, or customers talking to support. The feature request fits in one sentence: "Add a chat."
From the outside, it looks straightforward. A text input, a send button, messages appearing on screen. How hard can it be?
Very hard. Real-time messaging is one of those features that looks like 10% of the work but turns into 60% if you try to build it yourself. Here's what's hiding behind that simple chat bubble:
Companies like Slack, WhatsApp, and Discord have entire engineering teams dedicated to messaging infrastructure. That's their core product. When chat is just one feature in your app, spending months building and maintaining this infrastructure doesn't make sense.
Services like Stream, Sendbird, and PubNub exist specifically to solve this problem. They provide battle-tested messaging infrastructure through APIs and SDKs. You get years of engineering in a single integration.
What a managed service gives you out of the box:
The cost? A predictable monthly fee based on usage, instead of months of custom development and ongoing maintenance.
We recently built The Artist Suite, a platform connecting music artists with industry professionals. One of the core requirements was direct messaging between users. Artists needed to talk to managers, producers, and collaborators directly inside the app.
The entire chat feature was shipped in one sprint (two weeks). Here's how it works:
Backend (3 endpoints, ~100 lines of code):
The Django backend handles authentication and channel management. Stream never sees your users' passwords. The backend generates short-lived tokens and manages channel creation.
class TokenView(APIView):
"""Generate a Stream Chat token for the authenticated user."""
def post(self, request):
client = StreamChat(
api_key=settings.STREAM_API_KEY,
api_secret=settings.STREAM_API_SECRET,
)
# Upsert user info to Stream
client.upsert_user({"id": str(request.user.id), "name": request.user.name})
token = client.create_token(str(request.user.id))
return Response({"token": token, "api_key": settings.STREAM_API_KEY})
class CreateChannelView(APIView):
"""Create or get a messaging channel between two users."""
def post(self, request):
other_user_id = request.data["user_id"]
# Deterministic channel ID from sorted user IDs
members = sorted([str(request.user.id), str(other_user_id)])
channel_id = f"chat-{members[0]}-{members[1]}"
client = StreamChat(
api_key=settings.STREAM_API_KEY,
api_secret=settings.STREAM_API_SECRET,
)
channel = client.channel("messaging", channel_id, {"members": members})
channel.create(str(request.user.id))
return Response({"channel_id": channel_id})
chat-{user1}-{user2} with sorted IDs) means you always get the same channel for the same pair of users, no matter who initiates the conversation.Frontend (Vue composable + component):
The frontend connects to Stream using the token from the backend. All real-time updates, message rendering, and state management happen through Stream's JavaScript SDK.
export async function inboxGetToken(): Promise<{ token: string; api_key: string }> {
return await $api(endpoints.token, { method: "POST" })
}
export async function inboxCreateChannel(userId: string): Promise<{ channel_id: string }> {
return await $api(endpoints.channel, {
method: "POST",
body: { user_id: userId },
})
}
The chat component listens for message.new and message.read events from Stream, updates the UI in real time, and tracks unread counts per conversation.
No database models needed. Stream stores all messages, handles delivery, and manages state. The Django backend has zero chat-related database tables.
This build-vs-buy decision is clear-cut in most cases:
Use a managed service when:
Consider building custom when:
For most platforms we build at MusicTech Lab, the choice is obvious. Our clients' budgets should go toward the features that set their product apart, not toward reinventing infrastructure that already exists.
Chat is one of those features where the gap between "looks simple" and "is simple" is enormous. A managed service like Stream closes that gap. You get a production-grade messaging system integrated in days, not months.
We've used this approach across multiple projects, and it works every time. The client gets real-time messaging that just works. We get to spend our time on the features that actually matter for their business.
Planning an app with real-time features like chat, notifications, or live updates? Let's talk about the right building blocks for your project.
Building something similar or facing technical challenges? We've been there.
Let's talk — no sales pitch, just honest engineering advice.
Why the Programming World Loves Python
Why Python is one of the most popular programming languages. Its flexibility spans backend, AI, web development, and automation with an easy learning curve.
Why we use Sanity.io
Sanity.io has a number of advantages over its alternatives, such as GraphCMS, Storyblok, Contentful, etc. Find out what they are...
Technical Partner
Technical partner at MusicTech Lab with 15+ years in software development. Builder, problem solver, blues guitarist, long-distance swimmer, and cyclist.
Get music tech insights, case studies, and industry news delivered to your inbox.