Real-time features are one of the most-rebuilt things in software. Every team building a chat product starts with the same blank canvas, makes the same decisions about how to structure rooms, how to track presence, how to handle typing indicators, how to replay missed messages, and how to manage reconnection state. Then they spend two to four weeks wiring it together before they can show a product demo.
This is the build vs buy problem for real-time infrastructure. Raw pub/sub gives you complete flexibility at the cost of building everything yourself. Blueprint SDKs give you a domain-specific API that handles the common patterns, so you can ship a working feature on day one.
What Every Real-Time App Rebuilds
Before getting into the comparison, it is worth being specific about what "wiring it yourself" actually means. Here is the list of concerns that every chat application, every notification feed, and every live dashboard rebuilds from scratch:
- Room management. Creating rooms, joining and leaving them, listing members, enforcing membership rules.
- Presence tracking. Who is online, detecting disconnects, heartbeat timeouts, multi-tab handling, custom status metadata.
- Typing indicators. Starting and stopping a typing state, broadcasting it to other room members, timing out stale typing states.
- Message replay. Tracking sequence numbers, requesting missed messages on reconnect, rendering replayed vs live messages differently.
- Reconnection state. Detecting drops, exponential backoff, re-subscribing to channels, re-joining rooms, flushing any locally queued messages.
- Unread counts. Tracking which messages each user has seen, computing unread badges, marking conversations as read.
- Message history. Paginating through past messages, loading older messages on scroll, merging history with the live stream.
Each of these is a real engineering problem with edge cases. Typing indicators need a timeout: if a user starts typing and then closes their tab, the "Alice is typing" indicator should not stick around forever. Presence needs reference counting for multi-tab users. Reconnection needs to re-join rooms, not just re-open the socket. None of this is difficult individually, but together it is weeks of work before your core feature is even started.
The Raw Pub/Sub Approach
Here is roughly what it takes to wire a minimal chat room with raw pub/sub. No history, no unread counts, just connecting, joining a room, sending messages, and showing typing indicators.
import { NoLag } from '@nolag/js-sdk'
const client = NoLag(token)
let typingTimeout: ReturnType<typeof setTimeout> | null = null
// Subscribe to the room's message channel
await client.subscribe(`chat.room.${roomId}`, {
onMessage: (msg) => {
if (msg.payload.type === 'message') renderMessage(msg.payload)
if (msg.payload.type === 'typing_start') showTypingIndicator(msg.payload.userId)
if (msg.payload.type === 'typing_stop') hideTypingIndicator(msg.payload.userId)
}
})
// Subscribe to presence
await client.subscribe(`chat.presence.${roomId}`, {
onMessage: (msg) => updatePresence(msg.payload)
})
// Send a message
async function sendMessage(text: string) {
await client.publish(`chat.room.${roomId}`, {
payload: { type: 'message', userId, text, timestamp: Date.now() }
})
}
// Track typing with debounce
function onInputChange() {
client.publish(`chat.room.${roomId}`, {
payload: { type: 'typing_start', userId }
})
if (typingTimeout) clearTimeout(typingTimeout)
typingTimeout = setTimeout(() => {
client.publish(`chat.room.${roomId}`, {
payload: { type: 'typing_stop', userId }
})
}, 3000)
}
// On connect, publish presence join
async function joinRoom() {
await client.publish(`chat.presence.${roomId}`, {
payload: { type: 'join', userId, displayName, timestamp: Date.now() }
})
}
// On disconnect, need to publish presence leave
// But ungraceful disconnects won't fire this...
// You need server-side heartbeat logic to handle that case.
async function leaveRoom() {
await client.publish(`chat.presence.${roomId}`, {
payload: { type: 'leave', userId, timestamp: Date.now() }
})
}That is already over 50 lines and it is missing: reconnection re-join logic, multi-tab presence deduplication, heartbeat-based disconnect detection, message history loading, unread count tracking, and sequence-based replay. Each of those adds another chunk of code and another set of edge cases to manage.
The Blueprint SDK Approach
A Blueprint SDK wraps the pub/sub infrastructure with a domain-specific API designed for a particular use case. Here is the same chat room with the @nolag/chat Blueprint:
import { NoLagChat } from '@nolag/chat'
const chat = new NoLagChat(token, { appName: 'my-chat-app' })
// Join a room. Presence, history, and replay are automatic.
const room = await chat.joinRoom(roomId, {
user: { userId, displayName },
onMessage: (msg) => renderMessage(msg),
onPresenceChange: (presence) => updateMemberList(presence),
onTypingChange: (typing) => updateTypingIndicator(typing),
})
// Send a message
await room.sendMessage({ text: 'Hello!' })
// Start typing indicator (auto-stops after inactivity)
room.startTyping()
// Leave the room (presence updates automatically)
await room.leave() Ten lines of application code. Presence, typing indicators, message replay on reconnect, and history loading are all handled by the Blueprint. The onMessage handler receives messages in order, with the isReplay flag already handled correctly. Reconnection re-joins the room automatically. Typing states time out on their own.
What a Blueprint Includes
A Blueprint is not just a client SDK. It is a complete package with three parts:
A purpose-built SDK. The client library with the domain-specific API, covering the common patterns for that use case. For chat: joinRoom, sendMessage, startTyping, getHistory. For notifications: subscribe, markAsRead, getUnreadCount. For dashboards: watch, unwatch, setFilters.
Infrastructure configuration. The topic structure, room settings, QoS levels, retention windows, and lobby configuration that the SDK expects. This is pre-configured and deployed to your NoLag project when you install the Blueprint.
A working demo. A runnable example showing the Blueprint in action, with a UI you can use as a starting point or just as a reference.
When to Use Raw Pub/Sub vs a Blueprint
Blueprints are the right choice when your use case matches one of the available templates. Chat, notifications, live dashboards, and collaborative features are all well-understood problem spaces with settled patterns. Using a Blueprint means you get a working implementation of those patterns on day one, with the edge cases already handled.
Raw pub/sub is the right choice when your use case is genuinely novel or when you need full control over the message structure, topic design, and delivery semantics. A real-time multiplayer game with custom synchronization logic, a live auction platform with specific bid sequencing requirements, or a specialized IoT telemetry pipeline all benefit from starting with raw pub/sub and building exactly what they need.
The two are not mutually exclusive. You can use the @nolag/chat Blueprint for your chat feature while using raw pub/sub for a custom activity feed with unusual filtering requirements. Blueprints sit on top of the same infrastructure, so they compose with raw pub/sub subscriptions in the same application without any conflicts.
The question to ask is: am I building something that has been built many times before, or something genuinely new? If the answer is "I need a chat feature", use the Blueprint and spend your engineering time on the parts of your product that are actually differentiated. If the answer is "I need a message delivery system with custom routing logic tied to our domain model", reach for raw pub/sub.