[{"data":1,"prerenderedAt":2093},["ShallowReactive",2],{"navigation_docs_en":3,"-en-framework-realtime":448,"-en-framework-realtime-surround":2088},[4,45,120,157,173,203,233,259,271,287,307,326,339,356,373,386,398,425],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","\u002Fen\u002Fgetting-started","en\u002F01.getting-started",[10,15,20,25,30,35,40],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F01.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Installation","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F01.getting-started\u002F2.installation","i-lucide-download",{"title":21,"path":22,"stem":23,"icon":24},"Your first module","\u002Fen\u002Fgetting-started\u002Ffirst-module","en\u002F01.getting-started\u002F3.first-module","i-lucide-package-plus",{"title":26,"path":27,"stem":28,"icon":29},"Project structure","\u002Fen\u002Fgetting-started\u002Fproject-structure","en\u002F01.getting-started\u002F4.project-structure","i-lucide-folder-tree",{"title":31,"path":32,"stem":33,"icon":34},"The shop sample application","\u002Fen\u002Fgetting-started\u002Fsample-app","en\u002F01.getting-started\u002F5.sample-app","i-lucide-shopping-cart",{"title":36,"path":37,"stem":38,"icon":39},"Coming from Laravel","\u002Fen\u002Fgetting-started\u002Fcoming-from-laravel","en\u002F01.getting-started\u002F6.coming-from-laravel","i-lucide-arrow-right-left",{"title":41,"path":42,"stem":43,"icon":39},"Coming from Symfony","\u002Fen\u002Fgetting-started\u002Fcoming-from-symfony","en\u002F01.getting-started\u002F7.coming-from-symfony",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F02.cli",[51,55,60,65,70,75,80,85,90,95,100,105,110,115],{"title":52,"path":53,"stem":54,"icon":47},"Overview","\u002Fen\u002Fcli\u002Foverview","en\u002F02.cli\u002F1.overview",{"title":56,"path":57,"stem":58,"icon":59},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F02.cli\u002F10.add-compose","i-lucide-container",{"title":61,"path":62,"stem":63,"icon":64},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F02.cli\u002F11.add-mail","i-lucide-mail-plus",{"title":66,"path":67,"stem":68,"icon":69},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F02.cli\u002F12.add-notification","i-lucide-bell-plus",{"title":71,"path":72,"stem":73,"icon":74},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F02.cli\u002F13.add-realtime","i-lucide-radio-tower",{"title":76,"path":77,"stem":78,"icon":79},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F02.cli\u002F14.add-seeder","i-lucide-sprout",{"title":81,"path":82,"stem":83,"icon":84},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F02.cli\u002F2.new-project","i-lucide-folder-plus",{"title":86,"path":87,"stem":88,"icon":89},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F02.cli\u002F3.new-module","i-lucide-blocks",{"title":91,"path":92,"stem":93,"icon":94},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F02.cli\u002F4.add-surface","i-lucide-layers-2",{"title":96,"path":97,"stem":98,"icon":99},"add domain","\u002Fen\u002Fcli\u002Fadd-domain","en\u002F02.cli\u002F5.add-domain","i-lucide-box",{"title":101,"path":102,"stem":103,"icon":104},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F02.cli\u002F6.add-handler","i-lucide-webhook",{"title":106,"path":107,"stem":108,"icon":109},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F02.cli\u002F7.new-migration","i-lucide-file-plus",{"title":111,"path":112,"stem":113,"icon":114},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F02.cli\u002F8.worker-generators","i-lucide-cog",{"title":116,"path":117,"stem":118,"icon":119},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F02.cli\u002F9.add-db","i-lucide-database-zap",{"title":121,"icon":99,"path":122,"stem":123,"children":124,"page":44},"Framework","\u002Fen\u002Fframework","en\u002F03.framework",[125,128,133,138,142,147,152],{"title":52,"path":126,"stem":127,"icon":99},"\u002Fen\u002Fframework\u002Foverview","en\u002F03.framework\u002F1.overview",{"title":129,"path":130,"stem":131,"icon":132},"Application lifecycle","\u002Fen\u002Fframework\u002Fapp","en\u002F03.framework\u002F2.app","i-lucide-power",{"title":134,"path":135,"stem":136,"icon":137},"Server","\u002Fen\u002Fframework\u002Fserver","en\u002F03.framework\u002F3.server","i-lucide-server",{"title":139,"path":140,"stem":141,"icon":114},"Worker","\u002Fen\u002Fframework\u002Fworker","en\u002F03.framework\u002F4.worker",{"title":143,"path":144,"stem":145,"icon":146},"Realtime","\u002Fen\u002Fframework\u002Frealtime","en\u002F03.framework\u002F5.realtime","i-lucide-radio",{"title":148,"path":149,"stem":150,"icon":151},"Migrations","\u002Fen\u002Fframework\u002Fmigrations","en\u002F03.framework\u002F6.migrations","i-lucide-file-stack",{"title":153,"path":154,"stem":155,"icon":156},"Social login","\u002Fen\u002Fframework\u002Fsocial-login","en\u002F03.framework\u002F7.social-login","i-lucide-log-in",{"title":158,"icon":159,"path":160,"stem":161,"children":162,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F04.reference",[163,168],{"title":164,"path":165,"stem":166,"icon":167},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F04.reference\u002F1.configuration","i-lucide-settings",{"title":169,"path":170,"stem":171,"icon":172},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F04.reference\u002F2.dependencies","i-lucide-package",{"title":174,"icon":175,"path":176,"stem":177,"children":178,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F05.concepts",[179,184,189,193,198],{"title":180,"path":181,"stem":182,"icon":183},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F05.concepts\u002F1.architecture","i-lucide-layers",{"title":185,"path":186,"stem":187,"icon":188},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F05.concepts\u002F2.error-model","i-lucide-shield-alert",{"title":164,"path":190,"stem":191,"icon":192},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F05.concepts\u002F3.configuration","i-lucide-settings-2",{"title":194,"path":195,"stem":196,"icon":197},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F05.concepts\u002F4.codegen-pipeline","i-lucide-file-json",{"title":199,"path":200,"stem":201,"icon":202},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F05.concepts\u002F5.design-patterns","i-lucide-puzzle",{"title":204,"icon":205,"path":206,"stem":207,"children":208,"page":44},"auth","i-lucide-shield-check","\u002Fen\u002Fauth","en\u002F06.auth",[209,214,219,224,229],{"title":210,"path":211,"stem":212,"icon":213},"Authentication","\u002Fen\u002Fauth\u002Foverview","en\u002F06.auth\u002F1.overview","i-lucide-key-round",{"title":215,"path":216,"stem":217,"icon":218},"RBAC","\u002Fen\u002Fauth\u002Frbac","en\u002F06.auth\u002F2.rbac","i-lucide-users",{"title":220,"path":221,"stem":222,"icon":223},"Policies","\u002Fen\u002Fauth\u002Fpolicy","en\u002F06.auth\u002F3.policy","i-lucide-gavel",{"title":225,"path":226,"stem":227,"icon":228},"Using auth standalone","\u002Fen\u002Fauth\u002Fstandalone","en\u002F06.auth\u002F4.standalone","i-lucide-plug",{"title":230,"path":231,"stem":232,"icon":156},"Social login with goth, standalone","\u002Fen\u002Fauth\u002Fsocial-standalone","en\u002F06.auth\u002F5.social-standalone",{"title":234,"icon":235,"path":236,"stem":237,"children":238,"page":44},"dbx","i-lucide-database","\u002Fen\u002Fdbx","en\u002F07.dbx",[239,242,246,250,255],{"title":52,"path":240,"stem":241,"icon":235},"\u002Fen\u002Fdbx\u002Foverview","en\u002F07.dbx\u002F1.overview",{"title":243,"path":244,"stem":245,"icon":228},"pgx","\u002Fen\u002Fdbx\u002Fpg","en\u002F07.dbx\u002F2.pg",{"title":247,"path":248,"stem":249,"icon":183},"bun","\u002Fen\u002Fdbx\u002Fbunx","en\u002F07.dbx\u002F3.bunx",{"title":251,"path":252,"stem":253,"icon":254},"Transactions","\u002Fen\u002Fdbx\u002Ftransactions","en\u002F07.dbx\u002F4.transactions","i-lucide-git-merge",{"title":256,"path":257,"stem":258,"icon":79},"Seeders","\u002Fen\u002Fdbx\u002Fseed","en\u002F07.dbx\u002F5.seed",{"title":260,"icon":192,"path":261,"stem":262,"children":263,"page":44},"envconf","\u002Fen\u002Fenvconf","en\u002F08.envconf",[264,267],{"title":52,"path":265,"stem":266,"icon":192},"\u002Fen\u002Fenvconf\u002Foverview","en\u002F08.envconf\u002F1.overview",{"title":268,"path":269,"stem":270,"icon":192},"Recipes","\u002Fen\u002Fenvconf\u002Frecipes","en\u002F08.envconf\u002F2.recipes",{"title":272,"icon":188,"path":273,"stem":274,"children":275,"page":44},"errs","\u002Fen\u002Ferrs","en\u002F09.errs",[276,279,283],{"title":52,"path":277,"stem":278,"icon":188},"\u002Fen\u002Ferrs\u002Foverview","en\u002F09.errs\u002F1.overview",{"title":280,"path":281,"stem":282,"icon":188},"API","\u002Fen\u002Ferrs\u002Fapi","en\u002F09.errs\u002F2.api",{"title":284,"path":285,"stem":286,"icon":188},"Integration","\u002Fen\u002Ferrs\u002Fintegration","en\u002F09.errs\u002F3.integration",{"title":288,"icon":289,"path":290,"stem":291,"children":292,"page":44},"events","i-lucide-workflow","\u002Fen\u002Fevents","en\u002F10.events",[293,297,302],{"title":294,"path":295,"stem":296,"icon":146},"Events","\u002Fen\u002Fevents\u002Foverview","en\u002F10.events\u002F1.overview",{"title":298,"path":299,"stem":300,"icon":301},"Outbox","\u002Fen\u002Fevents\u002Foutbox","en\u002F10.events\u002F2.outbox","i-lucide-inbox",{"title":303,"path":304,"stem":305,"icon":306},"Scheduling","\u002Fen\u002Fevents\u002Fscheduler","en\u002F10.events\u002F3.scheduler","i-lucide-calendar-clock",{"title":308,"icon":309,"path":310,"stem":311,"children":312,"page":44},"httperr","i-lucide-globe","\u002Fen\u002Fhttperr","en\u002F11.httperr",[313,316,321],{"title":52,"path":314,"stem":315,"icon":309},"\u002Fen\u002Fhttperr\u002Foverview","en\u002F11.httperr\u002F1.overview",{"title":317,"path":318,"stem":319,"icon":320},"Error responses","\u002Fen\u002Fhttperr\u002Ferror-responses","en\u002F11.httperr\u002F2.error-responses","i-lucide-octagon-alert",{"title":322,"path":323,"stem":324,"icon":325},"Validation","\u002Fen\u002Fhttperr\u002Fvalidation","en\u002F11.httperr\u002F3.validation","i-lucide-badge-check",{"title":327,"icon":328,"path":329,"stem":330,"children":331,"page":44},"logx","i-lucide-scroll-text","\u002Fen\u002Flogx","en\u002F12.logx",[332,335],{"title":52,"path":333,"stem":334,"icon":328},"\u002Fen\u002Flogx\u002Foverview","en\u002F12.logx\u002F1.overview",{"title":336,"path":337,"stem":338,"icon":47},"Monolog formatter","\u002Fen\u002Flogx\u002Fmonolog","en\u002F12.logx\u002F2.monolog",{"title":340,"icon":341,"path":342,"stem":343,"children":344,"page":44},"mail","i-lucide-mail","\u002Fen\u002Fmail","en\u002F13.mail",[345,348,352],{"title":52,"path":346,"stem":347,"icon":341},"\u002Fen\u002Fmail\u002Foverview","en\u002F13.mail\u002F1.overview",{"title":349,"path":350,"stem":351,"icon":341},"SMTP","\u002Fen\u002Fmail\u002Fsmtp","en\u002F13.mail\u002F2.smtp",{"title":353,"path":354,"stem":355,"icon":341},"MJML","\u002Fen\u002Fmail\u002Fmjml","en\u002F13.mail\u002F3.mjml",{"title":357,"icon":358,"path":359,"stem":360,"children":361,"page":44},"notify","i-lucide-bell","\u002Fen\u002Fnotify","en\u002F14.notify",[362,365,369],{"title":52,"path":363,"stem":364,"icon":358},"\u002Fen\u002Fnotify\u002Foverview","en\u002F14.notify\u002F1.overview",{"title":366,"path":367,"stem":368,"icon":358},"Database channel","\u002Fen\u002Fnotify\u002Fdatabase","en\u002F14.notify\u002F2.database",{"title":370,"path":371,"stem":372,"icon":358},"Broadcast channel","\u002Fen\u002Fnotify\u002Fbroadcast","en\u002F14.notify\u002F3.broadcast",{"title":374,"icon":375,"path":376,"stem":377,"children":378,"page":44},"paginate","i-lucide-list-ordered","\u002Fen\u002Fpaginate","en\u002F15.paginate",[379,382],{"title":52,"path":380,"stem":381,"icon":375},"\u002Fen\u002Fpaginate\u002Foverview","en\u002F15.paginate\u002F1.overview",{"title":383,"path":384,"stem":385,"icon":39},"Cursor pagination","\u002Fen\u002Fpaginate\u002Fcursor","en\u002F15.paginate\u002F2.cursor",{"title":387,"icon":183,"path":388,"stem":389,"children":390,"page":44},"queue","\u002Fen\u002Fqueue","en\u002F16.queue",[391,394],{"title":52,"path":392,"stem":393,"icon":375},"\u002Fen\u002Fqueue\u002Foverview","en\u002F16.queue\u002F1.overview",{"title":395,"path":396,"stem":397,"icon":183},"Tasks","\u002Fen\u002Fqueue\u002Ftasks","en\u002F16.queue\u002F2.tasks",{"title":399,"icon":400,"path":401,"stem":402,"children":403,"page":44},"storage","i-lucide-hard-drive","\u002Fen\u002Fstorage","en\u002F17.storage",[404,407,411,415,420],{"title":52,"path":405,"stem":406,"icon":400},"\u002Fen\u002Fstorage\u002Foverview","en\u002F17.storage\u002F1.overview",{"title":408,"path":409,"stem":410,"icon":400},"Disk API","\u002Fen\u002Fstorage\u002Fdisk-api","en\u002F17.storage\u002F2.disk-api",{"title":412,"path":413,"stem":414,"icon":400},"Memory and local disk","\u002Fen\u002Fstorage\u002Flocal","en\u002F17.storage\u002F3.local",{"title":416,"path":417,"stem":418,"icon":419},"S3","\u002Fen\u002Fstorage\u002Fs3","en\u002F17.storage\u002F4.s3","i-lucide-cloud",{"title":421,"path":422,"stem":423,"icon":424},"Testing","\u002Fen\u002Fstorage\u002Ftesting","en\u002F17.storage\u002F5.testing","i-lucide-flask-conical",{"title":426,"icon":427,"path":428,"stem":429,"children":430,"page":44},"telemetry","i-lucide-activity","\u002Fen\u002Ftelemetry","en\u002F18.telemetry",[431,434,438,443],{"title":52,"path":432,"stem":433,"icon":427},"\u002Fen\u002Ftelemetry\u002Foverview","en\u002F18.telemetry\u002F1.overview",{"title":435,"path":436,"stem":437,"icon":328},"Logging","\u002Fen\u002Ftelemetry\u002Flogging","en\u002F18.telemetry\u002F2.logging",{"title":439,"path":440,"stem":441,"icon":442},"OpenTelemetry","\u002Fen\u002Ftelemetry\u002Fopentelemetry","en\u002F18.telemetry\u002F3.opentelemetry","i-lucide-radar",{"title":444,"path":445,"stem":446,"icon":447},"Sentry","\u002Fen\u002Ftelemetry\u002Fsentry","en\u002F18.telemetry\u002F4.sentry","i-lucide-bug",{"id":449,"title":143,"body":450,"description":2081,"extension":2082,"links":2083,"meta":2084,"navigation":2085,"path":144,"seo":2086,"stem":145,"__hash__":2087},"docs_en\u002Fen\u002F03.framework\u002F5.realtime.md",{"type":451,"value":452,"toc":2068},"minimark",[453,468,473,494,507,511,700,714,718,739,756,946,952,961,1078,1087,1091,1109,1286,1323,1326,1330,1340,1421,1432,1495,1505,1509,1512,1556,1560,1705,1730,1735,1753,1774,1778,1931,1944,1953,2011,2023,2027,2064],[454,455,456,457,462,463,467],"p",{},"The ",[458,459,461],"a",{"href":460},"\u002Fen\u002Fnotify\u002Fbroadcast#broadcast","broadcast channel"," delivers a notification to a recipient's live connection, the moment it happens: a \"poke\" on top of the durable ",[458,464,466],{"href":465},"\u002Fen\u002Fnotify\u002Fdatabase#database","database channel",", not a replacement for it. This page covers the gateway that holds those connections.",[469,470,472],"h2",{"id":471},"why-a-separate-gateway","Why a separate gateway",[454,474,475,476,480,481,484,485,488,489,493],{},"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 framework keeps these apart: ",[477,478,479],"code",{},"notify\u002Fbroadcast"," is a tiny publisher any process imports (no server, no held state); ",[477,482,483],{},"realtime"," is a dedicated binary (",[477,486,487],{},"cmd\u002Frealtime",", scaffolded by ",[458,490,491],{"href":72},[477,492,71],{},") that only holds connections and relays what gets published.",[454,495,496,497,499,500,502,503,506],{},"The driver seam is deliberate: nothing outside ",[477,498,479],{}," and ",[477,501,483],{}," imports ",[477,504,505],{},"centrifuge"," directly, so a different transport can replace it later without touching application code.",[469,508,510],{"id":509},"publishing","Publishing",[512,513,518],"pre",{"className":514,"code":515,"language":516,"meta":517,"style":517},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import \"github.com\u002Fgp-system\u002Fnotify\u002Fbroadcast\"\n\npub := broadcast.MustNewPublisher(ctx, cfg.Realtime.BroadcastValkey(), 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","",[477,519,520,540,547,605,624,643,661,695],{"__ignoreMap":517},[521,522,525,529,533,537],"span",{"class":523,"line":524},"line",1,[521,526,528],{"class":527},"s7zQu","import",[521,530,532],{"class":531},"sMK4o"," \"",[521,534,536],{"class":535},"sBMFI","github.com\u002Fgp-system\u002Fnotify\u002Fbroadcast",[521,538,539],{"class":531},"\"\n",[521,541,543],{"class":523,"line":542},2,[521,544,546],{"emptyLinePlaceholder":545},true,"\n",[521,548,550,554,557,560,563,567,570,573,576,579,581,583,585,588,591,593,595,597,599,602],{"class":523,"line":549},3,[521,551,553],{"class":552},"sTEyZ","pub ",[521,555,556],{"class":531},":=",[521,558,559],{"class":552}," broadcast",[521,561,562],{"class":531},".",[521,564,566],{"class":565},"s2Zo4","MustNewPublisher",[521,568,569],{"class":531},"(",[521,571,572],{"class":552},"ctx",[521,574,575],{"class":531},",",[521,577,578],{"class":552}," cfg",[521,580,562],{"class":531},[521,582,143],{"class":552},[521,584,562],{"class":531},[521,586,587],{"class":565},"BroadcastValkey",[521,589,590],{"class":531},"(),",[521,592,578],{"class":552},[521,594,562],{"class":531},[521,596,143],{"class":552},[521,598,562],{"class":531},[521,600,601],{"class":552},"Broadcast",[521,603,604],{"class":531},")\n",[521,606,608,611,613,616,618,621],{"class":523,"line":607},4,[521,609,610],{"class":552},"notifier ",[521,612,556],{"class":531},[521,614,615],{"class":552}," notify",[521,617,562],{"class":531},[521,619,620],{"class":565},"NewHub",[521,622,623],{"class":531},"(\n",[521,625,627,630,632,635,637,640],{"class":523,"line":626},5,[521,628,629],{"class":552},"    notify",[521,631,562],{"class":531},[521,633,634],{"class":565},"NewMailChannel",[521,636,569],{"class":531},[521,638,639],{"class":552},"mailer",[521,641,642],{"class":531},"),\n",[521,644,646,649,651,654,656,659],{"class":523,"line":645},6,[521,647,648],{"class":552},"    notifydatabase",[521,650,562],{"class":531},[521,652,653],{"class":565},"NewChannel",[521,655,569],{"class":531},[521,657,658],{"class":552},"store",[521,660,642],{"class":531},[521,662,664,666,668,671,673,676,678,680,682,684,686,688,690,693],{"class":523,"line":663},7,[521,665,629],{"class":552},[521,667,562],{"class":531},[521,669,670],{"class":565},"NewBroadcastChannel",[521,672,569],{"class":531},[521,674,675],{"class":552},"pub",[521,677,575],{"class":531},[521,679,578],{"class":552},[521,681,562],{"class":531},[521,683,143],{"class":552},[521,685,562],{"class":531},[521,687,601],{"class":552},[521,689,562],{"class":531},[521,691,692],{"class":552},"ChannelPrefix",[521,694,642],{"class":531},[521,696,698],{"class":523,"line":697},8,[521,699,604],{"class":531},[454,701,702,705,706,709,710,713],{},[477,703,704],{},"Publisher"," is a publish-only ",[477,707,708],{},"centrifuge.Node"," sharing the project's Valkey 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 ",[477,711,712],{},"notify.Hub.Send"," already enforces (database channel before broadcast, same delivery ID in both).",[469,715,717],{"id":716},"the-gateway","The gateway",[454,719,720,722,723,726,727,730,731,734,735,738],{},[477,721,487],{}," 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 ",[477,724,725],{},"centrifugal\u002Fcentrifuge",", the library behind Centrifugo and Grafana Live) under ",[477,728,729],{},"\u002Frealtime\u002F*",", authenticates each connection's JWT in the connect frame (no separate ",[477,732,733],{},"\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 ",[477,736,737],{},"user:\u003Csubject>"," channel. No client-side subscribe logic needed for the common case.",[454,740,741,742,747,748,751,752,755],{},"Shared topic channels are opt-in and explicitly authorized: each connection's JWT resolves to an ",[458,743,744],{"href":216},[477,745,746],{},"rbac.Identity",", and a topic is only ever subscribable if some ",[477,749,750],{},"Allow","\u002F",[477,753,754],{},"AllowPrefix"," callback vouches for that identity.",[512,757,760],{"className":514,"code":758,"filename":759,"language":516,"meta":517,"style":517},"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",[477,761,762,818,873,933,941],{"__ignoreMap":517},[521,763,764,767,769,772,774,777,779,781,783,785,787,789,791,794,798,801,803,805,808,811,815],{"class":523,"line":524},[521,765,766],{"class":552},"err ",[521,768,556],{"class":531},[521,770,771],{"class":552}," realtime",[521,773,562],{"class":531},[521,775,776],{"class":565},"Run",[521,778,569],{"class":531},[521,780,572],{"class":552},[521,782,575],{"class":531},[521,784,578],{"class":552},[521,786,562],{"class":531},[521,788,143],{"class":552},[521,790,575],{"class":531},[521,792,793],{"class":531}," func(",[521,795,797],{"class":796},"sHdIc","topics",[521,799,800],{"class":531}," *",[521,802,483],{"class":535},[521,804,562],{"class":531},[521,806,807],{"class":535},"TopicAuth",[521,809,810],{"class":531},")",[521,812,814],{"class":813},"spNyl"," error",[521,816,817],{"class":531}," {\n",[521,819,820,823,825,827,829,832,836,838,840,842,845,847,850,852,855,857,860,863,866,870],{"class":523,"line":542},[521,821,822],{"class":552},"    topics",[521,824,562],{"class":531},[521,826,750],{"class":565},[521,828,569],{"class":531},[521,830,831],{"class":531},"\"",[521,833,835],{"class":834},"sfazB","announcements",[521,837,831],{"class":531},[521,839,575],{"class":531},[521,841,793],{"class":531},[521,843,844],{"class":796},"id",[521,846,800],{"class":531},[521,848,849],{"class":535},"rbac",[521,851,562],{"class":531},[521,853,854],{"class":535},"Identity",[521,856,810],{"class":531},[521,858,859],{"class":813}," bool",[521,861,862],{"class":531}," {",[521,864,865],{"class":527}," return",[521,867,869],{"class":868},"sfNiH"," true",[521,871,872],{"class":531}," })\n",[521,874,875,877,879,881,883,885,888,890,892,894,896,898,900,902,904,906,908,910,912,915,917,920,922,924,927,929,931],{"class":523,"line":549},[521,876,822],{"class":552},[521,878,562],{"class":531},[521,880,750],{"class":565},[521,882,569],{"class":531},[521,884,831],{"class":531},[521,886,887],{"class":834},"admin-alerts",[521,889,831],{"class":531},[521,891,575],{"class":531},[521,893,793],{"class":531},[521,895,844],{"class":796},[521,897,800],{"class":531},[521,899,849],{"class":535},[521,901,562],{"class":531},[521,903,854],{"class":535},[521,905,810],{"class":531},[521,907,859],{"class":813},[521,909,862],{"class":531},[521,911,865],{"class":527},[521,913,914],{"class":552}," id",[521,916,562],{"class":531},[521,918,919],{"class":565},"HasRole",[521,921,569],{"class":531},[521,923,831],{"class":531},[521,925,926],{"class":834},"admin",[521,928,831],{"class":531},[521,930,810],{"class":531},[521,932,872],{"class":531},[521,934,935,938],{"class":523,"line":607},[521,936,937],{"class":527},"    return",[521,939,940],{"class":531}," nil\n",[521,942,943],{"class":523,"line":626},[521,944,945],{"class":531},"})\n",[454,947,948,949,951],{},"A topic with no registered ",[477,950,750],{}," callback is never subscribable.",[454,953,954,955,957,958,960],{},"For a channel family whose full name is only known at runtime, one per row\nof some dynamic resource (a tenant, a project, a repository), naming each\none individually with ",[477,956,750],{}," at startup is not possible: use ",[477,959,754],{},"\ninstead, whose callback receives the full topic string so it can look up\nwhich specific instance is being requested.",[512,962,964],{"className":514,"code":963,"language":516,"meta":517,"style":517},"topics.AllowPrefix(\"project:\", func(id *rbac.Identity, topic string) bool {\n    projectID, _ := strings.CutPrefix(topic, \"project:\")\n    return projectAccess.CanView(id.Subject, projectID)\n})\n",[477,965,966,1011,1046,1074],{"__ignoreMap":517},[521,967,968,970,972,974,976,978,981,983,985,987,989,991,993,995,997,999,1002,1005,1007,1009],{"class":523,"line":524},[521,969,797],{"class":552},[521,971,562],{"class":531},[521,973,754],{"class":565},[521,975,569],{"class":531},[521,977,831],{"class":531},[521,979,980],{"class":834},"project:",[521,982,831],{"class":531},[521,984,575],{"class":531},[521,986,793],{"class":531},[521,988,844],{"class":796},[521,990,800],{"class":531},[521,992,849],{"class":535},[521,994,562],{"class":531},[521,996,854],{"class":535},[521,998,575],{"class":531},[521,1000,1001],{"class":796}," topic",[521,1003,1004],{"class":813}," string",[521,1006,810],{"class":531},[521,1008,859],{"class":813},[521,1010,817],{"class":531},[521,1012,1013,1016,1018,1021,1023,1026,1028,1031,1033,1036,1038,1040,1042,1044],{"class":523,"line":542},[521,1014,1015],{"class":552},"    projectID",[521,1017,575],{"class":531},[521,1019,1020],{"class":552}," _ ",[521,1022,556],{"class":531},[521,1024,1025],{"class":552}," strings",[521,1027,562],{"class":531},[521,1029,1030],{"class":565},"CutPrefix",[521,1032,569],{"class":531},[521,1034,1035],{"class":552},"topic",[521,1037,575],{"class":531},[521,1039,532],{"class":531},[521,1041,980],{"class":834},[521,1043,831],{"class":531},[521,1045,604],{"class":531},[521,1047,1048,1050,1053,1055,1058,1060,1062,1064,1067,1069,1072],{"class":523,"line":549},[521,1049,937],{"class":527},[521,1051,1052],{"class":552}," projectAccess",[521,1054,562],{"class":531},[521,1056,1057],{"class":565},"CanView",[521,1059,569],{"class":531},[521,1061,844],{"class":552},[521,1063,562],{"class":531},[521,1065,1066],{"class":552},"Subject",[521,1068,575],{"class":531},[521,1070,1071],{"class":552}," projectID",[521,1073,604],{"class":531},[521,1075,1076],{"class":523,"line":607},[521,1077,945],{"class":531},[454,1079,1080,1081,1083,1084,1086],{},"An exact ",[477,1082,750],{}," match always takes priority over a matching ",[477,1085,754],{},";\namong prefixes, the first one registered that matches wins.",[469,1088,1090],{"id":1089},"client-to-server-rpc","Client-to-server RPC",[454,1092,1093,1094,1097,1098,1101,1102,1105,1106,1108],{},"By default the gateway is server-to-client only: a connection receives\npublishes but cannot write anything. ",[477,1095,1096],{},"WithRPC"," turns on the other direction: a\nclient sends a ",[477,1099,1100],{},"method"," name and a raw payload, and the server answers with a\nregistered ",[477,1103,1104],{},"RPCHandler",", seeing the same JWT-resolved identity a ",[477,1107,807],{},"\ncallback does.",[512,1110,1112],{"className":514,"code":1111,"filename":759,"language":516,"meta":517,"style":517},"err := realtime.Run(ctx, cfg.Realtime, register, realtime.WithRPC(func(rpc *realtime.RPCRegistry) error {\n    rpc.Handle(\"chat.send\", func(ctx context.Context, id *rbac.Identity, data []byte) ([]byte, error) {\n        return chatService.SendViaRPC(ctx, id, data)\n    })\n    return nil\n}))\n",[477,1113,1114,1172,1243,1270,1275,1281],{"__ignoreMap":517},[521,1115,1116,1118,1120,1122,1124,1126,1128,1130,1132,1134,1136,1138,1140,1143,1145,1147,1149,1151,1154,1157,1159,1161,1163,1166,1168,1170],{"class":523,"line":524},[521,1117,766],{"class":552},[521,1119,556],{"class":531},[521,1121,771],{"class":552},[521,1123,562],{"class":531},[521,1125,776],{"class":565},[521,1127,569],{"class":531},[521,1129,572],{"class":552},[521,1131,575],{"class":531},[521,1133,578],{"class":552},[521,1135,562],{"class":531},[521,1137,143],{"class":552},[521,1139,575],{"class":531},[521,1141,1142],{"class":552}," register",[521,1144,575],{"class":531},[521,1146,771],{"class":552},[521,1148,562],{"class":531},[521,1150,1096],{"class":565},[521,1152,1153],{"class":531},"(func(",[521,1155,1156],{"class":796},"rpc",[521,1158,800],{"class":531},[521,1160,483],{"class":535},[521,1162,562],{"class":531},[521,1164,1165],{"class":535},"RPCRegistry",[521,1167,810],{"class":531},[521,1169,814],{"class":813},[521,1171,817],{"class":531},[521,1173,1174,1177,1179,1182,1184,1186,1189,1191,1193,1195,1197,1200,1202,1205,1207,1209,1211,1213,1215,1217,1219,1222,1225,1228,1230,1233,1235,1237,1239,1241],{"class":523,"line":542},[521,1175,1176],{"class":552},"    rpc",[521,1178,562],{"class":531},[521,1180,1181],{"class":565},"Handle",[521,1183,569],{"class":531},[521,1185,831],{"class":531},[521,1187,1188],{"class":834},"chat.send",[521,1190,831],{"class":531},[521,1192,575],{"class":531},[521,1194,793],{"class":531},[521,1196,572],{"class":796},[521,1198,1199],{"class":535}," context",[521,1201,562],{"class":531},[521,1203,1204],{"class":535},"Context",[521,1206,575],{"class":531},[521,1208,914],{"class":796},[521,1210,800],{"class":531},[521,1212,849],{"class":535},[521,1214,562],{"class":531},[521,1216,854],{"class":535},[521,1218,575],{"class":531},[521,1220,1221],{"class":796}," data",[521,1223,1224],{"class":531}," []",[521,1226,1227],{"class":813},"byte",[521,1229,810],{"class":531},[521,1231,1232],{"class":531}," ([]",[521,1234,1227],{"class":813},[521,1236,575],{"class":531},[521,1238,814],{"class":813},[521,1240,810],{"class":531},[521,1242,817],{"class":531},[521,1244,1245,1248,1251,1253,1256,1258,1260,1262,1264,1266,1268],{"class":523,"line":549},[521,1246,1247],{"class":527},"        return",[521,1249,1250],{"class":552}," chatService",[521,1252,562],{"class":531},[521,1254,1255],{"class":565},"SendViaRPC",[521,1257,569],{"class":531},[521,1259,572],{"class":552},[521,1261,575],{"class":531},[521,1263,914],{"class":552},[521,1265,575],{"class":531},[521,1267,1221],{"class":552},[521,1269,604],{"class":531},[521,1271,1272],{"class":523,"line":607},[521,1273,1274],{"class":531},"    })\n",[521,1276,1277,1279],{"class":523,"line":626},[521,1278,937],{"class":527},[521,1280,940],{"class":531},[521,1282,1283],{"class":523,"line":645},[521,1284,1285],{"class":531},"}))\n",[454,1287,1288,1289,1292,1293,1295,1296,1299,1300,1303,1304,1307,1308,1311,1312,1315,1316,1318,1319,1322],{},"A method with no registered handler gets the client\n",[477,1290,1291],{},"centrifuge.ErrorMethodNotFound",". The handler's returned error maps much like\n",[477,1294,308],{}," does: an error carrying an ",[477,1297,1298],{},"errs.Status"," becomes the matching\n",[477,1301,1302],{},"centrifuge.Error"," (400\u002F422 → ",[477,1305,1306],{},"ErrorBadRequest",", 401\u002F403 →\n",[477,1309,1310],{},"ErrorPermissionDenied",", 404 → a framework-defined \"not found\" error), and its\n",[477,1313,1314],{},"errs.Public"," message is what the client sees; a plain error with no\n",[477,1317,1298],{}," maps to ",[477,1320,1321],{},"ErrorInternal",", with no internal detail leaked.",[454,1324,1325],{},"The gateway has no database access (see \"holds no business dependencies\"\nabove), so an RPC handler typically calls an existing HTTP endpoint (where\nvalidation, policy, and the transaction already live) rather than duplicating\nbusiness logic.",[469,1327,1329],{"id":1328},"options-and-the-middleware-chain","Options and the middleware chain",[454,1331,1332,1335,1336,1339],{},[477,1333,1334],{},"realtime.Run"," takes options in the same style as ",[477,1337,1338],{},"server.Run",":",[1341,1342,1343,1356],"table",{},[1344,1345,1346],"thead",{},[1347,1348,1349,1353],"tr",{},[1350,1351,1352],"th",{},"Option",[1350,1354,1355],{},"Effect",[1357,1358,1359,1370,1383,1393,1407],"tbody",{},[1347,1360,1361,1367],{},[1362,1363,1364],"td",{},[477,1365,1366],{},"WithCloser(name, fn)",[1362,1368,1369],{},"cleanup during graceful shutdown, after every held connection has been disconnected (LIFO; telemetry flushes last)",[1347,1371,1372,1377],{},[1362,1373,1374],{},[477,1375,1376],{},"WithMiddleware(...func(http.Handler) http.Handler)",[1362,1378,1379,1380],{},"appends net\u002Fhttp middleware after the framework defaults and before the transport endpoints; the same contract as ",[477,1381,1382],{},"server.WithMiddleware",[1347,1384,1385,1390],{},[1362,1386,1387],{},[477,1388,1389],{},"WithRPC(register)",[1362,1391,1392],{},"client-callable RPC methods (see above)",[1347,1394,1395,1400],{},[1362,1396,1397],{},[477,1398,1399],{},"WithHTTPServer(func(*http.Server))",[1362,1401,1402,1403,1406],{},"escape hatch for ",[477,1404,1405],{},"http.Server"," fields the framework does not expose",[1347,1408,1409,1414],{},[1362,1410,1411],{},[477,1412,1413],{},"WithoutTelemetry()",[1362,1415,1416,1417,1420],{},"skips ",[477,1418,1419],{},"telemetry.Setup"," and the otel middleware; for tests, or when the process configures telemetry itself",[454,1422,1423,1424,1427,1428,1431],{},"The framework chain around the transport endpoints is aligned with the ",[458,1425,1426],{"href":135},"server chassis",", built from the same exported ",[477,1429,1430],{},"gpsystem\u002Fhttpmw"," building blocks, outermost first:",[1433,1434,1435,1447,1460,1477,1486],"ol",{},[1436,1437,1438,1442,1443,1446],"li",{},[1439,1440,1441],"strong",{},"CORS, streaming preset"," (",[477,1444,1445],{},"httpmw.CORSStreaming","): unlike the API preset, credentials are allowed (some streaming setups authenticate with cookies), so the per-origin decision comes from the origin allower described below instead of a wildcard.",[1436,1448,1449,1442,1452,1455,1456,1459],{},[1439,1450,1451],{},"request id",[477,1453,1454],{},"httpmw.RequestID","): every response carries an ",[477,1457,1458],{},"X-Request-Id"," header, same semantics as on the API side.",[1436,1461,1462,1442,1465,1468,1469,1472,1473,1476],{},[1439,1463,1464],{},"otel",[477,1466,1467],{},"httpmw.Otel(\"realtime\")","): the gateway's span; ",[477,1470,1471],{},"\u002Frealtime\u002Fhealthz"," is filtered out of tracing, and ",[477,1474,1475],{},"WithoutTelemetry"," skips the whole entry.",[1436,1478,1479,1442,1482,1485],{},[1439,1480,1481],{},"Sentry request hub",[477,1483,1484],{},"httpmw.RequestHub","): breadcrumb isolation per request.",[1436,1487,1488,1442,1491,1494],{},[1439,1489,1490],{},"panic recovery",[477,1492,1493],{},"httpmw.Recoverer","): a panic in the HTTP layer becomes a plain 500 plus an error log carrying the stack; the gateway has no Problem writer, and streaming clients reconnect on any 5xx anyway.",[454,1496,1497,1500,1501,1504],{},[477,1498,1499],{},"WithMiddleware"," extras run inside these, right before the transport endpoints. There is deliberately ",[1439,1502,1503],{},"no access log",": the connection endpoints hold long-lived streams, so a per-request record would be misleading.",[469,1506,1508],{"id":1507},"scaling-instances-come-and-go-freely","Scaling: instances come and go freely",[454,1510,1511],{},"Every gateway instance is stateless: it only ever holds the channels its own connections need, and the shared Valkey broker fans a publish out to every instance that needs it, from any process, without a sticky-session requirement.",[1513,1514,1515,1525,1541,1547],"ul",{},[1436,1516,1517,1520,1521,1524],{},[1439,1518,1519],{},"Scale up",": ",[477,1522,1523],{},"docker compose up -d --scale realtime=N","; a new instance starts with zero state and accumulates subscriptions only as clients land on it.",[1436,1526,1527,1520,1530,1533,1534,1536,1537,1540],{},[1439,1528,1529],{},"Scale down \u002F rolling restart",[477,1531,1532],{},"SIGTERM"," flips a draining flag (",[477,1535,1471],{}," and new connect attempts return 503, so the load balancer's health check evicts the instance), ",[477,1538,1539],{},"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.",[1436,1542,1543,1546],{},[1439,1544,1545],{},"Crash",": the client side is identical to a graceful restart, just without the drain step.",[1436,1548,1549,1552,1553,1555],{},[1439,1550,1551],{},"Valkey restart",": reconnect and resubscribe are ",[477,1554,505],{},"'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.",[469,1557,1559],{"id":1558},"config","Config",[1341,1561,1562,1575],{},[1344,1563,1564],{},[1347,1565,1566,1569,1572],{},[1350,1567,1568],{},"Variable",[1350,1570,1571],{},"Default",[1350,1573,1574],{},"Meaning",[1357,1576,1577,1592,1607,1622,1638,1654,1671,1686],{},[1347,1578,1579,1584,1589],{},[1362,1580,1581],{},[477,1582,1583],{},"REALTIME_LISTEN_ADDR",[1362,1585,1586],{},[477,1587,1588],{},":3000",[1362,1590,1591],{},"container-internal listen address",[1347,1593,1594,1599,1604],{},[1362,1595,1596],{},[477,1597,1598],{},"REALTIME_DRAIN_TIMEOUT",[1362,1600,1601],{},[477,1602,1603],{},"20s",[1362,1605,1606],{},"graceful-shutdown budget; keep under the deployment's stop-grace-period",[1347,1608,1609,1614,1619],{},[1362,1610,1611],{},[477,1612,1613],{},"REALTIME_READ_HEADER_TIMEOUT",[1362,1615,1616],{},[477,1617,1618],{},"5s",[1362,1620,1621],{},"Slowloris defense on the connect handshake",[1347,1623,1624,1629,1632],{},[1362,1625,1626],{},[477,1627,1628],{},"REALTIME_CHANNEL_PREFIX",[1362,1630,1631],{},"the project name",[1362,1633,1634,1635],{},"Valkey channel namespace shared with every ",[477,1636,1637],{},"notify\u002Fbroadcast.Publisher",[1347,1639,1640,1645,1651],{},[1362,1641,1642],{},[477,1643,1644],{},"REALTIME_MAX_CONNECTIONS",[1362,1646,1647,1650],{},[477,1648,1649],{},"0"," (unlimited)",[1362,1652,1653],{},"per-instance stream cap; over it, connect attempts get 503",[1347,1655,1656,1661,1664],{},[1362,1657,1658],{},[477,1659,1660],{},"VALKEY_*",[1362,1662,1663],{},"inherited",[1362,1665,1666,1667,1670],{},"reused from ",[477,1668,1669],{},"queue.Config",": the gateway's broker is the same Valkey the queue uses",[1347,1672,1673,1678,1680],{},[1362,1674,1675],{},[477,1676,1677],{},"JWT_*",[1362,1679,1663],{},[1362,1681,1666,1682,1685],{},[477,1683,1684],{},"auth.Config",": the same token issuer every module trusts",[1347,1687,1688,1693,1698],{},[1362,1689,1690],{},[477,1691,1692],{},"CORS_ORIGINS",[1362,1694,1695],{},[477,1696,1697],{},"*",[1362,1699,1700,1701,1704],{},"shared with ",[477,1702,1703],{},"server.Config","; drives the gateway's origin policy across all three transports (websocket, http_stream, sse) and the CORS headers alike",[454,1706,1707,1708,1713,1714,1717,1718,1721,1722,1726,1727,562],{},"See the ",[458,1709,1710,1712],{"href":72},[477,1711,71],{}," reference"," for what gets scaffolded (the binary, ",[477,1715,1716],{},"compose.yml","'s Traefik-routed service, ",[477,1719,1720],{},".env.example",") and the ",[458,1723,1724],{"href":67},[477,1725,66],{}," page for wiring the broadcast channel into a notification's ",[477,1728,1729],{},"Via",[1731,1732,1734],"h3",{"id":1733},"origin-checking","Origin checking",[454,1736,1737,1739,1740,1742,1743,1745,1746,1749,1750,562],{},[477,1738,1692],{}," gives all three transports one rule: same-origin connections\n(the Origin header's host matches the request's Host, the documented\nproduction setup) and clients with no Origin header at all (server-to-server\ncalls, health checks) always pass, regardless of the list; every other case is\ndecided by the list (",[477,1741,1697],{}," allows everything, otherwise an exact match or a\npattern containing ",[477,1744,1697],{},", e.g. ",[477,1747,1748],{},"https:\u002F\u002F*.example.com","). Rejecting a foreign\norigin logs a ",[477,1751,1752],{},"Warn",[454,1754,1755,1756,1759,1760,1763,1764,1767,1768,1770,1771,1773],{},"In development it's common for the frontend and the gateway to be served from\ndifferent origins (say, ",[477,1757,1758],{},"next dev"," on the host at ",[477,1761,1762],{},"http:\u002F\u002Flocalhost:3000",",\nwith the gateway behind Traefik at ",[477,1765,1766],{},"http:\u002F\u002F\u003Capp>.localhost","): in that case\n",[477,1769,1692],{}," must include the frontend's origin (or stay ",[477,1772,1697],{}," in dev),\notherwise the websocket handshake gets a 403.",[469,1775,1777],{"id":1776},"client","Client",[512,1779,1783],{"className":1780,"code":1781,"language":1782,"meta":517,"style":517},"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",[477,1784,1785,1808,1812,1842,1870,1877,1907,1912,1918],{"__ignoreMap":517},[521,1786,1787,1789,1791,1794,1797,1800,1803,1805],{"class":523,"line":524},[521,1788,528],{"class":527},[521,1790,862],{"class":531},[521,1792,1793],{"class":552}," Centrifuge",[521,1795,1796],{"class":531}," }",[521,1798,1799],{"class":527}," from",[521,1801,1802],{"class":531}," '",[521,1804,505],{"class":834},[521,1806,1807],{"class":531},"'\n",[521,1809,1810],{"class":523,"line":542},[521,1811,546],{"emptyLinePlaceholder":545},[521,1813,1814,1817,1820,1823,1826,1828,1830,1833,1836,1838,1840],{"class":523,"line":549},[521,1815,1816],{"class":813},"const",[521,1818,1819],{"class":552}," client ",[521,1821,1822],{"class":531},"=",[521,1824,1825],{"class":531}," new",[521,1827,1793],{"class":565},[521,1829,569],{"class":552},[521,1831,1832],{"class":531},"'",[521,1834,1835],{"class":834},"wss:\u002F\u002Fyour-app.example\u002Frealtime\u002Fconnection\u002Fwebsocket",[521,1837,1832],{"class":531},[521,1839,575],{"class":531},[521,1841,817],{"class":531},[521,1843,1844,1847,1849,1852,1855,1858,1861,1864,1866],{"class":523,"line":607},[521,1845,1846],{"class":565},"    getToken",[521,1848,1339],{"class":531},[521,1850,1851],{"class":813}," async",[521,1853,1854],{"class":531}," ()",[521,1856,1857],{"class":813}," =>",[521,1859,1860],{"class":565}," fetchAccessToken",[521,1862,1863],{"class":552},"()",[521,1865,575],{"class":531},[521,1867,1869],{"class":1868},"sHwdD"," \u002F\u002F the same JWT the API uses\n",[521,1871,1872,1875],{"class":523,"line":626},[521,1873,1874],{"class":531},"}",[521,1876,604],{"class":552},[521,1878,1879,1881,1883,1886,1888,1890,1893,1895,1897,1899,1901,1903,1905],{"class":523,"line":645},[521,1880,1776],{"class":552},[521,1882,562],{"class":531},[521,1884,1885],{"class":565},"on",[521,1887,569],{"class":552},[521,1889,1832],{"class":531},[521,1891,1892],{"class":834},"publication",[521,1894,1832],{"class":531},[521,1896,575],{"class":531},[521,1898,1442],{"class":531},[521,1900,572],{"class":796},[521,1902,810],{"class":531},[521,1904,1857],{"class":813},[521,1906,817],{"class":531},[521,1908,1909],{"class":523,"line":663},[521,1910,1911],{"class":1868},"    \u002F\u002F ctx.data: { id, name, payload, at }, id ties back to the database channel's row\n",[521,1913,1914,1916],{"class":523,"line":697},[521,1915,1874],{"class":531},[521,1917,604],{"class":552},[521,1919,1921,1923,1925,1928],{"class":523,"line":1920},9,[521,1922,1776],{"class":552},[521,1924,562],{"class":531},[521,1926,1927],{"class":565},"connect",[521,1929,1930],{"class":552},"()\n",[454,1932,1933,1936,1937,1940,1941,1943],{},[477,1934,1935],{},"centrifuge-js"," handles reconnect backoff and token refresh; the server-side subscription to the recipient's own channel means the client needs no explicit ",[477,1938,1939],{},"subscribe"," call for notifications addressed to it. On reconnect, fetch unread notifications from your module's own inbox endpoint (built on the ",[458,1942,466],{"href":465},") to backfill anything missed while disconnected. The socket is a live poke, the database is the source of truth.",[454,1945,1946,1947,1949,1950,1339],{},"If the gateway registered a method through ",[477,1948,1096],{},", the client calls it with ",[477,1951,1952],{},"client.rpc(...)",[512,1954,1956],{"className":1780,"code":1955,"language":1782,"meta":517,"style":517},"const reply = await client.rpc('chat.send', { body: 'hi' })\n\u002F\u002F reply.data: the handler's raw response, JSON.parse is the caller's job\n",[477,1957,1958,2006],{"__ignoreMap":517},[521,1959,1960,1962,1965,1967,1970,1973,1975,1977,1979,1981,1983,1985,1987,1989,1993,1995,1997,2000,2002,2004],{"class":523,"line":524},[521,1961,1816],{"class":813},[521,1963,1964],{"class":552}," reply ",[521,1966,1822],{"class":531},[521,1968,1969],{"class":527}," await",[521,1971,1972],{"class":552}," client",[521,1974,562],{"class":531},[521,1976,1156],{"class":565},[521,1978,569],{"class":552},[521,1980,1832],{"class":531},[521,1982,1188],{"class":834},[521,1984,1832],{"class":531},[521,1986,575],{"class":531},[521,1988,862],{"class":531},[521,1990,1992],{"class":1991},"swJcz"," body",[521,1994,1339],{"class":531},[521,1996,1802],{"class":531},[521,1998,1999],{"class":834},"hi",[521,2001,1832],{"class":531},[521,2003,1796],{"class":531},[521,2005,604],{"class":552},[521,2007,2008],{"class":523,"line":542},[521,2009,2010],{"class":1868},"\u002F\u002F reply.data: the handler's raw response, JSON.parse is the caller's job\n",[454,2012,2013,2014,1442,2016,2018,2019,2022],{},"A failed call throws the ",[477,2015,1302],{},[477,2017,477],{},", ",[477,2020,2021],{},"message","); see the \"Client-to-server RPC\" section above for the error mapping.",[469,2024,2026],{"id":2025},"related-pages","Related pages",[1513,2028,2029,2036,2052],{},[1436,2030,2031,2035],{},[458,2032,2033],{"href":371},[477,2034,479],{},": the publish-only side any process imports.",[1436,2037,2038,2043,2044,2046,2047,751,2049,2051],{},[458,2039,2040],{"href":216},[477,2041,2042],{},"auth\u002Frbac",": the ",[477,2045,854],{}," a connection's JWT resolves to, and what ",[477,2048,750],{},[477,2050,754],{}," check against.",[1436,2053,2054,2056,2057,2060,2061,2063],{},[458,2055,129],{"href":130},": the shared ",[477,2058,2059],{},"app.Run"," skeleton ",[477,2062,1334],{}," builds on.",[2065,2066,2067],"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}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":517,"searchDepth":542,"depth":542,"links":2069},[2070,2071,2072,2073,2074,2075,2076,2079,2080],{"id":471,"depth":542,"text":472},{"id":509,"depth":542,"text":510},{"id":716,"depth":542,"text":717},{"id":1089,"depth":542,"text":1090},{"id":1328,"depth":542,"text":1329},{"id":1507,"depth":542,"text":1508},{"id":1558,"depth":542,"text":1559,"children":2077},[2078],{"id":1733,"depth":549,"text":1734},{"id":1776,"depth":542,"text":1777},{"id":2025,"depth":542,"text":2026},"A dedicated, horizontally scalable gateway that pushes notifications live to connected clients: server→client only, built on centrifuge.","md",null,{},{"icon":146},{"title":143,"description":2081},"QyNOHtuP40GhkBc9NyXOY7_wBRmJRnCSB02Oy8YVmoU",[2089,2091],{"title":139,"path":140,"stem":141,"description":2090,"icon":114,"children":-1},"The background-work chassis, asynq server, outbox relay and schedule runner in one binary, under a shared graceful shutdown.",{"title":148,"path":149,"stem":150,"description":2092,"icon":151,"children":-1},"Timestamp-prefixed SQL files embedded into the binary, with a generated cmd\u002Fmigrate runner, no external tool.",1787346801172]