[{"data":1,"prerenderedAt":868},["ShallowReactive",2],{"blog:/blog/pubsub-pattern-explained":3},{"id":4,"title":5,"author":6,"body":7,"category":857,"date":858,"description":859,"excerpt":860,"extension":861,"meta":862,"navigation":99,"path":863,"readTime":864,"seo":865,"stem":866,"__hash__":867},"blog/blog/pubsub-pattern-explained.md","The Pub/Sub Pattern Explained: Topics, Rooms, and Message Routing","Henco Burger",{"type":8,"value":9,"toc":845},"minimark",[10,14,17,22,25,28,31,35,38,151,154,158,161,164,172,175,190,195,198,204,207,211,218,224,227,234,238,241,244,415,428,431,435,445,448,452,455,772,775,779,782,802,824,838,841],[11,12,13],"p",{},"Publish/subscribe is one of those patterns that sounds abstract until you have built a real-time feature and realised that direct connections do not scale. Once you understand it properly, you see it everywhere: chat apps, IoT platforms, financial data feeds, multiplayer games, and collaborative editing tools all rely on some form of pub/sub under the hood.",[11,15,16],{},"This post explains pub/sub from the ground up, covers the three key concepts (topics, rooms, and filters), and walks through how they work together in practice.",[18,19,21],"h2",{"id":20},"the-problem-with-direct-connections","The Problem with Direct Connections",[11,23,24],{},"Imagine a simple chat app. User A sends a message. Users B, C, and D need to see it. The naive approach: User A maintains a direct WebSocket connection to B, C, and D. User A sends the message directly to each of them.",[11,26,27],{},"This works in a demo. It falls apart at scale. If you have 1,000 users in a room, every sender needs 999 active connections. The number of connections grows as O(n²). At 10,000 concurrent users you need 100 million connections. Even ignoring the impossibility of that at the network level, the sender now has to manage connection state for every other user. What happens when User B disconnects? What if User C is on a slow connection and cannot keep up? What if User A wants to send to multiple rooms simultaneously?",[11,29,30],{},"Direct connections couple the sender to the receiver. Pub/sub breaks that coupling.",[18,32,34],{"id":33},"the-core-idea-decouple-publishers-from-subscribers","The Core Idea: Decouple Publishers from Subscribers",[11,36,37],{},"In a pub/sub system, neither side knows about the other. A publisher sends a message to a named channel. A subscriber declares interest in a named channel. The broker, the thing in the middle, takes messages from publishers and delivers them to interested subscribers.",[39,40,41],"code-tabs",{},[42,43,49],"pre",{"className":44,"code":45,"filename":46,"language":47,"meta":48,"style":48},"language-typescript shiki shiki-themes github-light github-dark","// Publisher: doesn't know who is listening\nbroker.publish('chat:general', { text: 'Hello!', from: 'alice' })\n\n// Subscriber: doesn't know who is publishing\nbroker.subscribe('chat:general', (message) => {\n  renderMessage(message)\n})\n","TypeScript","typescript","",[50,51,52,61,94,101,107,136,145],"code",{"__ignoreMap":48},[53,54,57],"span",{"class":55,"line":56},"line",1,[53,58,60],{"class":59},"sJ8bj","// Publisher: doesn't know who is listening\n",[53,62,64,68,72,75,79,82,85,88,91],{"class":55,"line":63},2,[53,65,67],{"class":66},"sVt8B","broker.",[53,69,71],{"class":70},"sScJk","publish",[53,73,74],{"class":66},"(",[53,76,78],{"class":77},"sZZnC","'chat:general'",[53,80,81],{"class":66},", { text: ",[53,83,84],{"class":77},"'Hello!'",[53,86,87],{"class":66},", from: ",[53,89,90],{"class":77},"'alice'",[53,92,93],{"class":66}," })\n",[53,95,97],{"class":55,"line":96},3,[53,98,100],{"emptyLinePlaceholder":99},true,"\n",[53,102,104],{"class":55,"line":103},4,[53,105,106],{"class":59},"// Subscriber: doesn't know who is publishing\n",[53,108,110,112,115,117,119,122,126,129,133],{"class":55,"line":109},5,[53,111,67],{"class":66},[53,113,114],{"class":70},"subscribe",[53,116,74],{"class":66},[53,118,78],{"class":77},[53,120,121],{"class":66},", (",[53,123,125],{"class":124},"s4XuR","message",[53,127,128],{"class":66},") ",[53,130,132],{"class":131},"szBVR","=>",[53,134,135],{"class":66}," {\n",[53,137,139,142],{"class":55,"line":138},6,[53,140,141],{"class":70},"  renderMessage",[53,143,144],{"class":66},"(message)\n",[53,146,148],{"class":55,"line":147},7,[53,149,150],{"class":66},"})\n",[11,152,153],{},"This decoupling has several important properties. Publishers can exist without any subscribers. Subscribers can join and leave without affecting publishers. The broker can fan out a single published message to thousands of subscribers without the publisher doing any extra work. Some brokers can also hold messages for subscribers that are temporarily offline, though which subscribers, and for how long, varies a lot between systems (NoLag does it only for load-balanced worker groups; more on that at the end).",[18,155,157],{"id":156},"topics-named-channels-for-message-routing","Topics: Named Channels for Message Routing",[11,159,160],{},"A topic is the fundamental routing unit in a pub/sub system. It is just a named channel. Publishers address messages to a topic. Subscribers declare interest in a topic. The broker routes messages from publishers to all current subscribers of that topic.",[11,162,163],{},"In many systems topics are free-form hierarchical strings. Common conventions:",[42,165,170],{"className":166,"code":168,"language":169,"meta":48},[167],"language-text","MQTT-style hierarchy with /\nsensors/floor-2/temp-sensor-4\nusers/alice/notifications\norders/eu-west/status\n\nColon-delimited (common in web systems)\nchat:general:messages\npresence:room-42\ngame:session-7f3a:events\n","text",[50,171,168],{"__ignoreMap":48},[11,173,174],{},"The topic string is what the broker uses to route messages. It has no inherent meaning to the broker, it is just a string it uses as a key. The meaning is in the convention your app establishes.",[11,176,177,178,181,182,185,186,189],{},"NoLag is stricter than that on purpose. A topic name is a single token (",[50,179,180],{},"[a-zA-Z0-9_:-]",", no ",[50,183,184],{},"/","), and the only hierarchy is the fixed one, ",[50,187,188],{},"app/room/topic",". The grouping that other systems leave to a naming convention is a first-class object here, the room, which is what the next section is about.",[191,192,194],"h3",{"id":193},"wildcards-and-patterns","Wildcards and Patterns",[11,196,197],{},"Many pub/sub systems support wildcard subscriptions. Instead of subscribing to one specific topic, you subscribe to a pattern that matches many topics.",[42,199,202],{"className":200,"code":201,"language":169,"meta":48},[167],"Subscribe to all sensor readings on floor 2\nsensors/floor-2/+          (MQTT single-level wildcard)\n\nSubscribe to all events for any session\ngame/+/events\n\nSubscribe to everything under orders\norders/#                   (MQTT multi-level wildcard)\n",[50,203,201],{"__ignoreMap":48},[11,205,206],{},"Wildcard subscriptions are powerful but they push more routing work onto the broker. Most production systems have limits on wildcard subscription depth or require them to be granted explicitly. NoLag does not expose them to clients at all: a subscription names one topic in one room, and \"everything in this context\" is expressed by subscribing to the room's topics, which the blueprint SDKs do for you.",[18,208,210],{"id":209},"rooms-grouping-topics-for-application-logic","Rooms: Grouping Topics for Application Logic",[11,212,213,214,217],{},"Topics are a low-level concept. They are just named channels. Rooms are a higher-level concept that groups related topics together around a shared context. In a chat app, a room is a channel that users join together. It contains a topic for messages, a topic for streamed replies, a topic for typing indicators, and possibly a topic for reactions. This is what the NoLag chat blueprint's ",[50,215,216],{},"general"," room looks like on the wire:",[42,219,222],{"className":220,"code":221,"language":169,"meta":48},[167],"All topics that belong to the \"general\" room (APP is the app slug)\nAPP/general/messages\nAPP/general/_stream\nAPP/general/_typing\n\nPresence is not a topic. It is a property of the room itself.\n",[50,223,221],{"__ignoreMap":48},[11,225,226],{},"When a user joins the \"general\" room, they subscribe to these topics. When they leave, they unsubscribe from all of them. The room is an abstraction that bundles a set of subscriptions and gives them a shared lifecycle, and it is also the unit that presence and access control attach to.",[11,228,229,230,233],{},"Without the room abstraction, the application would have to manage these topic subscriptions individually. With it, you call ",[50,231,232],{},"joinRoom('general')"," and the SDK handles the rest.",[18,235,237],{"id":236},"filters-narrowing-message-delivery","Filters: Narrowing Message Delivery",[11,239,240],{},"Even within a topic, not every subscriber always wants every message. A dashboard might be showing orders for a specific customer. A mobile app might only want notifications of certain severities. Filters let subscribers express finer-grained interest without creating a separate topic for every possible combination of criteria.",[11,242,243],{},"NoLag filters are routing keys, not predicates. The publisher tags each message with one key, the subscriber lists the keys it wants, and the broker routes on the key without ever looking inside the payload:",[39,245,246],{},[42,247,249],{"className":44,"code":248,"filename":46,"language":47,"meta":48,"style":48},"// Subscribe to orders, but only for customer 42\nroom.subscribe('orders', { filters: ['customer_42'] })\n\n// Subscribe to notifications, but only these severities (any of them)\nroom.subscribe('notifications', { filters: ['warning', 'error', 'critical'] })\n\nroom.on('orders', (order, meta) => renderOrder(order, meta.filter))\n\n// The publisher tags each message with its routing key\nroom.emit('orders', { orderId: 'o_1', total: 42 }, { filter: 'customer_42' })\nroom.emit('notifications', { text: 'Disk almost full' }, { filter: 'warning' })\n",[50,250,251,256,277,281,286,315,319,350,355,361,393],{"__ignoreMap":48},[53,252,253],{"class":55,"line":56},[53,254,255],{"class":59},"// Subscribe to orders, but only for customer 42\n",[53,257,258,261,263,265,268,271,274],{"class":55,"line":63},[53,259,260],{"class":66},"room.",[53,262,114],{"class":70},[53,264,74],{"class":66},[53,266,267],{"class":77},"'orders'",[53,269,270],{"class":66},", { filters: [",[53,272,273],{"class":77},"'customer_42'",[53,275,276],{"class":66},"] })\n",[53,278,279],{"class":55,"line":96},[53,280,100],{"emptyLinePlaceholder":99},[53,282,283],{"class":55,"line":103},[53,284,285],{"class":59},"// Subscribe to notifications, but only these severities (any of them)\n",[53,287,288,290,292,294,297,299,302,305,308,310,313],{"class":55,"line":109},[53,289,260],{"class":66},[53,291,114],{"class":70},[53,293,74],{"class":66},[53,295,296],{"class":77},"'notifications'",[53,298,270],{"class":66},[53,300,301],{"class":77},"'warning'",[53,303,304],{"class":66},", ",[53,306,307],{"class":77},"'error'",[53,309,304],{"class":66},[53,311,312],{"class":77},"'critical'",[53,314,276],{"class":66},[53,316,317],{"class":55,"line":138},[53,318,100],{"emptyLinePlaceholder":99},[53,320,321,323,326,328,330,332,335,337,340,342,344,347],{"class":55,"line":147},[53,322,260],{"class":66},[53,324,325],{"class":70},"on",[53,327,74],{"class":66},[53,329,267],{"class":77},[53,331,121],{"class":66},[53,333,334],{"class":124},"order",[53,336,304],{"class":66},[53,338,339],{"class":124},"meta",[53,341,128],{"class":66},[53,343,132],{"class":131},[53,345,346],{"class":70}," renderOrder",[53,348,349],{"class":66},"(order, meta.filter))\n",[53,351,353],{"class":55,"line":352},8,[53,354,100],{"emptyLinePlaceholder":99},[53,356,358],{"class":55,"line":357},9,[53,359,360],{"class":59},"// The publisher tags each message with its routing key\n",[53,362,364,366,369,371,373,376,379,382,386,389,391],{"class":55,"line":363},10,[53,365,260],{"class":66},[53,367,368],{"class":70},"emit",[53,370,74],{"class":66},[53,372,267],{"class":77},[53,374,375],{"class":66},", { orderId: ",[53,377,378],{"class":77},"'o_1'",[53,380,381],{"class":66},", total: ",[53,383,385],{"class":384},"sj4cs","42",[53,387,388],{"class":66}," }, { filter: ",[53,390,273],{"class":77},[53,392,93],{"class":66},[53,394,396,398,400,402,404,406,409,411,413],{"class":55,"line":395},11,[53,397,260],{"class":66},[53,399,368],{"class":70},[53,401,74],{"class":66},[53,403,296],{"class":77},[53,405,81],{"class":66},[53,407,408],{"class":77},"'Disk almost full'",[53,410,388],{"class":66},[53,412,301],{"class":77},[53,414,93],{"class":66},[11,416,417,418,421,422,427],{},"Two rules follow from \"keys, not predicates\". A subscription without filters is a wildcard: it receives every message on the topic, including the tagged ones, so filters narrow what a client asks for and are not a privacy boundary (that is what per-topic ACL is for). And a message published without a filter reaches only those wildcard subscribers. A subscription can carry up to 100 keys, and they can be swapped at any time with ",[50,419,420],{},"room.setFilters(topic, keys)"," without resubscribing. The ",[423,424,426],"a",{"href":425},"/docs/concepts/filters","filters guide"," has the details.",[11,429,430],{},"Filters can be applied at the broker (the broker only delivers matching messages, which saves bandwidth) or at the client (the client receives all messages but discards non-matching ones). Broker-side filtering is more efficient; routing on a key rather than a predicate is what lets NoLag do it without understanding your message schema. Client-side filtering is simpler to implement but wastes bandwidth on messages the client will ignore.",[18,432,434],{"id":433},"how-pubsub-scales-vs-direct-connections","How Pub/Sub Scales vs Direct Connections",[11,436,437,438,441,442,444],{},"Going back to the 1,000-user chat room: with pub/sub, every user has exactly one connection, to the broker. When User A publishes to the ",[50,439,440],{},"messages"," topic in ",[50,443,216],{},", the broker fans the message out to the 999 other subscribers (never back to User A, who already has the message). User A does not know how many subscribers there are. It does not maintain a connection to any of them. The connection count grows as O(n), not O(n²).",[11,446,447],{},"The broker can also be distributed. Multiple broker nodes can share subscription state, so a message published to one node gets delivered by all nodes. This is how systems like Kafka, Redis Pub/Sub, and NATS scale horizontally. The application logic does not change when you add broker nodes. The routing layer absorbs the scale.",[18,449,451],{"id":450},"using-pubsub-in-practice-the-chat-app","Using Pub/Sub in Practice: The Chat App",[11,453,454],{},"Here is how all three concepts (topics, rooms, filters) work together in a real chat app with multiple channels:",[39,456,457],{},[42,458,460],{"className":44,"code":459,"filename":46,"language":47,"meta":48,"style":48},"import { NoLag } from '@nolag/js-sdk'\nimport { NoLagChat } from '@nolag/chat'\n\n// APP_SLUG is the slug returned when you created the app from the chat\n// blueprint (it has a random suffix); the blueprint seeds general, random, help and dev.\nconst client = NoLag(ACTOR_TOKEN)\nconst chat = new NoLagChat({ client, appName: APP_SLUG, username: 'alice' })\nawait client.connect()\nawait chat.ready()\n\n// User joins two rooms (synchronous: the subscriptions go out, the handles come back)\nconst general = chat.joinRoom('general')\nconst dev = chat.joinRoom('dev')\n\n// Under the hood, each joinRoom subscribes to that room's topics:\n// APP_SLUG/general/messages, APP_SLUG/general/_stream, APP_SLUG/general/_typing\n// APP_SLUG/dev/messages, ...\n\n// A message published to general's messages topic only reaches\n// subscribers of that specific topic. Users in #dev\n// do not receive messages from #general.\n\n// User sends a message: published to APP_SLUG/general/messages.\n// The sender gets it back as the returned (optimistic) message, never from the broker.\ngeneral.sendMessage('Morning everyone')\n\n// User receives only messages from rooms they have joined\ngeneral.on('message', (msg) => appendToGeneralFeed(msg))\ndev.on('message', (msg) => appendToDevFeed(msg))\n",[50,461,462,476,488,492,497,502,524,552,566,578,582,587,609,630,635,641,647,653,658,664,670,676,681,687,693,709,714,720,747],{"__ignoreMap":48},[53,463,464,467,470,473],{"class":55,"line":56},[53,465,466],{"class":131},"import",[53,468,469],{"class":66}," { NoLag } ",[53,471,472],{"class":131},"from",[53,474,475],{"class":77}," '@nolag/js-sdk'\n",[53,477,478,480,483,485],{"class":55,"line":63},[53,479,466],{"class":131},[53,481,482],{"class":66}," { NoLagChat } ",[53,484,472],{"class":131},[53,486,487],{"class":77}," '@nolag/chat'\n",[53,489,490],{"class":55,"line":96},[53,491,100],{"emptyLinePlaceholder":99},[53,493,494],{"class":55,"line":103},[53,495,496],{"class":59},"// APP_SLUG is the slug returned when you created the app from the chat\n",[53,498,499],{"class":55,"line":109},[53,500,501],{"class":59},"// blueprint (it has a random suffix); the blueprint seeds general, random, help and dev.\n",[53,503,504,507,510,513,516,518,521],{"class":55,"line":138},[53,505,506],{"class":131},"const",[53,508,509],{"class":384}," client",[53,511,512],{"class":131}," =",[53,514,515],{"class":70}," NoLag",[53,517,74],{"class":66},[53,519,520],{"class":384},"ACTOR_TOKEN",[53,522,523],{"class":66},")\n",[53,525,526,528,531,533,536,539,542,545,548,550],{"class":55,"line":147},[53,527,506],{"class":131},[53,529,530],{"class":384}," chat",[53,532,512],{"class":131},[53,534,535],{"class":131}," new",[53,537,538],{"class":70}," NoLagChat",[53,540,541],{"class":66},"({ client, appName: ",[53,543,544],{"class":384},"APP_SLUG",[53,546,547],{"class":66},", username: ",[53,549,90],{"class":77},[53,551,93],{"class":66},[53,553,554,557,560,563],{"class":55,"line":352},[53,555,556],{"class":131},"await",[53,558,559],{"class":66}," client.",[53,561,562],{"class":70},"connect",[53,564,565],{"class":66},"()\n",[53,567,568,570,573,576],{"class":55,"line":357},[53,569,556],{"class":131},[53,571,572],{"class":66}," chat.",[53,574,575],{"class":70},"ready",[53,577,565],{"class":66},[53,579,580],{"class":55,"line":363},[53,581,100],{"emptyLinePlaceholder":99},[53,583,584],{"class":55,"line":395},[53,585,586],{"class":59},"// User joins two rooms (synchronous: the subscriptions go out, the handles come back)\n",[53,588,590,592,595,597,599,602,604,607],{"class":55,"line":589},12,[53,591,506],{"class":131},[53,593,594],{"class":384}," general",[53,596,512],{"class":131},[53,598,572],{"class":66},[53,600,601],{"class":70},"joinRoom",[53,603,74],{"class":66},[53,605,606],{"class":77},"'general'",[53,608,523],{"class":66},[53,610,612,614,617,619,621,623,625,628],{"class":55,"line":611},13,[53,613,506],{"class":131},[53,615,616],{"class":384}," dev",[53,618,512],{"class":131},[53,620,572],{"class":66},[53,622,601],{"class":70},[53,624,74],{"class":66},[53,626,627],{"class":77},"'dev'",[53,629,523],{"class":66},[53,631,633],{"class":55,"line":632},14,[53,634,100],{"emptyLinePlaceholder":99},[53,636,638],{"class":55,"line":637},15,[53,639,640],{"class":59},"// Under the hood, each joinRoom subscribes to that room's topics:\n",[53,642,644],{"class":55,"line":643},16,[53,645,646],{"class":59},"// APP_SLUG/general/messages, APP_SLUG/general/_stream, APP_SLUG/general/_typing\n",[53,648,650],{"class":55,"line":649},17,[53,651,652],{"class":59},"// APP_SLUG/dev/messages, ...\n",[53,654,656],{"class":55,"line":655},18,[53,657,100],{"emptyLinePlaceholder":99},[53,659,661],{"class":55,"line":660},19,[53,662,663],{"class":59},"// A message published to general's messages topic only reaches\n",[53,665,667],{"class":55,"line":666},20,[53,668,669],{"class":59},"// subscribers of that specific topic. Users in #dev\n",[53,671,673],{"class":55,"line":672},21,[53,674,675],{"class":59},"// do not receive messages from #general.\n",[53,677,679],{"class":55,"line":678},22,[53,680,100],{"emptyLinePlaceholder":99},[53,682,684],{"class":55,"line":683},23,[53,685,686],{"class":59},"// User sends a message: published to APP_SLUG/general/messages.\n",[53,688,690],{"class":55,"line":689},24,[53,691,692],{"class":59},"// The sender gets it back as the returned (optimistic) message, never from the broker.\n",[53,694,696,699,702,704,707],{"class":55,"line":695},25,[53,697,698],{"class":66},"general.",[53,700,701],{"class":70},"sendMessage",[53,703,74],{"class":66},[53,705,706],{"class":77},"'Morning everyone'",[53,708,523],{"class":66},[53,710,712],{"class":55,"line":711},26,[53,713,100],{"emptyLinePlaceholder":99},[53,715,717],{"class":55,"line":716},27,[53,718,719],{"class":59},"// User receives only messages from rooms they have joined\n",[53,721,723,725,727,729,732,734,737,739,741,744],{"class":55,"line":722},28,[53,724,698],{"class":66},[53,726,325],{"class":70},[53,728,74],{"class":66},[53,730,731],{"class":77},"'message'",[53,733,121],{"class":66},[53,735,736],{"class":124},"msg",[53,738,128],{"class":66},[53,740,132],{"class":131},[53,742,743],{"class":70}," appendToGeneralFeed",[53,745,746],{"class":66},"(msg))\n",[53,748,750,753,755,757,759,761,763,765,767,770],{"class":55,"line":749},29,[53,751,752],{"class":66},"dev.",[53,754,325],{"class":70},[53,756,74],{"class":66},[53,758,731],{"class":77},[53,760,121],{"class":66},[53,762,736],{"class":124},[53,764,128],{"class":66},[53,766,132],{"class":131},[53,768,769],{"class":70}," appendToDevFeed",[53,771,746],{"class":66},[11,773,774],{},"This is the pattern at the core of every real-time communication app. The room abstraction makes it ergonomic. The topic routing underneath makes it scalable. The broker in the middle makes it decoupled.",[18,776,778],{"id":777},"nolags-three-noun-model-app-room-topic","NoLag's Three-Noun Model: App, Room, Topic",[11,780,781],{},"NoLag organises all of this around three nouns.",[11,783,784,785,789,790,793,794,797,798,801],{},"An ",[786,787,788],"strong",{},"app"," is the top-level namespace. It maps to your application, and all rooms and topics belong to it. Isolation between apps is complete: no topic in one app is visible to another. Credentials are not per app: API keys and actor tokens are issued at the project level, and an actor is granted access per app, room and topic. Every app slug gets a random suffix when it is created (",[50,791,792],{},"chat"," becomes ",[50,795,796],{},"chat-a3f9","), and that suffixed slug is what a client passes to ",[50,799,800],{},"setApp()",".",[11,803,804,805,808,809,812,813,816,817,820,821,801],{},"A ",[786,806,807],{},"room"," is a grouping context within an app. It corresponds to the room abstraction described above: a conversation channel, a game session, a document, a project. Rooms have their own presence list and their own topics, and access is granted per topic. A client subscribes to the topics it wants within a room; the fluent ",[50,810,811],{},"client.setApp(slug).setRoom(slug)"," context just supplies the prefix. Rooms never exist implicitly: create them in the control plane (with ",[50,814,815],{},"autoProvisionRooms"," enabled on the app, one ",[50,818,819],{},"rooms/ensure"," call does it), because subscribing or publishing to an unknown room is rejected with ",[50,822,823],{},"unknown_topic",[11,825,804,826,829,830,833,834,801],{},[786,827,828],{},"topic"," is the actual message channel within a room. Each room contains multiple topics for different event types, declared on the app. When you call ",[50,831,832],{},"joinRoom()"," on a blueprint SDK, it subscribes to the topics that blueprint uses for the room. Message history is not a room property: a fresh subscription receives no history and an ordinary reconnect restores subscriptions without replaying anything, so keep what you need in your own store. The broker replays only for load-balanced worker groups with persistent sessions; see ",[423,835,837],{"href":836},"/docs/concepts/replay","Replay and Durable Delivery",[11,839,840],{},"This three-level hierarchy covers the vast majority of real-time use cases without requiring you to design a topic namespace from scratch. App, room, topic. That is the model.",[842,843,844],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":48,"searchDepth":63,"depth":63,"links":846},[847,848,849,852,853,854,855,856],{"id":20,"depth":63,"text":21},{"id":33,"depth":63,"text":34},{"id":156,"depth":63,"text":157,"children":850},[851],{"id":193,"depth":96,"text":194},{"id":209,"depth":63,"text":210},{"id":236,"depth":63,"text":237},{"id":433,"depth":63,"text":434},{"id":450,"depth":63,"text":451},{"id":777,"depth":63,"text":778},"Architecture","2026-04-07","Understand publish/subscribe from first principles. Learn how topics, rooms, and filters route messages at scale, and how NoLag models real-time apps with apps, rooms, and topics.",null,"md",{},"/blog/pubsub-pattern-explained","9 min read",{"title":5,"description":859},"blog/pubsub-pattern-explained","fEnp0vZkSE_28Obu8ijl-DCnbmWzHuMKBiXHLlQZR14",1789869417600]