[{"data":1,"prerenderedAt":1243},["ShallowReactive",2],{"navigation_docs_en":3,"-en-storage-mail-realtime":342,"-en-storage-mail-realtime-surround":1238},[4,45,61,96,132,166,196,217,241,265],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","\u002Fen\u002Fgetting-started","en\u002F1.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Coming from Laravel","\u002Fen\u002Fgetting-started\u002Fcoming-from-laravel","en\u002F1.getting-started\u002F2.coming-from-laravel","i-lucide-arrow-right-left",{"title":21,"path":22,"stem":23,"icon":19},"Coming from Symfony","\u002Fen\u002Fgetting-started\u002Fcoming-from-symfony","en\u002F1.getting-started\u002F3.coming-from-symfony",{"title":25,"path":26,"stem":27,"icon":28},"Installation","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F1.getting-started\u002F4.installation","i-lucide-download",{"title":30,"path":31,"stem":32,"icon":33},"Your first module","\u002Fen\u002Fgetting-started\u002Ffirst-module","en\u002F1.getting-started\u002F5.first-module","i-lucide-package-plus",{"title":35,"path":36,"stem":37,"icon":38},"Project structure","\u002Fen\u002Fgetting-started\u002Fproject-structure","en\u002F1.getting-started\u002F6.project-structure","i-lucide-folder-tree",{"title":40,"path":41,"stem":42,"icon":43},"The shop sample application","\u002Fen\u002Fgetting-started\u002Fsample-app","en\u002F1.getting-started\u002F7.sample-app","i-lucide-shopping-cart",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F10.reference",[51,56],{"title":52,"path":53,"stem":54,"icon":55},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F10.reference\u002F1.configuration","i-lucide-settings",{"title":57,"path":58,"stem":59,"icon":60},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F10.reference\u002F2.dependencies","i-lucide-package",{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F2.concepts",[67,72,77,82,86,91],{"title":68,"path":69,"stem":70,"icon":71},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F2.concepts\u002F1.architecture","i-lucide-layers",{"title":73,"path":74,"stem":75,"icon":76},"Application lifecycle","\u002Fen\u002Fconcepts\u002Fapplication-lifecycle","en\u002F2.concepts\u002F2.application-lifecycle","i-lucide-power",{"title":78,"path":79,"stem":80,"icon":81},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F2.concepts\u002F3.error-model","i-lucide-shield-alert",{"title":52,"path":83,"stem":84,"icon":85},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F2.concepts\u002F4.configuration","i-lucide-settings-2",{"title":87,"path":88,"stem":89,"icon":90},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F2.concepts\u002F5.codegen-pipeline","i-lucide-file-json",{"title":92,"path":93,"stem":94,"icon":95},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F2.concepts\u002F6.design-patterns","i-lucide-puzzle",{"title":97,"icon":98,"path":99,"stem":100,"children":101,"page":44},"HTTP & Routing","i-lucide-globe","\u002Fen\u002Fhttp","en\u002F3.http",[102,107,112,117,122,127],{"title":103,"path":104,"stem":105,"icon":106},"The server core","\u002Fen\u002Fhttp\u002Fserver","en\u002F3.http\u002F1.server","i-lucide-server",{"title":108,"path":109,"stem":110,"icon":111},"The Fiber engine","\u002Fen\u002Fhttp\u002Ffiber","en\u002F3.http\u002F2.fiber","i-lucide-zap",{"title":113,"path":114,"stem":115,"icon":116},"The chi engine","\u002Fen\u002Fhttp\u002Fchi","en\u002F3.http\u002F3.chi","i-lucide-route",{"title":118,"path":119,"stem":120,"icon":121},"Error responses","\u002Fen\u002Fhttp\u002Ferror-responses","en\u002F3.http\u002F4.error-responses","i-lucide-octagon-alert",{"title":123,"path":124,"stem":125,"icon":126},"Validation","\u002Fen\u002Fhttp\u002Fvalidation","en\u002F3.http\u002F5.validation","i-lucide-badge-check",{"title":128,"path":129,"stem":130,"icon":131},"Pagination","\u002Fen\u002Fhttp\u002Fpagination","en\u002F3.http\u002F6.pagination","i-lucide-list-ordered",{"title":133,"icon":134,"path":135,"stem":136,"children":137,"page":44},"Database","i-lucide-database","\u002Fen\u002Fdatabase","en\u002F4.database",[138,142,147,151,156,161],{"title":139,"path":140,"stem":141,"icon":134},"Overview","\u002Fen\u002Fdatabase\u002Foverview","en\u002F4.database\u002F1.overview",{"title":143,"path":144,"stem":145,"icon":146},"pgx","\u002Fen\u002Fdatabase\u002Fpgx","en\u002F4.database\u002F2.pgx","i-lucide-plug",{"title":148,"path":149,"stem":150,"icon":71},"bun","\u002Fen\u002Fdatabase\u002Fbun","en\u002F4.database\u002F3.bun",{"title":152,"path":153,"stem":154,"icon":155},"Transactions","\u002Fen\u002Fdatabase\u002Ftransactions","en\u002F4.database\u002F4.transactions","i-lucide-git-merge",{"title":157,"path":158,"stem":159,"icon":160},"Migrations","\u002Fen\u002Fdatabase\u002Fmigrations","en\u002F4.database\u002F5.migrations","i-lucide-file-stack",{"title":162,"path":163,"stem":164,"icon":165},"Seeders","\u002Fen\u002Fdatabase\u002Fseeding","en\u002F4.database\u002F6.seeding","i-lucide-sprout",{"title":167,"icon":168,"path":169,"stem":170,"children":171,"page":44},"Async & Background","i-lucide-workflow","\u002Fen\u002Fasync","en\u002F5.async",[172,176,181,186,191],{"title":173,"path":174,"stem":175,"icon":131},"Queue","\u002Fen\u002Fasync\u002Fqueue","en\u002F5.async\u002F1.queue",{"title":177,"path":178,"stem":179,"icon":180},"Events","\u002Fen\u002Fasync\u002Fevents","en\u002F5.async\u002F2.events","i-lucide-radio",{"title":182,"path":183,"stem":184,"icon":185},"Outbox","\u002Fen\u002Fasync\u002Foutbox","en\u002F5.async\u002F3.outbox","i-lucide-inbox",{"title":187,"path":188,"stem":189,"icon":190},"Scheduling","\u002Fen\u002Fasync\u002Fscheduler","en\u002F5.async\u002F4.scheduler","i-lucide-calendar-clock",{"title":192,"path":193,"stem":194,"icon":195},"Worker","\u002Fen\u002Fasync\u002Fworker","en\u002F5.async\u002F5.worker","i-lucide-cog",{"title":197,"icon":198,"path":199,"stem":200,"children":201,"page":44},"Security","i-lucide-shield-check","\u002Fen\u002Fsecurity","en\u002F6.security",[202,207,212],{"title":203,"path":204,"stem":205,"icon":206},"Authentication","\u002Fen\u002Fsecurity\u002Fauthentication","en\u002F6.security\u002F1.authentication","i-lucide-key-round",{"title":208,"path":209,"stem":210,"icon":211},"RBAC","\u002Fen\u002Fsecurity\u002Frbac","en\u002F6.security\u002F2.rbac","i-lucide-users",{"title":213,"path":214,"stem":215,"icon":216},"Policies","\u002Fen\u002Fsecurity\u002Fpolicies","en\u002F6.security\u002F3.policies","i-lucide-gavel",{"title":218,"icon":219,"path":220,"stem":221,"children":222,"page":44},"Observability","i-lucide-activity","\u002Fen\u002Fobservability","en\u002F7.observability",[223,226,231,236],{"title":139,"path":224,"stem":225,"icon":219},"\u002Fen\u002Fobservability\u002Foverview","en\u002F7.observability\u002F1.overview",{"title":227,"path":228,"stem":229,"icon":230},"Logging","\u002Fen\u002Fobservability\u002Flogging","en\u002F7.observability\u002F2.logging","i-lucide-scroll-text",{"title":232,"path":233,"stem":234,"icon":235},"OpenTelemetry","\u002Fen\u002Fobservability\u002Fopentelemetry","en\u002F7.observability\u002F3.opentelemetry","i-lucide-radar",{"title":237,"path":238,"stem":239,"icon":240},"Sentry","\u002Fen\u002Fobservability\u002Fsentry","en\u002F7.observability\u002F4.sentry","i-lucide-bug",{"title":242,"icon":243,"path":244,"stem":245,"children":246,"page":44},"Storage & Mail","i-lucide-hard-drive","\u002Fen\u002Fstorage-mail","en\u002F8.storage-mail",[247,251,256,261],{"title":248,"path":249,"stem":250,"icon":243},"Storage","\u002Fen\u002Fstorage-mail\u002Fstorage","en\u002F8.storage-mail\u002F1.storage",{"title":252,"path":253,"stem":254,"icon":255},"Email","\u002Fen\u002Fstorage-mail\u002Fmail","en\u002F8.storage-mail\u002F2.mail","i-lucide-mail",{"title":257,"path":258,"stem":259,"icon":260},"Notifications","\u002Fen\u002Fstorage-mail\u002Fnotifications","en\u002F8.storage-mail\u002F3.notifications","i-lucide-bell",{"title":262,"path":263,"stem":264,"icon":180},"Realtime","\u002Fen\u002Fstorage-mail\u002Frealtime","en\u002F8.storage-mail\u002F4.realtime",{"title":266,"icon":267,"path":268,"stem":269,"children":270,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F9.cli",[271,274,279,284,289,294,299,303,308,313,318,323,328,333,337],{"title":139,"path":272,"stem":273,"icon":267},"\u002Fen\u002Fcli\u002Foverview","en\u002F9.cli\u002F1.overview",{"title":275,"path":276,"stem":277,"icon":278},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F9.cli\u002F10.add-db","i-lucide-database-zap",{"title":280,"path":281,"stem":282,"icon":283},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F9.cli\u002F11.add-compose","i-lucide-container",{"title":285,"path":286,"stem":287,"icon":288},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F9.cli\u002F12.add-mail","i-lucide-mail-plus",{"title":290,"path":291,"stem":292,"icon":293},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F9.cli\u002F13.add-notification","i-lucide-bell-plus",{"title":295,"path":296,"stem":297,"icon":298},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F9.cli\u002F14.add-realtime","i-lucide-radio-tower",{"title":300,"path":301,"stem":302,"icon":165},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F9.cli\u002F15.add-seeder",{"title":304,"path":305,"stem":306,"icon":307},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F9.cli\u002F2.new-project","i-lucide-folder-plus",{"title":309,"path":310,"stem":311,"icon":312},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F9.cli\u002F3.new-module","i-lucide-blocks",{"title":314,"path":315,"stem":316,"icon":317},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F9.cli\u002F4.add-surface","i-lucide-layers-2",{"title":319,"path":320,"stem":321,"icon":322},"add core","\u002Fen\u002Fcli\u002Fadd-core","en\u002F9.cli\u002F5.add-core","i-lucide-box",{"title":324,"path":325,"stem":326,"icon":327},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F9.cli\u002F6.add-handler","i-lucide-webhook",{"title":329,"path":330,"stem":331,"icon":332},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F9.cli\u002F7.new-migration","i-lucide-file-plus",{"title":334,"path":335,"stem":336,"icon":195},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F9.cli\u002F8.worker-generators",{"title":338,"path":339,"stem":340,"icon":341},"upgrade templates","\u002Fen\u002Fcli\u002Fupgrade-templates","en\u002F9.cli\u002F9.upgrade-templates","i-lucide-refresh-cw",{"id":343,"title":262,"body":344,"description":1231,"extension":1232,"links":1233,"meta":1234,"navigation":1235,"path":263,"seo":1236,"stem":264,"__hash__":1237},"docs_en\u002Fen\u002F8.storage-mail\u002F4.realtime.md",{"type":345,"value":346,"toc":1223},"minimark",[347,362,367,388,401,405,593,607,611,632,635,826,832,836,839,886,890,1022,1047,1051,1206,1219],[348,349,350,351,356,357,361],"p",{},"The ",[352,353,355],"a",{"href":354},"\u002Fen\u002Fstorage-mail\u002Fnotifications#broadcast","broadcast channel"," delivers a notification to a recipient's live connection, the moment it happens: a \"poke\" on top of the durable ",[352,358,360],{"href":359},"\u002Fen\u002Fstorage-mail\u002Fnotifications#database","database channel",", not a replacement for it. This page covers the gateway that holds those connections.",[363,364,366],"h2",{"id":365},"why-a-separate-gateway","Why a separate gateway",[348,368,369,370,374,375,378,379,382,383,387],{},"Sending notifications happens from any process, often the worker, in a listener. Holding client connections is a different job with a different scaling shape: many long-lived streams, one per connected user, that must survive instances coming and going freely. The kit keeps these apart: ",[371,372,373],"code",{},"notify\u002Fbroadcast"," is a tiny publisher any process imports (no server, no held state); ",[371,376,377],{},"realtime"," is a dedicated binary (",[371,380,381],{},"cmd\u002Frealtime",", scaffolded by ",[352,384,385],{"href":296},[371,386,295],{},") that only holds connections and relays what gets published.",[348,389,390,391,393,394,396,397,400],{},"The driver seam is deliberate: nothing outside ",[371,392,373],{}," and ",[371,395,377],{}," imports ",[371,398,399],{},"centrifuge"," directly, so a different transport can replace it later without touching application code.",[363,402,404],{"id":403},"publishing","Publishing",[406,407,412],"pre",{"className":408,"code":409,"language":410,"meta":411,"style":411},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import \"github.com\u002Fgp-system\u002Fgpsystem\u002Fnotify\u002Fbroadcast\"\n\npub := broadcast.MustNewPublisher(ctx, cfg.Realtime.Redis, cfg.Realtime.Broadcast)\nnotifier := notify.NewHub(\n    notify.NewMailChannel(mailer),\n    notifydatabase.NewChannel(store),\n    notify.NewBroadcastChannel(pub, cfg.Realtime.Broadcast.ChannelPrefix),\n)\n","go","",[371,413,414,434,441,498,517,536,554,588],{"__ignoreMap":411},[415,416,419,423,427,431],"span",{"class":417,"line":418},"line",1,[415,420,422],{"class":421},"s7zQu","import",[415,424,426],{"class":425},"sMK4o"," \"",[415,428,430],{"class":429},"sBMFI","github.com\u002Fgp-system\u002Fgpsystem\u002Fnotify\u002Fbroadcast",[415,432,433],{"class":425},"\"\n",[415,435,437],{"class":417,"line":436},2,[415,438,440],{"emptyLinePlaceholder":439},true,"\n",[415,442,444,448,451,454,457,461,464,467,470,473,475,477,479,482,484,486,488,490,492,495],{"class":417,"line":443},3,[415,445,447],{"class":446},"sTEyZ","pub ",[415,449,450],{"class":425},":=",[415,452,453],{"class":446}," broadcast",[415,455,456],{"class":425},".",[415,458,460],{"class":459},"s2Zo4","MustNewPublisher",[415,462,463],{"class":425},"(",[415,465,466],{"class":446},"ctx",[415,468,469],{"class":425},",",[415,471,472],{"class":446}," cfg",[415,474,456],{"class":425},[415,476,262],{"class":446},[415,478,456],{"class":425},[415,480,481],{"class":446},"Redis",[415,483,469],{"class":425},[415,485,472],{"class":446},[415,487,456],{"class":425},[415,489,262],{"class":446},[415,491,456],{"class":425},[415,493,494],{"class":446},"Broadcast",[415,496,497],{"class":425},")\n",[415,499,501,504,506,509,511,514],{"class":417,"line":500},4,[415,502,503],{"class":446},"notifier ",[415,505,450],{"class":425},[415,507,508],{"class":446}," notify",[415,510,456],{"class":425},[415,512,513],{"class":459},"NewHub",[415,515,516],{"class":425},"(\n",[415,518,520,523,525,528,530,533],{"class":417,"line":519},5,[415,521,522],{"class":446},"    notify",[415,524,456],{"class":425},[415,526,527],{"class":459},"NewMailChannel",[415,529,463],{"class":425},[415,531,532],{"class":446},"mailer",[415,534,535],{"class":425},"),\n",[415,537,539,542,544,547,549,552],{"class":417,"line":538},6,[415,540,541],{"class":446},"    notifydatabase",[415,543,456],{"class":425},[415,545,546],{"class":459},"NewChannel",[415,548,463],{"class":425},[415,550,551],{"class":446},"store",[415,553,535],{"class":425},[415,555,557,559,561,564,566,569,571,573,575,577,579,581,583,586],{"class":417,"line":556},7,[415,558,522],{"class":446},[415,560,456],{"class":425},[415,562,563],{"class":459},"NewBroadcastChannel",[415,565,463],{"class":425},[415,567,568],{"class":446},"pub",[415,570,469],{"class":425},[415,572,472],{"class":446},[415,574,456],{"class":425},[415,576,262],{"class":446},[415,578,456],{"class":425},[415,580,494],{"class":446},[415,582,456],{"class":425},[415,584,585],{"class":446},"ChannelPrefix",[415,587,535],{"class":425},[415,589,591],{"class":417,"line":590},8,[415,592,497],{"class":425},[348,594,595,598,599,602,603,606],{},[371,596,597],{},"Publisher"," is a publish-only ",[371,600,601],{},"centrifuge.Node"," sharing the project's Redis broker; it never accepts a connection, so a worker process depends on nothing but a network client. Publishing is fire-and-forget: a failed publish is returned to the caller, not retried. Durability lives in the database channel, and a client that misses a live push backfills from it on reconnect. Insert-then-publish is the contract ",[371,604,605],{},"notify.Hub.Send"," already enforces (database channel before broadcast, same delivery ID in both).",[363,608,610],{"id":609},"the-gateway","The gateway",[348,612,613,615,616,619,620,623,624,627,628,631],{},[371,614,381],{}," holds no business dependencies (no database pool, no outbox), which is exactly why it scales freely. It exposes WebSocket plus an HTTP-streaming\u002FSSE fallback (all through ",[371,617,618],{},"centrifugal\u002Fcentrifuge",", the library behind Centrifugo and Grafana Live) under ",[371,621,622],{},"\u002Frealtime\u002F*",", authenticates each connection's JWT in the connect frame (no separate ",[371,625,626],{},"\u002Fbroadcasting\u002Fauth"," endpoint; v1's only private channel is a connection's own subject, so authenticating once at connect is sufficient), and server-side subscribes every connection to its own ",[371,629,630],{},"user:\u003Csubject>"," channel. No client-side subscribe logic needed for the common case.",[348,633,634],{},"Shared topic channels are opt-in and explicitly authorized:",[406,636,639],{"className":408,"code":637,"filename":638,"language":410,"meta":411,"style":411},"err := realtime.Run(ctx, cfg.Realtime, func(topics *realtime.TopicAuth) error {\n    topics.Allow(\"announcements\", func(id *rbac.Identity) bool { return true })\n    topics.Allow(\"admin-alerts\", func(id *rbac.Identity) bool { return id.HasRole(\"admin\") })\n    return nil\n})\n","cmd\u002Frealtime\u002Fmain.go",[371,640,641,697,753,813,821],{"__ignoreMap":411},[415,642,643,646,648,651,653,656,658,660,662,664,666,668,670,673,677,680,682,684,687,690,694],{"class":417,"line":418},[415,644,645],{"class":446},"err ",[415,647,450],{"class":425},[415,649,650],{"class":446}," realtime",[415,652,456],{"class":425},[415,654,655],{"class":459},"Run",[415,657,463],{"class":425},[415,659,466],{"class":446},[415,661,469],{"class":425},[415,663,472],{"class":446},[415,665,456],{"class":425},[415,667,262],{"class":446},[415,669,469],{"class":425},[415,671,672],{"class":425}," func(",[415,674,676],{"class":675},"sHdIc","topics",[415,678,679],{"class":425}," *",[415,681,377],{"class":429},[415,683,456],{"class":425},[415,685,686],{"class":429},"TopicAuth",[415,688,689],{"class":425},")",[415,691,693],{"class":692},"spNyl"," error",[415,695,696],{"class":425}," {\n",[415,698,699,702,704,707,709,712,716,718,720,722,725,727,730,732,735,737,740,743,746,750],{"class":417,"line":436},[415,700,701],{"class":446},"    topics",[415,703,456],{"class":425},[415,705,706],{"class":459},"Allow",[415,708,463],{"class":425},[415,710,711],{"class":425},"\"",[415,713,715],{"class":714},"sfazB","announcements",[415,717,711],{"class":425},[415,719,469],{"class":425},[415,721,672],{"class":425},[415,723,724],{"class":675},"id",[415,726,679],{"class":425},[415,728,729],{"class":429},"rbac",[415,731,456],{"class":425},[415,733,734],{"class":429},"Identity",[415,736,689],{"class":425},[415,738,739],{"class":692}," bool",[415,741,742],{"class":425}," {",[415,744,745],{"class":421}," return",[415,747,749],{"class":748},"sfNiH"," true",[415,751,752],{"class":425}," })\n",[415,754,755,757,759,761,763,765,768,770,772,774,776,778,780,782,784,786,788,790,792,795,797,800,802,804,807,809,811],{"class":417,"line":443},[415,756,701],{"class":446},[415,758,456],{"class":425},[415,760,706],{"class":459},[415,762,463],{"class":425},[415,764,711],{"class":425},[415,766,767],{"class":714},"admin-alerts",[415,769,711],{"class":425},[415,771,469],{"class":425},[415,773,672],{"class":425},[415,775,724],{"class":675},[415,777,679],{"class":425},[415,779,729],{"class":429},[415,781,456],{"class":425},[415,783,734],{"class":429},[415,785,689],{"class":425},[415,787,739],{"class":692},[415,789,742],{"class":425},[415,791,745],{"class":421},[415,793,794],{"class":446}," id",[415,796,456],{"class":425},[415,798,799],{"class":459},"HasRole",[415,801,463],{"class":425},[415,803,711],{"class":425},[415,805,806],{"class":714},"admin",[415,808,711],{"class":425},[415,810,689],{"class":425},[415,812,752],{"class":425},[415,814,815,818],{"class":417,"line":500},[415,816,817],{"class":421},"    return",[415,819,820],{"class":425}," nil\n",[415,822,823],{"class":417,"line":519},[415,824,825],{"class":425},"})\n",[348,827,828,829,831],{},"A topic with no registered ",[371,830,706],{}," callback is never subscribable.",[363,833,835],{"id":834},"scaling-instances-come-and-go-freely","Scaling: instances come and go freely",[348,837,838],{},"Every gateway instance is stateless: it only ever holds the channels its own connections need, and the shared Redis broker fans a publish out to every instance that needs it, from any process, without a sticky-session requirement.",[840,841,842,854,871,877],"ul",{},[843,844,845,849,850,853],"li",{},[846,847,848],"strong",{},"Scale up",": ",[371,851,852],{},"docker compose up -d --scale realtime=N","; a new instance starts with zero state and accumulates subscriptions only as clients land on it.",[843,855,856,849,859,862,863,866,867,870],{},[846,857,858],{},"Scale down \u002F rolling restart",[371,860,861],{},"SIGTERM"," flips a draining flag (",[371,864,865],{},"\u002Frealtime\u002Fhealthz"," and new connect attempts return 503, so the load balancer's health check evicts the instance), ",[371,868,869],{},"node.Shutdown"," disconnects every held connection with a reconnect-eligible code, closers run, telemetry flushes. The client's own reconnect logic (backoff, built into the JS client) lands it on a surviving instance, which subscribes it fresh; anything published during the gap is recovered from the database channel, not replayed by the gateway.",[843,872,873,876],{},[846,874,875],{},"Crash",": the client side is identical to a graceful restart, just without the drain step.",[843,878,879,882,883,885],{},[846,880,881],{},"Redis restart",": reconnect and resubscribe are ",[371,884,399],{},"'s job; publishing during the outage is logged and dropped (the same fire-and-forget contract), recovered the same way as a missed publish during a client's own reconnect.",[363,887,889],{"id":888},"config","Config",[891,892,893,909],"table",{},[894,895,896],"thead",{},[897,898,899,903,906],"tr",{},[900,901,902],"th",{},"Variable",[900,904,905],{},"Default",[900,907,908],{},"Meaning",[910,911,912,928,943,958,974,990,1007],"tbody",{},[897,913,914,920,925],{},[915,916,917],"td",{},[371,918,919],{},"REALTIME_LISTEN_ADDR",[915,921,922],{},[371,923,924],{},":3000",[915,926,927],{},"container-internal listen address",[897,929,930,935,940],{},[915,931,932],{},[371,933,934],{},"REALTIME_DRAIN_TIMEOUT",[915,936,937],{},[371,938,939],{},"20s",[915,941,942],{},"graceful-shutdown budget; keep under the deployment's stop-grace-period",[897,944,945,950,955],{},[915,946,947],{},[371,948,949],{},"REALTIME_READ_HEADER_TIMEOUT",[915,951,952],{},[371,953,954],{},"5s",[915,956,957],{},"Slowloris defense on the connect handshake",[897,959,960,965,968],{},[915,961,962],{},[371,963,964],{},"REALTIME_CHANNEL_PREFIX",[915,966,967],{},"the project name",[915,969,970,971],{},"Redis channel namespace shared with every ",[371,972,973],{},"notify\u002Fbroadcast.Publisher",[897,975,976,981,987],{},[915,977,978],{},[371,979,980],{},"REALTIME_MAX_CONNECTIONS",[915,982,983,986],{},[371,984,985],{},"0"," (unlimited)",[915,988,989],{},"per-instance stream cap; over it, connect attempts get 503",[897,991,992,997,1000],{},[915,993,994],{},[371,995,996],{},"REDIS_*",[915,998,999],{},"inherited",[915,1001,1002,1003,1006],{},"reused from ",[371,1004,1005],{},"queue.Config",": the gateway's broker is the same Redis the queue uses",[897,1008,1009,1014,1016],{},[915,1010,1011],{},[371,1012,1013],{},"JWT_*",[915,1015,999],{},[915,1017,1002,1018,1021],{},[371,1019,1020],{},"auth.Config",": the same token issuer every module trusts",[348,1023,1024,1025,1030,1031,1034,1035,1038,1039,1043,1044,456],{},"See the ",[352,1026,1027,1029],{"href":296},[371,1028,295],{}," reference"," for what gets scaffolded (the binary, ",[371,1032,1033],{},"compose.yml","'s Traefik-routed service, ",[371,1036,1037],{},".env.example",") and the ",[352,1040,1041],{"href":291},[371,1042,290],{}," page for wiring the broadcast channel into a notification's ",[371,1045,1046],{},"Via",[363,1048,1050],{"id":1049},"client","Client",[406,1052,1056],{"className":1053,"code":1054,"language":1055,"meta":411,"style":411},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { Centrifuge } from 'centrifuge'\n\nconst client = new Centrifuge('wss:\u002F\u002Fyour-app.example\u002Frealtime\u002Fconnection\u002Fwebsocket', {\n    getToken: async () => fetchAccessToken(), \u002F\u002F the same JWT the API uses\n})\nclient.on('publication', (ctx) => {\n    \u002F\u002F ctx.data: { id, name, payload, at }, id ties back to the database channel's row\n})\nclient.connect()\n","js",[371,1057,1058,1081,1085,1115,1144,1151,1182,1187,1193],{"__ignoreMap":411},[415,1059,1060,1062,1064,1067,1070,1073,1076,1078],{"class":417,"line":418},[415,1061,422],{"class":421},[415,1063,742],{"class":425},[415,1065,1066],{"class":446}," Centrifuge",[415,1068,1069],{"class":425}," }",[415,1071,1072],{"class":421}," from",[415,1074,1075],{"class":425}," '",[415,1077,399],{"class":714},[415,1079,1080],{"class":425},"'\n",[415,1082,1083],{"class":417,"line":436},[415,1084,440],{"emptyLinePlaceholder":439},[415,1086,1087,1090,1093,1096,1099,1101,1103,1106,1109,1111,1113],{"class":417,"line":443},[415,1088,1089],{"class":692},"const",[415,1091,1092],{"class":446}," client ",[415,1094,1095],{"class":425},"=",[415,1097,1098],{"class":425}," new",[415,1100,1066],{"class":459},[415,1102,463],{"class":446},[415,1104,1105],{"class":425},"'",[415,1107,1108],{"class":714},"wss:\u002F\u002Fyour-app.example\u002Frealtime\u002Fconnection\u002Fwebsocket",[415,1110,1105],{"class":425},[415,1112,469],{"class":425},[415,1114,696],{"class":425},[415,1116,1117,1120,1123,1126,1129,1132,1135,1138,1140],{"class":417,"line":500},[415,1118,1119],{"class":459},"    getToken",[415,1121,1122],{"class":425},":",[415,1124,1125],{"class":692}," async",[415,1127,1128],{"class":425}," ()",[415,1130,1131],{"class":692}," =>",[415,1133,1134],{"class":459}," fetchAccessToken",[415,1136,1137],{"class":446},"()",[415,1139,469],{"class":425},[415,1141,1143],{"class":1142},"sHwdD"," \u002F\u002F the same JWT the API uses\n",[415,1145,1146,1149],{"class":417,"line":519},[415,1147,1148],{"class":425},"}",[415,1150,497],{"class":446},[415,1152,1153,1155,1157,1160,1162,1164,1167,1169,1171,1174,1176,1178,1180],{"class":417,"line":538},[415,1154,1049],{"class":446},[415,1156,456],{"class":425},[415,1158,1159],{"class":459},"on",[415,1161,463],{"class":446},[415,1163,1105],{"class":425},[415,1165,1166],{"class":714},"publication",[415,1168,1105],{"class":425},[415,1170,469],{"class":425},[415,1172,1173],{"class":425}," (",[415,1175,466],{"class":675},[415,1177,689],{"class":425},[415,1179,1131],{"class":692},[415,1181,696],{"class":425},[415,1183,1184],{"class":417,"line":556},[415,1185,1186],{"class":1142},"    \u002F\u002F ctx.data: { id, name, payload, at }, id ties back to the database channel's row\n",[415,1188,1189,1191],{"class":417,"line":590},[415,1190,1148],{"class":425},[415,1192,497],{"class":446},[415,1194,1196,1198,1200,1203],{"class":417,"line":1195},9,[415,1197,1049],{"class":446},[415,1199,456],{"class":425},[415,1201,1202],{"class":459},"connect",[415,1204,1205],{"class":446},"()\n",[348,1207,1208,1211,1212,1215,1216,1218],{},[371,1209,1210],{},"centrifuge-js"," handles reconnect backoff and token refresh; the server-side subscription to the recipient's own channel means the client needs no explicit ",[371,1213,1214],{},"subscribe"," call for notifications addressed to it. On reconnect, fetch unread notifications from your module's own inbox endpoint (built on the ",[352,1217,360],{"href":359},") to backfill anything missed while disconnected. The socket is a live poke, the database is the source of truth.",[1220,1221,1222],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":411,"searchDepth":436,"depth":436,"links":1224},[1225,1226,1227,1228,1229,1230],{"id":365,"depth":436,"text":366},{"id":403,"depth":436,"text":404},{"id":609,"depth":436,"text":610},{"id":834,"depth":436,"text":835},{"id":888,"depth":436,"text":889},{"id":1049,"depth":436,"text":1050},"A dedicated, horizontally scalable gateway that pushes notifications live to connected clients: server→client only, built on centrifuge.","md",null,{},{"icon":180},{"title":262,"description":1231},"ykXk5g09YuGz0Pa4S5zeGwSm28KX7lJmRc4ksxnrOYw",[1239,1241],{"title":257,"path":258,"stem":259,"description":1240,"icon":260,"children":-1},"Multi-channel notifications: one payload, several channels (database, mail, broadcast), driven by a shared Hub.",{"title":139,"path":272,"stem":273,"description":1242,"icon":267,"children":-1},"How the gpsystem CLI works: tool directive, safety rules, anchors, manifest.",1784668710920]