[{"data":1,"prerenderedAt":1663},["ShallowReactive",2],{"blog:/blog/build-chat-app-in-minutes":3},{"id":4,"title":5,"author":6,"body":7,"category":1652,"date":1653,"description":1654,"excerpt":1655,"extension":1656,"meta":1657,"navigation":169,"path":1658,"readTime":1659,"seo":1660,"stem":1661,"__hash__":1662},"blog/blog/build-chat-app-in-minutes.md","How to Build a Real-Time Chat App in Under 10 Minutes","Henco Burger",{"type":8,"value":9,"toc":1639},"minimark",[10,14,22,27,30,62,65,69,98,105,109,120,445,448,452,463,643,658,661,690,694,701,933,940,944,947,1141,1148,1152,1158,1237,1241,1244,1333,1337,1340,1400,1569,1573,1576,1601,1611,1615,1623,1635],[11,12,13],"p",{},"Most real-time chat tutorials start with raw WebSockets, a custom message format, and a server you have to build yourself. By the end you have a working toy that breaks the moment two people type at the same time or one of them loses their wifi signal.",[11,15,16,17,21],{},"This tutorial skips the plumbing. We will use ",[18,19,20],"code",{},"@nolag/chat",", a purpose-built chat SDK that wraps the NoLag core with a high-level API. Multi-room chat, typing indicators, presence, and unread counts are all included out of the box.",[23,24,26],"h2",{"id":25},"step-1-install-the-sdk","Step 1: Install the SDK",[11,28,29],{},"The chat SDK is a peer of the core NoLag JS SDK. Install both:",[31,32,33],"code-tabs",{},[34,35,41],"pre",{"className":36,"code":37,"filename":38,"language":39,"meta":40,"style":40},"language-bash shiki shiki-themes github-light github-dark","npm install @nolag/chat @nolag/js-sdk\n","Terminal","bash","",[18,42,43],{"__ignoreMap":40},[44,45,48,52,56,59],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sScJk","npm",[44,53,55],{"class":54},"sZZnC"," install",[44,57,58],{"class":54}," @nolag/chat",[44,60,61],{"class":54}," @nolag/js-sdk\n",[11,63,64],{},"The package is framework agnostic. It works with React, Vue, vanilla JS, or anything else that runs JavaScript. It is pure events and callbacks, so you wire it to your framework's reactivity however you prefer.",[23,66,68],{"id":67},"step-2-get-your-app-and-token","Step 2: Get Your App and Token",[11,70,71,72,75,76,79,80,79,83,79,86,89,90,93,94,97],{},"Create an app from the ",[18,73,74],{},"nolag-chat-sdk"," blueprint in the NoLag portal (or via the API). That seeds the rooms this tutorial uses (",[18,77,78],{},"general",", ",[18,81,82],{},"random",[18,84,85],{},"help",[18,87,88],{},"dev",") and returns an app slug with a random suffix, for example ",[18,91,92],{},"my-chat-app-a3f9",". That suffixed slug is the ",[18,95,96],{},"appName"," below; rooms never exist implicitly, so if you want other room names create them first.",[11,99,100,101,104],{},"Then create an actor (type ",[18,102,103],{},"user",") and take its access token. The token authenticates the WebSocket connection and scopes what the client can access.",[23,106,108],{"id":107},"step-3-connect","Step 3: Connect",[11,110,111,112,115,116,119],{},"Create a core ",[18,113,114],{},"NoLag"," client with your token, hand it to a ",[18,117,118],{},"NoLagChat"," instance along with a username and optionally the rooms you want to pre-subscribe to, then connect the client and wait for the wrapper to be ready.",[31,121,122],{},[34,123,128],{"className":124,"code":125,"filename":126,"language":127,"meta":40,"style":40},"language-typescript shiki shiki-themes github-light github-dark","import { NoLag } from '@nolag/js-sdk';\nimport { NoLagChat } from '@nolag/chat';\n\nconst client = NoLag(token);\nconst chat = new NoLagChat({\n  client,\n  username: 'Alice',\n  appName: APP_SLUG, // the suffixed slug returned when you created the app\n  rooms: ['general', 'random'],\n});\n\nchat.on('connected', () => {\n  console.log('Connected!');\n});\n\nchat.on('userOnline', (user) => {\n  console.log(user.username, 'is online');\n});\n\nchat.on('userOffline', (user) => {\n  console.log(user.username, 'went offline');\n});\n\nawait client.connect();\nawait chat.ready();\n","TypeScript","typescript",[18,129,130,149,164,171,190,209,215,227,242,259,265,270,294,311,316,321,346,361,366,371,393,407,412,417,432],{"__ignoreMap":40},[44,131,132,136,140,143,146],{"class":46,"line":47},[44,133,135],{"class":134},"szBVR","import",[44,137,139],{"class":138},"sVt8B"," { NoLag } ",[44,141,142],{"class":134},"from",[44,144,145],{"class":54}," '@nolag/js-sdk'",[44,147,148],{"class":138},";\n",[44,150,152,154,157,159,162],{"class":46,"line":151},2,[44,153,135],{"class":134},[44,155,156],{"class":138}," { NoLagChat } ",[44,158,142],{"class":134},[44,160,161],{"class":54}," '@nolag/chat'",[44,163,148],{"class":138},[44,165,167],{"class":46,"line":166},3,[44,168,170],{"emptyLinePlaceholder":169},true,"\n",[44,172,174,177,181,184,187],{"class":46,"line":173},4,[44,175,176],{"class":134},"const",[44,178,180],{"class":179},"sj4cs"," client",[44,182,183],{"class":134}," =",[44,185,186],{"class":50}," NoLag",[44,188,189],{"class":138},"(token);\n",[44,191,193,195,198,200,203,206],{"class":46,"line":192},5,[44,194,176],{"class":134},[44,196,197],{"class":179}," chat",[44,199,183],{"class":134},[44,201,202],{"class":134}," new",[44,204,205],{"class":50}," NoLagChat",[44,207,208],{"class":138},"({\n",[44,210,212],{"class":46,"line":211},6,[44,213,214],{"class":138},"  client,\n",[44,216,218,221,224],{"class":46,"line":217},7,[44,219,220],{"class":138},"  username: ",[44,222,223],{"class":54},"'Alice'",[44,225,226],{"class":138},",\n",[44,228,230,233,236,238],{"class":46,"line":229},8,[44,231,232],{"class":138},"  appName: ",[44,234,235],{"class":179},"APP_SLUG",[44,237,79],{"class":138},[44,239,241],{"class":240},"sJ8bj","// the suffixed slug returned when you created the app\n",[44,243,245,248,251,253,256],{"class":46,"line":244},9,[44,246,247],{"class":138},"  rooms: [",[44,249,250],{"class":54},"'general'",[44,252,79],{"class":138},[44,254,255],{"class":54},"'random'",[44,257,258],{"class":138},"],\n",[44,260,262],{"class":46,"line":261},10,[44,263,264],{"class":138},"});\n",[44,266,268],{"class":46,"line":267},11,[44,269,170],{"emptyLinePlaceholder":169},[44,271,273,276,279,282,285,288,291],{"class":46,"line":272},12,[44,274,275],{"class":138},"chat.",[44,277,278],{"class":50},"on",[44,280,281],{"class":138},"(",[44,283,284],{"class":54},"'connected'",[44,286,287],{"class":138},", () ",[44,289,290],{"class":134},"=>",[44,292,293],{"class":138}," {\n",[44,295,297,300,303,305,308],{"class":46,"line":296},13,[44,298,299],{"class":138},"  console.",[44,301,302],{"class":50},"log",[44,304,281],{"class":138},[44,306,307],{"class":54},"'Connected!'",[44,309,310],{"class":138},");\n",[44,312,314],{"class":46,"line":313},14,[44,315,264],{"class":138},[44,317,319],{"class":46,"line":318},15,[44,320,170],{"emptyLinePlaceholder":169},[44,322,324,326,328,330,333,336,339,342,344],{"class":46,"line":323},16,[44,325,275],{"class":138},[44,327,278],{"class":50},[44,329,281],{"class":138},[44,331,332],{"class":54},"'userOnline'",[44,334,335],{"class":138},", (",[44,337,103],{"class":338},"s4XuR",[44,340,341],{"class":138},") ",[44,343,290],{"class":134},[44,345,293],{"class":138},[44,347,349,351,353,356,359],{"class":46,"line":348},17,[44,350,299],{"class":138},[44,352,302],{"class":50},[44,354,355],{"class":138},"(user.username, ",[44,357,358],{"class":54},"'is online'",[44,360,310],{"class":138},[44,362,364],{"class":46,"line":363},18,[44,365,264],{"class":138},[44,367,369],{"class":46,"line":368},19,[44,370,170],{"emptyLinePlaceholder":169},[44,372,374,376,378,380,383,385,387,389,391],{"class":46,"line":373},20,[44,375,275],{"class":138},[44,377,278],{"class":50},[44,379,281],{"class":138},[44,381,382],{"class":54},"'userOffline'",[44,384,335],{"class":138},[44,386,103],{"class":338},[44,388,341],{"class":138},[44,390,290],{"class":134},[44,392,293],{"class":138},[44,394,396,398,400,402,405],{"class":46,"line":395},21,[44,397,299],{"class":138},[44,399,302],{"class":50},[44,401,355],{"class":138},[44,403,404],{"class":54},"'went offline'",[44,406,310],{"class":138},[44,408,410],{"class":46,"line":409},22,[44,411,264],{"class":138},[44,413,415],{"class":46,"line":414},23,[44,416,170],{"emptyLinePlaceholder":169},[44,418,420,423,426,429],{"class":46,"line":419},24,[44,421,422],{"class":134},"await",[44,424,425],{"class":138}," client.",[44,427,428],{"class":50},"connect",[44,430,431],{"class":138},"();\n",[44,433,435,437,440,443],{"class":46,"line":434},25,[44,436,422],{"class":134},[44,438,439],{"class":138}," chat.",[44,441,442],{"class":50},"ready",[44,444,431],{"class":138},[11,446,447],{},"That is it for connection setup. Your app owns the connection; the wrapper attaches to it, sets up global presence tracking via a lobby, and pre-subscribes to the rooms you listed. If the connection drops, the client reconnects automatically and the wrapper restores your rooms and presence.",[23,449,451],{"id":450},"step-4-join-a-room-and-send-messages","Step 4: Join a Room and Send Messages",[11,453,454,455,458,459,462],{},"Rooms are the unit of conversation. Call ",[18,456,457],{},"joinRoom()"," to activate a room. This returns a ",[18,460,461],{},"ChatRoom"," object you use to send and receive messages.",[31,464,465],{},[34,466,468],{"className":124,"code":467,"filename":126,"language":127,"meta":40,"style":40},"const room = chat.joinRoom('general');\n\n// Listen for incoming messages from other users\nroom.on('message', (msg) => {\n  console.log(`${msg.username}: ${msg.text}`);\n});\n\n// Your own messages arrive on `messageSent`. Publishers never receive their\n// own messages back, so the SDK adds them to the local store for you.\nroom.on('messageSent', (msg) => {\n  console.log(`me: ${msg.text}`);\n});\n\n// Send a message (returns an optimistic ChatMessage immediately)\nroom.sendMessage('Hello from the tutorial!');\n",[18,469,470,490,494,499,522,556,560,564,569,574,595,616,620,624,629],{"__ignoreMap":40},[44,471,472,474,477,479,481,484,486,488],{"class":46,"line":47},[44,473,176],{"class":134},[44,475,476],{"class":179}," room",[44,478,183],{"class":134},[44,480,439],{"class":138},[44,482,483],{"class":50},"joinRoom",[44,485,281],{"class":138},[44,487,250],{"class":54},[44,489,310],{"class":138},[44,491,492],{"class":46,"line":151},[44,493,170],{"emptyLinePlaceholder":169},[44,495,496],{"class":46,"line":166},[44,497,498],{"class":240},"// Listen for incoming messages from other users\n",[44,500,501,504,506,508,511,513,516,518,520],{"class":46,"line":173},[44,502,503],{"class":138},"room.",[44,505,278],{"class":50},[44,507,281],{"class":138},[44,509,510],{"class":54},"'message'",[44,512,335],{"class":138},[44,514,515],{"class":338},"msg",[44,517,341],{"class":138},[44,519,290],{"class":134},[44,521,293],{"class":138},[44,523,524,526,528,530,533,535,538,541,544,546,548,551,554],{"class":46,"line":192},[44,525,299],{"class":138},[44,527,302],{"class":50},[44,529,281],{"class":138},[44,531,532],{"class":54},"`${",[44,534,515],{"class":138},[44,536,537],{"class":54},".",[44,539,540],{"class":138},"username",[44,542,543],{"class":54},"}: ${",[44,545,515],{"class":138},[44,547,537],{"class":54},[44,549,550],{"class":138},"text",[44,552,553],{"class":54},"}`",[44,555,310],{"class":138},[44,557,558],{"class":46,"line":211},[44,559,264],{"class":138},[44,561,562],{"class":46,"line":217},[44,563,170],{"emptyLinePlaceholder":169},[44,565,566],{"class":46,"line":229},[44,567,568],{"class":240},"// Your own messages arrive on `messageSent`. Publishers never receive their\n",[44,570,571],{"class":46,"line":244},[44,572,573],{"class":240},"// own messages back, so the SDK adds them to the local store for you.\n",[44,575,576,578,580,582,585,587,589,591,593],{"class":46,"line":261},[44,577,503],{"class":138},[44,579,278],{"class":50},[44,581,281],{"class":138},[44,583,584],{"class":54},"'messageSent'",[44,586,335],{"class":138},[44,588,515],{"class":338},[44,590,341],{"class":138},[44,592,290],{"class":134},[44,594,293],{"class":138},[44,596,597,599,601,603,606,608,610,612,614],{"class":46,"line":267},[44,598,299],{"class":138},[44,600,302],{"class":50},[44,602,281],{"class":138},[44,604,605],{"class":54},"`me: ${",[44,607,515],{"class":138},[44,609,537],{"class":54},[44,611,550],{"class":138},[44,613,553],{"class":54},[44,615,310],{"class":138},[44,617,618],{"class":46,"line":272},[44,619,264],{"class":138},[44,621,622],{"class":46,"line":296},[44,623,170],{"emptyLinePlaceholder":169},[44,625,626],{"class":46,"line":313},[44,627,628],{"class":240},"// Send a message (returns an optimistic ChatMessage immediately)\n",[44,630,631,633,636,638,641],{"class":46,"line":318},[44,632,503],{"class":138},[44,634,635],{"class":50},"sendMessage",[44,637,281],{"class":138},[44,639,640],{"class":54},"'Hello from the tutorial!'",[44,642,310],{"class":138},[11,644,645,646,649,650,653,654,657],{},"Messages are delivered in order. The ",[18,647,648],{},"sendMessage()"," call returns an optimistic message object instantly so your UI can render it before the server confirms. The message status starts as ",[18,651,652],{},"'sending'"," and is ",[18,655,656],{},"'sent'"," once the SDK has handed it to the connection.",[11,659,660],{},"You can also fetch all messages in the room at any time:",[31,662,663],{},[34,664,666],{"className":124,"code":665,"filename":126,"language":127,"meta":40,"style":40},"const messages = room.getMessages();\n// Returns all cached messages in timestamp order\n",[18,667,668,685],{"__ignoreMap":40},[44,669,670,672,675,677,680,683],{"class":46,"line":47},[44,671,176],{"class":134},[44,673,674],{"class":179}," messages",[44,676,183],{"class":134},[44,678,679],{"class":138}," room.",[44,681,682],{"class":50},"getMessages",[44,684,431],{"class":138},[44,686,687],{"class":46,"line":151},[44,688,689],{"class":240},"// Returns all cached messages in timestamp order\n",[23,691,693],{"id":692},"step-5-add-typing-indicators","Step 5: Add Typing Indicators",[11,695,696,697,700],{},"Typing indicators make chat feel alive. The SDK handles the debounce and timeout for you. Call ",[18,698,699],{},"startTyping()"," when the user types, and it auto-stops after a configurable timeout (default 3 seconds).",[31,702,703],{},[34,704,706],{"className":124,"code":705,"filename":126,"language":127,"meta":40,"style":40},"// Trigger when the user types in the input\ninputElement.addEventListener('input', () => {\n  room.startTyping();\n});\n\n// Listen for typing state changes\nroom.on('typing', ({ users }) => {\n  if (users.length === 1) {\n    indicator.textContent = `${users[0].username} is typing...`;\n  } else if (users.length > 1) {\n    indicator.textContent = `${users[0].username} and ${users.length - 1} others are typing...`;\n  } else {\n    indicator.textContent = '';\n  }\n});\n\n// You can also stop typing explicitly\nroom.stopTyping();\n",[18,707,708,713,732,742,746,750,755,779,799,828,850,887,895,906,911,915,919,924],{"__ignoreMap":40},[44,709,710],{"class":46,"line":47},[44,711,712],{"class":240},"// Trigger when the user types in the input\n",[44,714,715,718,721,723,726,728,730],{"class":46,"line":151},[44,716,717],{"class":138},"inputElement.",[44,719,720],{"class":50},"addEventListener",[44,722,281],{"class":138},[44,724,725],{"class":54},"'input'",[44,727,287],{"class":138},[44,729,290],{"class":134},[44,731,293],{"class":138},[44,733,734,737,740],{"class":46,"line":166},[44,735,736],{"class":138},"  room.",[44,738,739],{"class":50},"startTyping",[44,741,431],{"class":138},[44,743,744],{"class":46,"line":173},[44,745,264],{"class":138},[44,747,748],{"class":46,"line":192},[44,749,170],{"emptyLinePlaceholder":169},[44,751,752],{"class":46,"line":211},[44,753,754],{"class":240},"// Listen for typing state changes\n",[44,756,757,759,761,763,766,769,772,775,777],{"class":46,"line":217},[44,758,503],{"class":138},[44,760,278],{"class":50},[44,762,281],{"class":138},[44,764,765],{"class":54},"'typing'",[44,767,768],{"class":138},", ({ ",[44,770,771],{"class":338},"users",[44,773,774],{"class":138}," }) ",[44,776,290],{"class":134},[44,778,293],{"class":138},[44,780,781,784,787,790,793,796],{"class":46,"line":229},[44,782,783],{"class":134},"  if",[44,785,786],{"class":138}," (users.",[44,788,789],{"class":179},"length",[44,791,792],{"class":134}," ===",[44,794,795],{"class":179}," 1",[44,797,798],{"class":138},") {\n",[44,800,801,804,807,810,812,815,818,821,823,826],{"class":46,"line":244},[44,802,803],{"class":138},"    indicator.textContent ",[44,805,806],{"class":134},"=",[44,808,809],{"class":54}," `${",[44,811,771],{"class":138},[44,813,814],{"class":54},"[",[44,816,817],{"class":179},"0",[44,819,820],{"class":54},"].",[44,822,540],{"class":138},[44,824,825],{"class":54},"} is typing...`",[44,827,148],{"class":138},[44,829,830,833,836,839,841,843,846,848],{"class":46,"line":261},[44,831,832],{"class":138},"  } ",[44,834,835],{"class":134},"else",[44,837,838],{"class":134}," if",[44,840,786],{"class":138},[44,842,789],{"class":179},[44,844,845],{"class":134}," >",[44,847,795],{"class":179},[44,849,798],{"class":138},[44,851,852,854,856,858,860,862,864,866,868,871,873,875,877,880,882,885],{"class":46,"line":267},[44,853,803],{"class":138},[44,855,806],{"class":134},[44,857,809],{"class":54},[44,859,771],{"class":138},[44,861,814],{"class":54},[44,863,817],{"class":179},[44,865,820],{"class":54},[44,867,540],{"class":138},[44,869,870],{"class":54},"} and ${",[44,872,771],{"class":138},[44,874,537],{"class":54},[44,876,789],{"class":179},[44,878,879],{"class":134}," -",[44,881,795],{"class":179},[44,883,884],{"class":54},"} others are typing...`",[44,886,148],{"class":138},[44,888,889,891,893],{"class":46,"line":272},[44,890,832],{"class":138},[44,892,835],{"class":134},[44,894,293],{"class":138},[44,896,897,899,901,904],{"class":46,"line":296},[44,898,803],{"class":138},[44,900,806],{"class":134},[44,902,903],{"class":54}," ''",[44,905,148],{"class":138},[44,907,908],{"class":46,"line":313},[44,909,910],{"class":138},"  }\n",[44,912,913],{"class":46,"line":318},[44,914,264],{"class":138},[44,916,917],{"class":46,"line":323},[44,918,170],{"emptyLinePlaceholder":169},[44,920,921],{"class":46,"line":348},[44,922,923],{"class":240},"// You can also stop typing explicitly\n",[44,925,926,928,931],{"class":46,"line":363},[44,927,503],{"class":138},[44,929,930],{"class":50},"stopTyping",[44,932,431],{"class":138},[11,934,935,936,939],{},"Typing events are sent on a separate internal topic (",[18,937,938],{},"_typing",") so they do not pollute your message stream. They are ephemeral and not persisted.",[23,941,943],{"id":942},"step-6-show-who-is-online","Step 6: Show Who Is Online",[11,945,946],{},"Presence is built into the SDK at two levels: global (who is online across all rooms) and per-room (who is in this specific room).",[31,948,949],{},[34,950,952],{"className":124,"code":951,"filename":126,"language":127,"meta":40,"style":40},"// Global: all users online across all rooms\nconst onlineUsers = chat.getOnlineUsers();\n\n// Per-room: users in this specific room\nconst roomMembers = room.getUsers();\n\n// Listen for users joining or leaving the room\nroom.on('userJoined', (user) => {\n  console.log(user.username, 'joined the room');\n});\n\nroom.on('userLeft', (user) => {\n  console.log(user.username, 'left the room');\n});\n\n// Update your own status\nchat.setStatus('away');   // 'online', 'away', 'busy', 'offline'\n\n// Update your profile\nchat.updateProfile({ username: 'Alice B' });\n",[18,953,954,959,975,979,984,1000,1004,1009,1030,1043,1047,1051,1072,1085,1089,1093,1098,1116,1120,1125],{"__ignoreMap":40},[44,955,956],{"class":46,"line":47},[44,957,958],{"class":240},"// Global: all users online across all rooms\n",[44,960,961,963,966,968,970,973],{"class":46,"line":151},[44,962,176],{"class":134},[44,964,965],{"class":179}," onlineUsers",[44,967,183],{"class":134},[44,969,439],{"class":138},[44,971,972],{"class":50},"getOnlineUsers",[44,974,431],{"class":138},[44,976,977],{"class":46,"line":166},[44,978,170],{"emptyLinePlaceholder":169},[44,980,981],{"class":46,"line":173},[44,982,983],{"class":240},"// Per-room: users in this specific room\n",[44,985,986,988,991,993,995,998],{"class":46,"line":192},[44,987,176],{"class":134},[44,989,990],{"class":179}," roomMembers",[44,992,183],{"class":134},[44,994,679],{"class":138},[44,996,997],{"class":50},"getUsers",[44,999,431],{"class":138},[44,1001,1002],{"class":46,"line":211},[44,1003,170],{"emptyLinePlaceholder":169},[44,1005,1006],{"class":46,"line":217},[44,1007,1008],{"class":240},"// Listen for users joining or leaving the room\n",[44,1010,1011,1013,1015,1017,1020,1022,1024,1026,1028],{"class":46,"line":229},[44,1012,503],{"class":138},[44,1014,278],{"class":50},[44,1016,281],{"class":138},[44,1018,1019],{"class":54},"'userJoined'",[44,1021,335],{"class":138},[44,1023,103],{"class":338},[44,1025,341],{"class":138},[44,1027,290],{"class":134},[44,1029,293],{"class":138},[44,1031,1032,1034,1036,1038,1041],{"class":46,"line":244},[44,1033,299],{"class":138},[44,1035,302],{"class":50},[44,1037,355],{"class":138},[44,1039,1040],{"class":54},"'joined the room'",[44,1042,310],{"class":138},[44,1044,1045],{"class":46,"line":261},[44,1046,264],{"class":138},[44,1048,1049],{"class":46,"line":267},[44,1050,170],{"emptyLinePlaceholder":169},[44,1052,1053,1055,1057,1059,1062,1064,1066,1068,1070],{"class":46,"line":272},[44,1054,503],{"class":138},[44,1056,278],{"class":50},[44,1058,281],{"class":138},[44,1060,1061],{"class":54},"'userLeft'",[44,1063,335],{"class":138},[44,1065,103],{"class":338},[44,1067,341],{"class":138},[44,1069,290],{"class":134},[44,1071,293],{"class":138},[44,1073,1074,1076,1078,1080,1083],{"class":46,"line":296},[44,1075,299],{"class":138},[44,1077,302],{"class":50},[44,1079,355],{"class":138},[44,1081,1082],{"class":54},"'left the room'",[44,1084,310],{"class":138},[44,1086,1087],{"class":46,"line":313},[44,1088,264],{"class":138},[44,1090,1091],{"class":46,"line":318},[44,1092,170],{"emptyLinePlaceholder":169},[44,1094,1095],{"class":46,"line":323},[44,1096,1097],{"class":240},"// Update your own status\n",[44,1099,1100,1102,1105,1107,1110,1113],{"class":46,"line":348},[44,1101,275],{"class":138},[44,1103,1104],{"class":50},"setStatus",[44,1106,281],{"class":138},[44,1108,1109],{"class":54},"'away'",[44,1111,1112],{"class":138},");   ",[44,1114,1115],{"class":240},"// 'online', 'away', 'busy', 'offline'\n",[44,1117,1118],{"class":46,"line":363},[44,1119,170],{"emptyLinePlaceholder":169},[44,1121,1122],{"class":46,"line":368},[44,1123,1124],{"class":240},"// Update your profile\n",[44,1126,1127,1129,1132,1135,1138],{"class":46,"line":373},[44,1128,275],{"class":138},[44,1130,1131],{"class":50},"updateProfile",[44,1133,1134],{"class":138},"({ username: ",[44,1136,1137],{"class":54},"'Alice B'",[44,1139,1140],{"class":138}," });\n",[11,1142,1143,1144,1147],{},"Global presence uses a lobby under the hood. When a user connects, they are tracked across rooms. When their connection closes, a ",[18,1145,1146],{},"userOffline"," event fires. Heartbeats are handled by the core client, so you do not have to think about them.",[23,1149,1151],{"id":1150},"switching-rooms","Switching Rooms",[11,1153,1154,1155,1157],{},"Calling ",[18,1156,457],{}," activates that room and deactivates the previous one. Presence is tracked per active room. You can also leave rooms entirely:",[31,1159,1160],{},[34,1161,1163],{"className":124,"code":1162,"filename":126,"language":127,"meta":40,"style":40},"// Switch to a different room\nconst devRoom = chat.joinRoom('dev');\n\n// Leave a room completely\nchat.leaveRoom('random');\n\n// Get all joined rooms\nconst rooms = chat.getRooms();\n",[18,1164,1165,1170,1190,1194,1199,1212,1216,1221],{"__ignoreMap":40},[44,1166,1167],{"class":46,"line":47},[44,1168,1169],{"class":240},"// Switch to a different room\n",[44,1171,1172,1174,1177,1179,1181,1183,1185,1188],{"class":46,"line":151},[44,1173,176],{"class":134},[44,1175,1176],{"class":179}," devRoom",[44,1178,183],{"class":134},[44,1180,439],{"class":138},[44,1182,483],{"class":50},[44,1184,281],{"class":138},[44,1186,1187],{"class":54},"'dev'",[44,1189,310],{"class":138},[44,1191,1192],{"class":46,"line":166},[44,1193,170],{"emptyLinePlaceholder":169},[44,1195,1196],{"class":46,"line":173},[44,1197,1198],{"class":240},"// Leave a room completely\n",[44,1200,1201,1203,1206,1208,1210],{"class":46,"line":192},[44,1202,275],{"class":138},[44,1204,1205],{"class":50},"leaveRoom",[44,1207,281],{"class":138},[44,1209,255],{"class":54},[44,1211,310],{"class":138},[44,1213,1214],{"class":46,"line":211},[44,1215,170],{"emptyLinePlaceholder":169},[44,1217,1218],{"class":46,"line":217},[44,1219,1220],{"class":240},"// Get all joined rooms\n",[44,1222,1223,1225,1228,1230,1232,1235],{"class":46,"line":229},[44,1224,176],{"class":134},[44,1226,1227],{"class":179}," rooms",[44,1229,183],{"class":134},[44,1231,439],{"class":138},[44,1233,1234],{"class":50},"getRooms",[44,1236,431],{"class":138},[23,1238,1240],{"id":1239},"unread-counts","Unread Counts",[11,1242,1243],{},"The SDK tracks unread messages per room automatically. When a room is not active, incoming messages increment the unread count:",[31,1245,1246],{},[34,1247,1249],{"className":124,"code":1248,"filename":126,"language":127,"meta":40,"style":40},"room.on('unreadChanged', ({ room: roomName, count }) => {\n  updateBadge(roomName, count);\n});\n\n// Check the current count\nconsole.log(room.unreadCount);\n\n// Mark as read (resets to 0)\nroom.markRead();\n",[18,1250,1251,1284,1292,1296,1300,1305,1315,1319,1324],{"__ignoreMap":40},[44,1252,1253,1255,1257,1259,1262,1264,1267,1270,1273,1275,1278,1280,1282],{"class":46,"line":47},[44,1254,503],{"class":138},[44,1256,278],{"class":50},[44,1258,281],{"class":138},[44,1260,1261],{"class":54},"'unreadChanged'",[44,1263,768],{"class":138},[44,1265,1266],{"class":338},"room",[44,1268,1269],{"class":138},": ",[44,1271,1272],{"class":338},"roomName",[44,1274,79],{"class":138},[44,1276,1277],{"class":338},"count",[44,1279,774],{"class":138},[44,1281,290],{"class":134},[44,1283,293],{"class":138},[44,1285,1286,1289],{"class":46,"line":151},[44,1287,1288],{"class":50},"  updateBadge",[44,1290,1291],{"class":138},"(roomName, count);\n",[44,1293,1294],{"class":46,"line":166},[44,1295,264],{"class":138},[44,1297,1298],{"class":46,"line":173},[44,1299,170],{"emptyLinePlaceholder":169},[44,1301,1302],{"class":46,"line":192},[44,1303,1304],{"class":240},"// Check the current count\n",[44,1306,1307,1310,1312],{"class":46,"line":211},[44,1308,1309],{"class":138},"console.",[44,1311,302],{"class":50},[44,1313,1314],{"class":138},"(room.unreadCount);\n",[44,1316,1317],{"class":46,"line":217},[44,1318,170],{"emptyLinePlaceholder":169},[44,1320,1321],{"class":46,"line":229},[44,1322,1323],{"class":240},"// Mark as read (resets to 0)\n",[44,1325,1326,1328,1331],{"class":46,"line":244},[44,1327,503],{"class":138},[44,1329,1330],{"class":50},"markRead",[44,1332,431],{"class":138},[23,1334,1336],{"id":1335},"what-you-get-for-free","What You Get for Free",[11,1338,1339],{},"Building all of this yourself from raw WebSockets takes weeks. Here is what the SDK handles without any extra code:",[1341,1342,1343,1361,1373,1386],"ul",{},[1344,1345,1346,1350,1351,1354,1355,1360],"li",{},[1347,1348,1349],"strong",{},"Reconnection."," The client reconnects automatically on disconnect and the wrapper restores all rooms and presence state. A ",[18,1352,1353],{},"reconnected"," event fires so you can update your UI. Messages published while you were away are not replayed to chat clients (NoLag replays only to load-balanced worker groups, see ",[1356,1357,1359],"a",{"href":1358},"/docs/concepts/replay","replay","), so keep your own history store if you need catch-up across sessions.",[1344,1362,1363,1366,1367,1369,1370,1372],{},[1347,1364,1365],{},"Optimistic sends."," Messages appear in the UI instantly with a ",[18,1368,652],{}," status that flips to ",[18,1371,656],{}," as soon as they are handed to the connection.",[1344,1374,1375,1378,1379,1382,1383,537],{},[1347,1376,1377],{},"Unread counts."," Per-room unread tracking with ",[18,1380,1381],{},"unreadCount"," and ",[18,1384,1385],{},"markRead()",[1344,1387,1388,1391,1392,1395,1396,1399],{},[1347,1389,1390],{},"Framework agnostic."," Wire it to any framework's reactivity. The SDK is pure events. Listen to both ",[18,1393,1394],{},"message"," (others) and ",[18,1397,1398],{},"messageSent"," (you) to keep a rendered list complete.",[31,1401,1402,1488],{},[34,1403,1406],{"className":124,"code":1404,"filename":1405,"language":127,"meta":40,"style":40},"const messages = ref([]);\nconst refresh = () => {\n  messages.value = [...room.getMessages()];\n};\nroom.on('message', refresh);\nroom.on('messageSent', refresh);\n","Vue 3",[18,1407,1408,1422,1438,1458,1463,1476],{"__ignoreMap":40},[44,1409,1410,1412,1414,1416,1419],{"class":46,"line":47},[44,1411,176],{"class":134},[44,1413,674],{"class":179},[44,1415,183],{"class":134},[44,1417,1418],{"class":50}," ref",[44,1420,1421],{"class":138},"([]);\n",[44,1423,1424,1426,1429,1431,1434,1436],{"class":46,"line":151},[44,1425,176],{"class":134},[44,1427,1428],{"class":50}," refresh",[44,1430,183],{"class":134},[44,1432,1433],{"class":138}," () ",[44,1435,290],{"class":134},[44,1437,293],{"class":138},[44,1439,1440,1443,1445,1448,1451,1453,1455],{"class":46,"line":166},[44,1441,1442],{"class":138},"  messages.value ",[44,1444,806],{"class":134},[44,1446,1447],{"class":138}," [",[44,1449,1450],{"class":134},"...",[44,1452,503],{"class":138},[44,1454,682],{"class":50},[44,1456,1457],{"class":138},"()];\n",[44,1459,1460],{"class":46,"line":173},[44,1461,1462],{"class":138},"};\n",[44,1464,1465,1467,1469,1471,1473],{"class":46,"line":192},[44,1466,503],{"class":138},[44,1468,278],{"class":50},[44,1470,281],{"class":138},[44,1472,510],{"class":54},[44,1474,1475],{"class":138},", refresh);\n",[44,1477,1478,1480,1482,1484,1486],{"class":46,"line":211},[44,1479,503],{"class":138},[44,1481,278],{"class":50},[44,1483,281],{"class":138},[44,1485,584],{"class":54},[44,1487,1475],{"class":138},[34,1489,1492],{"className":124,"code":1490,"filename":1491,"language":127,"meta":40,"style":40},"const [messages, setMessages] = useState([]);\nconst refresh = () => setMessages([...room.getMessages()]);\nroom.on('message', refresh);\nroom.on('messageSent', refresh);\n","React",[18,1493,1494,1518,1545,1557],{"__ignoreMap":40},[44,1495,1496,1498,1500,1503,1505,1508,1511,1513,1516],{"class":46,"line":47},[44,1497,176],{"class":134},[44,1499,1447],{"class":138},[44,1501,1502],{"class":179},"messages",[44,1504,79],{"class":138},[44,1506,1507],{"class":179},"setMessages",[44,1509,1510],{"class":138},"] ",[44,1512,806],{"class":134},[44,1514,1515],{"class":50}," useState",[44,1517,1421],{"class":138},[44,1519,1520,1522,1524,1526,1528,1530,1533,1536,1538,1540,1542],{"class":46,"line":151},[44,1521,176],{"class":134},[44,1523,1428],{"class":50},[44,1525,183],{"class":134},[44,1527,1433],{"class":138},[44,1529,290],{"class":134},[44,1531,1532],{"class":50}," setMessages",[44,1534,1535],{"class":138},"([",[44,1537,1450],{"class":134},[44,1539,503],{"class":138},[44,1541,682],{"class":50},[44,1543,1544],{"class":138},"()]);\n",[44,1546,1547,1549,1551,1553,1555],{"class":46,"line":166},[44,1548,503],{"class":138},[44,1550,278],{"class":50},[44,1552,281],{"class":138},[44,1554,510],{"class":54},[44,1556,1475],{"class":138},[44,1558,1559,1561,1563,1565,1567],{"class":46,"line":173},[44,1560,503],{"class":138},[44,1562,278],{"class":50},[44,1564,281],{"class":138},[44,1566,584],{"class":54},[44,1568,1475],{"class":138},[23,1570,1572],{"id":1571},"disconnect","Disconnect",[11,1574,1575],{},"Clean up when the user leaves:",[31,1577,1578],{},[34,1579,1581],{"className":124,"code":1580,"filename":126,"language":127,"meta":40,"style":40},"chat.detach();\nclient.disconnect();\n",[18,1582,1583,1592],{"__ignoreMap":40},[44,1584,1585,1587,1590],{"class":46,"line":47},[44,1586,275],{"class":138},[44,1588,1589],{"class":50},"detach",[44,1591,431],{"class":138},[44,1593,1594,1597,1599],{"class":46,"line":151},[44,1595,1596],{"class":138},"client.",[44,1598,1571],{"class":50},[44,1600,431],{"class":138},[11,1602,1603,1606,1607,1610],{},[18,1604,1605],{},"detach()"," leaves all rooms, unsubscribes from the lobby, and releases the wrapper's handlers; ",[18,1608,1609],{},"disconnect()"," closes the WebSocket. The wrapper never touches the socket itself, so the same client can carry other wrappers or raw subscriptions until you disconnect it.",[23,1612,1614],{"id":1613},"where-to-go-from-here","Where to Go From Here",[11,1616,1617,1618,1622],{},"This walkthrough covers the happy path. For a production app you will also want to handle token refresh, room-level permissions, and message moderation. All of that is covered in the ",[1356,1619,1621],{"href":1620},"/docs","NoLag docs",". But for a working chat prototype with rooms, presence, typing indicators, optimistic sends, and unread counts, the steps above are all you need.",[11,1624,1625,1626,1382,1628,1630,1631,537],{},"The full API reference for ",[18,1627,118],{},[18,1629,461],{}," is on the ",[1356,1632,1634],{"href":1633},"/docs/high-level-sdks/chat","chat SDK page",[1636,1637,1638],"style",{},"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 .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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":40,"searchDepth":151,"depth":151,"links":1640},[1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651],{"id":25,"depth":151,"text":26},{"id":67,"depth":151,"text":68},{"id":107,"depth":151,"text":108},{"id":450,"depth":151,"text":451},{"id":692,"depth":151,"text":693},{"id":942,"depth":151,"text":943},{"id":1150,"depth":151,"text":1151},{"id":1239,"depth":151,"text":1240},{"id":1335,"depth":151,"text":1336},{"id":1571,"depth":151,"text":1572},{"id":1613,"depth":151,"text":1614},"Tutorial","2026-04-21","Step-by-step tutorial: install the NoLag chat SDK, connect with a token, join a room, send messages, show typing indicators, and display online users.",null,"md",{},"/blog/build-chat-app-in-minutes","6 min read",{"title":5,"description":1654},"blog/build-chat-app-in-minutes","8thDQoe2oyZCt5lZ3J44ATrZBQ_Gtwn_j5C6lrWmWIg",1789869416248]