---
title: "Blueprint SDKs vs Raw Pub/Sub: Ship in Minutes Instead of Weeks"
description: "Every real-time app rebuilds the same plumbing: rooms, presence, typing indicators, replay, reconnection state, unread counts. Blueprint SDKs give you a domain-specific API so you can skip the wiring entirely."
excerpt: "Every real-time app rebuilds the same plumbing: rooms, presence, typing, replay. Blueprint SDKs give you a domain specific API so you can skip the wiring."
date: '2026-02-24'
readTime: 6 min read
category: Product
---

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 catch a client up after a gap, 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.
- **Catch-up after a gap.** Deciding what a client that was away should see, and rendering it differently from live traffic.
- **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.

```typescript [TypeScript]
import { NoLag } from '@nolag/js-sdk'

const client = NoLag(token)
await client.connect()

// Create the app and the room first; the slug you get back has a random suffix
const room = client.setApp(APP_SLUG).setRoom(roomId)
let typingTimeout: ReturnType<typeof setTimeout> | null = null

// Subscribe to the room's message and typing topics
room.subscribe('messages')
room.on('messages', (msg: any) => renderMessage(msg))

room.subscribe('typing')
room.on('typing', (evt: any) => {
  if (evt.type === 'typing_start') showTypingIndicator(evt.userId)
  if (evt.type === 'typing_stop') hideTypingIndicator(evt.userId)
})

// Presence: announce ourselves in the room, watch everyone else.
// Presence events are client-level, so they arrive for every room this
// connection is in and you filter them yourself.
room.setPresence({ userId, displayName })
client.on('presence:join', (actor) => addMember(actor.actorTokenId, actor.presence))
client.on('presence:update', (actor) => updateMember(actor.actorTokenId, actor.presence))
client.on('presence:leave', (actor) => removeMember(actor.actorTokenId))

// Send a message. Publishers never receive their own messages,
// so render it locally as well.
function sendMessage(text: string) {
  const msg = { userId, displayName, text, timestamp: Date.now() }
  room.emit('messages', msg)
  renderMessage(msg)
}

// Track typing with debounce
function onInputChange() {
  room.emit('typing', { type: 'typing_start', userId }, { qos: 0 })
  if (typingTimeout) clearTimeout(typingTimeout)
  typingTimeout = setTimeout(() => {
    room.emit('typing', { type: 'typing_stop', userId }, { qos: 0 })
  }, 3000)
}

// The broker clears presence when the socket closes, but it does not
// restore room presence after a reconnect. You have to set it again.
client.on('reconnect', () => room.setPresence({ userId, displayName }))
```

That is already over 40 lines and it is missing: multi-tab presence deduplication, mapping actor ids to user profiles, the receiver-side timeout that clears a stale typing indicator, message history loading, unread count tracking, and any notion of which messages a reconnecting client missed. 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:

```typescript [TypeScript]
import { NoLag } from '@nolag/js-sdk'
import { NoLagChat } from '@nolag/chat'

const client = NoLag(token)
// appName is the suffixed slug returned when you created the app from the chat blueprint
const chat = new NoLagChat({ client, appName: APP_SLUG, username: displayName })
await client.connect()
await chat.ready()

// Join a room. Presence and typing indicators are automatic.
const room = chat.joinRoom(roomId)
room.on('message', (msg) => renderMessage(msg))
room.on('userJoined', () => updateMemberList(room.getUsers()))
room.on('userLeft', () => updateMemberList(room.getUsers()))
room.on('typing', ({ users }) => updateTypingIndicator(users))

// Send a message (returns an optimistic ChatMessage for your own UI)
room.sendMessage('Hello!')

// Start typing indicator (auto-stops after inactivity)
room.startTyping()

// Leave the room (presence updates automatically)
chat.leaveRoom(roomId)
```

About fifteen lines of application code. Presence, typing indicators, user identity, and unread counts are all handled by the Blueprint. The `message` handler receives other users' messages in order, and your own sends come back through `messageSent` with an optimistic status so you never have to special-case the self-echo rule. Reconnection re-joins the room and re-sets presence automatically. Typing states time out on their own.

One thing no blueprint does for you is catch-up after a gap. NoLag replays missed messages only to load-balanced worker groups (see [replay](/docs/concepts/replay)); a chat client that reconnects gets its subscriptions back and nothing else. The chat SDK keeps an in-memory cache of what it has seen, so a product that needs history across sessions stores it itself and reads it on join.

## 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`, `getMessages`. For notifications: `subscribe`, `markRead`, `markAllRead`. For dashboards: `joinPanel`, `leavePanel`, `setFilters`.

**Infrastructure configuration.** The rooms, topic structure, lobby, and retention settings that the SDK expects. Creating an app from the blueprint seeds all of it into your NoLag project, and the suffixed app slug you get back is the `appName` you hand to the SDK.

**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 attach to the same core client, 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.
