[{"data":1,"prerenderedAt":711},["ShallowReactive",2],{"blog:/blog/migrate-from-pusher-to-nolag":3},{"id":4,"title":5,"author":6,"body":7,"category":700,"date":701,"description":702,"excerpt":703,"extension":704,"meta":705,"navigation":164,"path":706,"readTime":707,"seo":708,"stem":709,"__hash__":710},"blog/blog/migrate-from-pusher-to-nolag.md","Migrate from Pusher to NoLag","Henco Burger",{"type":8,"value":9,"toc":691},"minimark",[10,14,19,22,45,49,126,130,301,305,573,584,588,619,628,632,658,662,687],[11,12,13],"p",{},"Pusher Channels popularised hosted realtime pub/sub, and a lot of apps still run on it. If you are moving to NoLag, the good news is that the mental model is almost identical: you subscribe to a channel, you bind to events, and a publisher sends data to everyone listening. This guide maps Pusher concepts onto NoLag and shows the same subscribe-and-publish flow in both.",[15,16,18],"h2",{"id":17},"why-teams-move","Why teams move",[11,20,21],{},"NoLag keeps the pub/sub foundation you already rely on and adds a few things Pusher does not focus on:",[23,24,25,33,39],"ul",{},[26,27,28,32],"li",{},[29,30,31],"strong",{},"Blueprint SDKs"," for chat, notifications, dashboards, tracking, and more, so common patterns are a few lines instead of a from-scratch build.",[26,34,35,38],{},[29,36,37],{},"A coordination layer for AI agents",": dispatch work, share state, observe decisions, and gate actions with human approval, on the same infrastructure as your app.",[26,40,41,44],{},[29,42,43],{},"Per-message QoS"," on the broker hop, and presence built in.",[15,46,48],{"id":47},"concept-map","Concept map",[50,51,52,65],"table",{},[53,54,55],"thead",{},[56,57,58,62],"tr",{},[59,60,61],"th",{},"Pusher",[59,63,64],{},"NoLag",[66,67,68,77,85,98,110,118],"tbody",{},[56,69,70,74],{},[71,72,73],"td",{},"App key + cluster",[71,75,76],{},"App (a container for rooms and topics)",[56,78,79,82],{},[71,80,81],{},"Channel",[71,83,84],{},"Room + topic",[56,86,87,93],{},[71,88,89],{},[90,91,92],"code",{},"channel.bind('event', cb)",[71,94,95],{},[90,96,97],{},"room.on('topic', cb)",[56,99,100,105],{},[71,101,102],{},[90,103,104],{},"pusher.trigger('channel', 'event', data)",[71,106,107],{},[90,108,109],{},"room.emit('topic', data)",[56,111,112,115],{},[71,113,114],{},"Presence channel",[71,116,117],{},"Presence (per room)",[56,119,120,123],{},[71,121,122],{},"Private channel + auth endpoint",[71,124,125],{},"Actors, access tokens, and per-topic ACL",[15,127,129],{"id":128},"before-pusher","Before: Pusher",[131,132,137],"pre",{"className":133,"code":134,"language":135,"meta":136,"style":136},"language-js shiki shiki-themes github-light github-dark","import Pusher from 'pusher-js'\n\nconst pusher = new Pusher('APP_KEY', { cluster: 'eu' })\n\nconst channel = pusher.subscribe('chat')\nchannel.bind('message', (data) => {\n  console.log('Received:', data)\n})\n\n// On your server, using the pusher server SDK:\n// pusher.trigger('chat', 'message', { text: 'Hello!' })\n","js","",[90,138,139,159,166,201,206,230,260,277,283,288,295],{"__ignoreMap":136},[140,141,144,148,152,155],"span",{"class":142,"line":143},"line",1,[140,145,147],{"class":146},"szBVR","import",[140,149,151],{"class":150},"sVt8B"," Pusher ",[140,153,154],{"class":146},"from",[140,156,158],{"class":157},"sZZnC"," 'pusher-js'\n",[140,160,162],{"class":142,"line":161},2,[140,163,165],{"emptyLinePlaceholder":164},true,"\n",[140,167,169,172,176,179,182,186,189,192,195,198],{"class":142,"line":168},3,[140,170,171],{"class":146},"const",[140,173,175],{"class":174},"sj4cs"," pusher",[140,177,178],{"class":146}," =",[140,180,181],{"class":146}," new",[140,183,185],{"class":184},"sScJk"," Pusher",[140,187,188],{"class":150},"(",[140,190,191],{"class":157},"'APP_KEY'",[140,193,194],{"class":150},", { cluster: ",[140,196,197],{"class":157},"'eu'",[140,199,200],{"class":150}," })\n",[140,202,204],{"class":142,"line":203},4,[140,205,165],{"emptyLinePlaceholder":164},[140,207,209,211,214,216,219,222,224,227],{"class":142,"line":208},5,[140,210,171],{"class":146},[140,212,213],{"class":174}," channel",[140,215,178],{"class":146},[140,217,218],{"class":150}," pusher.",[140,220,221],{"class":184},"subscribe",[140,223,188],{"class":150},[140,225,226],{"class":157},"'chat'",[140,228,229],{"class":150},")\n",[140,231,233,236,239,241,244,247,251,254,257],{"class":142,"line":232},6,[140,234,235],{"class":150},"channel.",[140,237,238],{"class":184},"bind",[140,240,188],{"class":150},[140,242,243],{"class":157},"'message'",[140,245,246],{"class":150},", (",[140,248,250],{"class":249},"s4XuR","data",[140,252,253],{"class":150},") ",[140,255,256],{"class":146},"=>",[140,258,259],{"class":150}," {\n",[140,261,263,266,269,271,274],{"class":142,"line":262},7,[140,264,265],{"class":150},"  console.",[140,267,268],{"class":184},"log",[140,270,188],{"class":150},[140,272,273],{"class":157},"'Received:'",[140,275,276],{"class":150},", data)\n",[140,278,280],{"class":142,"line":279},8,[140,281,282],{"class":150},"})\n",[140,284,286],{"class":142,"line":285},9,[140,287,165],{"emptyLinePlaceholder":164},[140,289,291],{"class":142,"line":290},10,[140,292,294],{"class":293},"sJ8bj","// On your server, using the pusher server SDK:\n",[140,296,298],{"class":142,"line":297},11,[140,299,300],{"class":293},"// pusher.trigger('chat', 'message', { text: 'Hello!' })\n",[15,302,304],{"id":303},"after-nolag","After: NoLag",[131,306,310],{"className":307,"code":308,"language":309,"meta":136,"style":136},"language-ts shiki shiki-themes github-light github-dark","import { NoLag } from '@nolag/js-sdk'\n\n// Create the app and its `general` room in the control plane first. The app\n// slug you get back has a random suffix (for example `chat-a3f9`): that is\n// what you pass to setApp(). Room slugs are kept verbatim.\nconst APP_SLUG = 'chat-a3f9'\n\n// Browser: subscribe as one actor\nconst client = NoLag('your_access_token')\nawait client.connect()\n\nconst room = client.setApp(APP_SLUG).setRoom('general')\nroom.subscribe('message')\nroom.on('message', (data) => {\n  console.log('Received:', data)\n})\n\n// Server (or any other authorised actor): publish. Publishers never receive\n// their own messages, so publish from a different actor than the one listening.\nconst server = NoLag('service_access_token')\nawait server.connect()\nserver.setApp(APP_SLUG).setRoom('general').emit('message', { text: 'Hello!' })\n","ts",[90,311,312,324,328,333,338,343,355,359,364,383,397,401,434,448,470,483,488,493,499,505,524,536],{"__ignoreMap":136},[140,313,314,316,319,321],{"class":142,"line":143},[140,315,147],{"class":146},[140,317,318],{"class":150}," { NoLag } ",[140,320,154],{"class":146},[140,322,323],{"class":157}," '@nolag/js-sdk'\n",[140,325,326],{"class":142,"line":161},[140,327,165],{"emptyLinePlaceholder":164},[140,329,330],{"class":142,"line":168},[140,331,332],{"class":293},"// Create the app and its `general` room in the control plane first. The app\n",[140,334,335],{"class":142,"line":203},[140,336,337],{"class":293},"// slug you get back has a random suffix (for example `chat-a3f9`): that is\n",[140,339,340],{"class":142,"line":208},[140,341,342],{"class":293},"// what you pass to setApp(). Room slugs are kept verbatim.\n",[140,344,345,347,350,352],{"class":142,"line":232},[140,346,171],{"class":146},[140,348,349],{"class":174}," APP_SLUG",[140,351,178],{"class":146},[140,353,354],{"class":157}," 'chat-a3f9'\n",[140,356,357],{"class":142,"line":262},[140,358,165],{"emptyLinePlaceholder":164},[140,360,361],{"class":142,"line":279},[140,362,363],{"class":293},"// Browser: subscribe as one actor\n",[140,365,366,368,371,373,376,378,381],{"class":142,"line":285},[140,367,171],{"class":146},[140,369,370],{"class":174}," client",[140,372,178],{"class":146},[140,374,375],{"class":184}," NoLag",[140,377,188],{"class":150},[140,379,380],{"class":157},"'your_access_token'",[140,382,229],{"class":150},[140,384,385,388,391,394],{"class":142,"line":290},[140,386,387],{"class":146},"await",[140,389,390],{"class":150}," client.",[140,392,393],{"class":184},"connect",[140,395,396],{"class":150},"()\n",[140,398,399],{"class":142,"line":297},[140,400,165],{"emptyLinePlaceholder":164},[140,402,404,406,409,411,413,416,418,421,424,427,429,432],{"class":142,"line":403},12,[140,405,171],{"class":146},[140,407,408],{"class":174}," room",[140,410,178],{"class":146},[140,412,390],{"class":150},[140,414,415],{"class":184},"setApp",[140,417,188],{"class":150},[140,419,420],{"class":174},"APP_SLUG",[140,422,423],{"class":150},").",[140,425,426],{"class":184},"setRoom",[140,428,188],{"class":150},[140,430,431],{"class":157},"'general'",[140,433,229],{"class":150},[140,435,437,440,442,444,446],{"class":142,"line":436},13,[140,438,439],{"class":150},"room.",[140,441,221],{"class":184},[140,443,188],{"class":150},[140,445,243],{"class":157},[140,447,229],{"class":150},[140,449,451,453,456,458,460,462,464,466,468],{"class":142,"line":450},14,[140,452,439],{"class":150},[140,454,455],{"class":184},"on",[140,457,188],{"class":150},[140,459,243],{"class":157},[140,461,246],{"class":150},[140,463,250],{"class":249},[140,465,253],{"class":150},[140,467,256],{"class":146},[140,469,259],{"class":150},[140,471,473,475,477,479,481],{"class":142,"line":472},15,[140,474,265],{"class":150},[140,476,268],{"class":184},[140,478,188],{"class":150},[140,480,273],{"class":157},[140,482,276],{"class":150},[140,484,486],{"class":142,"line":485},16,[140,487,282],{"class":150},[140,489,491],{"class":142,"line":490},17,[140,492,165],{"emptyLinePlaceholder":164},[140,494,496],{"class":142,"line":495},18,[140,497,498],{"class":293},"// Server (or any other authorised actor): publish. Publishers never receive\n",[140,500,502],{"class":142,"line":501},19,[140,503,504],{"class":293},"// their own messages, so publish from a different actor than the one listening.\n",[140,506,508,510,513,515,517,519,522],{"class":142,"line":507},20,[140,509,171],{"class":146},[140,511,512],{"class":174}," server",[140,514,178],{"class":146},[140,516,375],{"class":184},[140,518,188],{"class":150},[140,520,521],{"class":157},"'service_access_token'",[140,523,229],{"class":150},[140,525,527,529,532,534],{"class":142,"line":526},21,[140,528,387],{"class":146},[140,530,531],{"class":150}," server.",[140,533,393],{"class":184},[140,535,396],{"class":150},[140,537,539,542,544,546,548,550,552,554,556,558,561,563,565,568,571],{"class":142,"line":538},22,[140,540,541],{"class":150},"server.",[140,543,415],{"class":184},[140,545,188],{"class":150},[140,547,420],{"class":174},[140,549,423],{"class":150},[140,551,426],{"class":184},[140,553,188],{"class":150},[140,555,431],{"class":157},[140,557,423],{"class":150},[140,559,560],{"class":184},"emit",[140,562,188],{"class":150},[140,564,243],{"class":157},[140,566,567],{"class":150},", { text: ",[140,569,570],{"class":157},"'Hello!'",[140,572,200],{"class":150},[11,574,575,576,579,580,583],{},"The shape is the same. The main differences are that NoLag groups topics inside a ",[29,577,578],{},"room"," (so you get natural namespacing and presence per room), access is controlled with ",[29,581,582],{},"actors and tokens"," rather than a per-channel auth endpoint, and any authorised actor can publish from the client side, not only your server. A NoLag publisher never hears its own message back, so append it to your UI locally when you send.",[15,585,587],{"id":586},"auth-delete-your-auth-endpoint","Auth: delete your auth endpoint",[11,589,590,591,594,595,598,599,598,602,598,605,598,608,598,611,614,615,618],{},"Pusher private and presence channels call back to an auth endpoint you host, on every subscribe. NoLag does not need one. A connection authenticates once with an ",[29,592,593],{},"actor access token",", and what that actor can read or write is enforced at the broker with per-topic ACL, so there is no per-subscribe round-trip to your server. Every credential resolves to a typed actor: ",[90,596,597],{},"user",", ",[90,600,601],{},"device",[90,603,604],{},"service",[90,606,607],{},"session",[90,609,610],{},"agent",[90,612,613],{},"orchestrator"," or ",[90,616,617],{},"observer",".",[11,620,621,622,627],{},"For browser clients, mint a short-lived JWT (",[623,624,626],"a",{"href":625},"/docs/client-tokens","client token",") on your backend, signed with a project signing key. No NoLag API call is needed to issue one, and it expires on its own within minutes, so a long-lived secret never reaches the browser.",[15,629,631],{"id":630},"presence","Presence",[11,633,634,635,638,639,642,643,598,646,649,650,653,654,657],{},"Pusher presence channels map to NoLag ",[623,636,630],{"href":637},"/docs/concepts/presence",", tracked per room. A client joins the member list by calling ",[90,640,641],{},"room.setPresence(data)",", and everyone else gets ",[90,644,645],{},"presence:join",[90,647,648],{},"presence:leave"," and ",[90,651,652],{},"presence:update"," events on the client, plus ",[90,655,656],{},"room.fetchPresence()"," for the current member list, without standing up your own tracking.",[15,659,661],{"id":660},"next-steps","Next steps",[23,663,664,672,680],{},[26,665,666,667,671],{},"Follow the ",[623,668,670],{"href":669},"/docs/getting-started","5-minute quick start"," to connect for the first time.",[26,673,674,675,679],{},"If you are building chat, the ",[623,676,678],{"href":677},"/docs/high-level-sdks","chat blueprint"," gives you rooms, who-is-online presence, typing indicators and streamed messages out of the box. It keeps an in-memory cache of the messages it has seen; message history is yours to store.",[26,681,682,683,618],{},"Compare the platforms in more detail: ",[623,684,686],{"href":685},"/compare/pusher","NoLag vs Pusher",[688,689,690],"style",{},"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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);}",{"title":136,"searchDepth":161,"depth":161,"links":692},[693,694,695,696,697,698,699],{"id":17,"depth":161,"text":18},{"id":47,"depth":161,"text":48},{"id":128,"depth":161,"text":129},{"id":303,"depth":161,"text":304},{"id":586,"depth":161,"text":587},{"id":630,"depth":161,"text":631},{"id":660,"depth":161,"text":661},"Migration Guide","2026-07-29","A practical guide to moving a realtime app from Pusher Channels to NoLag, with a concept map and before-and-after code for subscribing and publishing.",null,"md",{},"/blog/migrate-from-pusher-to-nolag","6 min read",{"title":5,"description":702},"blog/migrate-from-pusher-to-nolag","BM4Kg579K86xBAie0DLQF52NIXMoxVQ7cqUX5zijc3A",1789869417387]