[{"data":1,"prerenderedAt":1534},["ShallowReactive",2],{"navigation_docs_en":3,"-en-kit-realtime":441,"-en-kit-realtime-surround":1529},[4,45,60,81,112,129,146,173,197,213,242,279,356,372,384,403,429],{"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},"queue","i-lucide-layers","\u002Fen\u002Fqueue","en\u002F10.queue",[51,56],{"title":52,"path":53,"stem":54,"icon":55},"Overview","\u002Fen\u002Fqueue\u002Foverview","en\u002F10.queue\u002F1.overview","i-lucide-list-ordered",{"title":57,"path":58,"stem":59,"icon":47},"Tasks","\u002Fen\u002Fqueue\u002Ftasks","en\u002F10.queue\u002F2.tasks",{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":44},"events","i-lucide-workflow","\u002Fen\u002Fevents","en\u002F11.events",[66,71,76],{"title":67,"path":68,"stem":69,"icon":70},"Events","\u002Fen\u002Fevents\u002Foverview","en\u002F11.events\u002F1.overview","i-lucide-radio",{"title":72,"path":73,"stem":74,"icon":75},"Outbox","\u002Fen\u002Fevents\u002Foutbox","en\u002F11.events\u002F2.outbox","i-lucide-inbox",{"title":77,"path":78,"stem":79,"icon":80},"Scheduling","\u002Fen\u002Fevents\u002Fscheduler","en\u002F11.events\u002F3.scheduler","i-lucide-calendar-clock",{"title":82,"icon":83,"path":84,"stem":85,"children":86,"page":44},"auth","i-lucide-shield-check","\u002Fen\u002Fauth","en\u002F12.auth",[87,92,97,102,107],{"title":88,"path":89,"stem":90,"icon":91},"Authentication","\u002Fen\u002Fauth\u002Foverview","en\u002F12.auth\u002F1.overview","i-lucide-key-round",{"title":93,"path":94,"stem":95,"icon":96},"RBAC","\u002Fen\u002Fauth\u002Frbac","en\u002F12.auth\u002F2.rbac","i-lucide-users",{"title":98,"path":99,"stem":100,"icon":101},"Policies","\u002Fen\u002Fauth\u002Fpolicy","en\u002F12.auth\u002F3.policy","i-lucide-gavel",{"title":103,"path":104,"stem":105,"icon":106},"Using auth standalone","\u002Fen\u002Fauth\u002Fstandalone","en\u002F12.auth\u002F4.standalone","i-lucide-plug",{"title":108,"path":109,"stem":110,"icon":111},"Social login with goth, standalone","\u002Fen\u002Fauth\u002Fsocial-standalone","en\u002F12.auth\u002F5.social-standalone","i-lucide-log-in",{"title":113,"icon":114,"path":115,"stem":116,"children":117,"page":44},"mail","i-lucide-mail","\u002Fen\u002Fmail","en\u002F13.mail",[118,121,125],{"title":52,"path":119,"stem":120,"icon":114},"\u002Fen\u002Fmail\u002Foverview","en\u002F13.mail\u002F1.overview",{"title":122,"path":123,"stem":124,"icon":114},"SMTP","\u002Fen\u002Fmail\u002Fsmtp","en\u002F13.mail\u002F2.smtp",{"title":126,"path":127,"stem":128,"icon":114},"MJML","\u002Fen\u002Fmail\u002Fmjml","en\u002F13.mail\u002F3.mjml",{"title":130,"icon":131,"path":132,"stem":133,"children":134,"page":44},"notify","i-lucide-bell","\u002Fen\u002Fnotify","en\u002F14.notify",[135,138,142],{"title":52,"path":136,"stem":137,"icon":131},"\u002Fen\u002Fnotify\u002Foverview","en\u002F14.notify\u002F1.overview",{"title":139,"path":140,"stem":141,"icon":131},"Database channel","\u002Fen\u002Fnotify\u002Fdatabase","en\u002F14.notify\u002F2.database",{"title":143,"path":144,"stem":145,"icon":131},"Broadcast channel","\u002Fen\u002Fnotify\u002Fbroadcast","en\u002F14.notify\u002F3.broadcast",{"title":147,"icon":148,"path":149,"stem":150,"children":151,"page":44},"storage","i-lucide-hard-drive","\u002Fen\u002Fstorage","en\u002F15.storage",[152,155,159,163,168],{"title":52,"path":153,"stem":154,"icon":148},"\u002Fen\u002Fstorage\u002Foverview","en\u002F15.storage\u002F1.overview",{"title":156,"path":157,"stem":158},"Disk API","\u002Fen\u002Fstorage\u002Fdisk-api","en\u002F15.storage\u002F2.disk-api",{"title":160,"path":161,"stem":162,"icon":148},"Memory and local disk","\u002Fen\u002Fstorage\u002Flocal","en\u002F15.storage\u002F3.local",{"title":164,"path":165,"stem":166,"icon":167},"S3","\u002Fen\u002Fstorage\u002Fs3","en\u002F15.storage\u002F4.s3","i-lucide-cloud",{"title":169,"path":170,"stem":171,"icon":172},"Testing","\u002Fen\u002Fstorage\u002Ftesting","en\u002F15.storage\u002F5.testing","i-lucide-flask-conical",{"title":174,"icon":175,"path":176,"stem":177,"children":178,"page":44},"telemetry","i-lucide-activity","\u002Fen\u002Ftelemetry","en\u002F16.telemetry",[179,182,187,192],{"title":52,"path":180,"stem":181,"icon":175},"\u002Fen\u002Ftelemetry\u002Foverview","en\u002F16.telemetry\u002F1.overview",{"title":183,"path":184,"stem":185,"icon":186},"Logging","\u002Fen\u002Ftelemetry\u002Flogging","en\u002F16.telemetry\u002F2.logging","i-lucide-scroll-text",{"title":188,"path":189,"stem":190,"icon":191},"OpenTelemetry","\u002Fen\u002Ftelemetry\u002Fopentelemetry","en\u002F16.telemetry\u002F3.opentelemetry","i-lucide-radar",{"title":193,"path":194,"stem":195,"icon":196},"Sentry","\u002Fen\u002Ftelemetry\u002Fsentry","en\u002F16.telemetry\u002F4.sentry","i-lucide-bug",{"title":198,"icon":199,"path":200,"stem":201,"children":202,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F17.reference",[203,208],{"title":204,"path":205,"stem":206,"icon":207},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F17.reference\u002F1.configuration","i-lucide-settings",{"title":209,"path":210,"stem":211,"icon":212},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F17.reference\u002F2.dependencies","i-lucide-package",{"title":214,"icon":215,"path":216,"stem":217,"children":218,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F2.concepts",[219,223,228,232,237],{"title":220,"path":221,"stem":222,"icon":47},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F2.concepts\u002F1.architecture",{"title":224,"path":225,"stem":226,"icon":227},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F2.concepts\u002F2.error-model","i-lucide-shield-alert",{"title":204,"path":229,"stem":230,"icon":231},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F2.concepts\u002F3.configuration","i-lucide-settings-2",{"title":233,"path":234,"stem":235,"icon":236},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F2.concepts\u002F4.codegen-pipeline","i-lucide-file-json",{"title":238,"path":239,"stem":240,"icon":241},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F2.concepts\u002F5.design-patterns","i-lucide-puzzle",{"title":243,"icon":244,"path":245,"stem":246,"children":247,"page":44},"Kit","i-lucide-box","\u002Fen\u002Fkit","en\u002F3.kit",[248,251,256,261,266,270,275],{"title":52,"path":249,"stem":250,"icon":244},"\u002Fen\u002Fkit\u002Foverview","en\u002F3.kit\u002F1.overview",{"title":252,"path":253,"stem":254,"icon":255},"Application lifecycle","\u002Fen\u002Fkit\u002Fapp","en\u002F3.kit\u002F2.app","i-lucide-power",{"title":257,"path":258,"stem":259,"icon":260},"Server","\u002Fen\u002Fkit\u002Fserver","en\u002F3.kit\u002F3.server","i-lucide-server",{"title":262,"path":263,"stem":264,"icon":265},"Worker","\u002Fen\u002Fkit\u002Fworker","en\u002F3.kit\u002F4.worker","i-lucide-cog",{"title":267,"path":268,"stem":269,"icon":70},"Realtime","\u002Fen\u002Fkit\u002Frealtime","en\u002F3.kit\u002F5.realtime",{"title":271,"path":272,"stem":273,"icon":274},"Migrations","\u002Fen\u002Fkit\u002Fmigrations","en\u002F3.kit\u002F6.migrations","i-lucide-file-stack",{"title":276,"path":277,"stem":278,"icon":111},"Social login","\u002Fen\u002Fkit\u002Fsocial-login","en\u002F3.kit\u002F7.social-login",{"title":280,"icon":281,"path":282,"stem":283,"children":284,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F4.cli",[285,288,293,298,303,308,313,318,323,328,333,337,342,347,351],{"title":52,"path":286,"stem":287,"icon":281},"\u002Fen\u002Fcli\u002Foverview","en\u002F4.cli\u002F1.overview",{"title":289,"path":290,"stem":291,"icon":292},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F4.cli\u002F10.add-db","i-lucide-database-zap",{"title":294,"path":295,"stem":296,"icon":297},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F4.cli\u002F11.add-compose","i-lucide-container",{"title":299,"path":300,"stem":301,"icon":302},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F4.cli\u002F12.add-mail","i-lucide-mail-plus",{"title":304,"path":305,"stem":306,"icon":307},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F4.cli\u002F13.add-notification","i-lucide-bell-plus",{"title":309,"path":310,"stem":311,"icon":312},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F4.cli\u002F14.add-realtime","i-lucide-radio-tower",{"title":314,"path":315,"stem":316,"icon":317},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F4.cli\u002F15.add-seeder","i-lucide-sprout",{"title":319,"path":320,"stem":321,"icon":322},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F4.cli\u002F2.new-project","i-lucide-folder-plus",{"title":324,"path":325,"stem":326,"icon":327},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F4.cli\u002F3.new-module","i-lucide-blocks",{"title":329,"path":330,"stem":331,"icon":332},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F4.cli\u002F4.add-surface","i-lucide-layers-2",{"title":334,"path":335,"stem":336,"icon":244},"add core","\u002Fen\u002Fcli\u002Fadd-core","en\u002F4.cli\u002F5.add-core",{"title":338,"path":339,"stem":340,"icon":341},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F4.cli\u002F6.add-handler","i-lucide-webhook",{"title":343,"path":344,"stem":345,"icon":346},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F4.cli\u002F7.new-migration","i-lucide-file-plus",{"title":348,"path":349,"stem":350,"icon":265},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F4.cli\u002F8.worker-generators",{"title":352,"path":353,"stem":354,"icon":355},"upgrade templates","\u002Fen\u002Fcli\u002Fupgrade-templates","en\u002F4.cli\u002F9.upgrade-templates","i-lucide-refresh-cw",{"title":357,"icon":227,"path":358,"stem":359,"children":360,"page":44},"errs","\u002Fen\u002Ferrs","en\u002F5.errs",[361,364,368],{"title":52,"path":362,"stem":363,"icon":227},"\u002Fen\u002Ferrs\u002Foverview","en\u002F5.errs\u002F1.overview",{"title":365,"path":366,"stem":367},"API","\u002Fen\u002Ferrs\u002Fapi","en\u002F5.errs\u002F2.api",{"title":369,"path":370,"stem":371,"icon":227},"Integration","\u002Fen\u002Ferrs\u002Fintegration","en\u002F5.errs\u002F3.integration",{"title":373,"icon":231,"path":374,"stem":375,"children":376,"page":44},"envconf","\u002Fen\u002Fenvconf","en\u002F6.envconf",[377,380],{"title":52,"path":378,"stem":379,"icon":231},"\u002Fen\u002Fenvconf\u002Foverview","en\u002F6.envconf\u002F1.overview",{"title":381,"path":382,"stem":383},"Recipes","\u002Fen\u002Fenvconf\u002Frecipes","en\u002F6.envconf\u002F2.recipes",{"title":385,"icon":386,"path":387,"stem":388,"children":389,"page":44},"httperr","i-lucide-globe","\u002Fen\u002Fhttperr","en\u002F7.httperr",[390,393,398],{"title":52,"path":391,"stem":392,"icon":386},"\u002Fen\u002Fhttperr\u002Foverview","en\u002F7.httperr\u002F1.overview",{"title":394,"path":395,"stem":396,"icon":397},"Error responses","\u002Fen\u002Fhttperr\u002Ferror-responses","en\u002F7.httperr\u002F2.error-responses","i-lucide-octagon-alert",{"title":399,"path":400,"stem":401,"icon":402},"Validation","\u002Fen\u002Fhttperr\u002Fvalidation","en\u002F7.httperr\u002F3.validation","i-lucide-badge-check",{"title":404,"icon":405,"path":406,"stem":407,"children":408,"page":44},"dbx","i-lucide-database","\u002Fen\u002Fdbx","en\u002F8.dbx",[409,412,416,420,425],{"title":52,"path":410,"stem":411,"icon":405},"\u002Fen\u002Fdbx\u002Foverview","en\u002F8.dbx\u002F1.overview",{"title":413,"path":414,"stem":415,"icon":106},"pgx","\u002Fen\u002Fdbx\u002Fpg","en\u002F8.dbx\u002F2.pg",{"title":417,"path":418,"stem":419,"icon":47},"bun","\u002Fen\u002Fdbx\u002Fbunx","en\u002F8.dbx\u002F3.bunx",{"title":421,"path":422,"stem":423,"icon":424},"Transactions","\u002Fen\u002Fdbx\u002Ftransactions","en\u002F8.dbx\u002F4.transactions","i-lucide-git-merge",{"title":426,"path":427,"stem":428,"icon":317},"Seeders","\u002Fen\u002Fdbx\u002Fseed","en\u002F8.dbx\u002F5.seed",{"title":430,"icon":55,"path":431,"stem":432,"children":433,"page":44},"paginate","\u002Fen\u002Fpaginate","en\u002F9.paginate",[434,437],{"title":52,"path":435,"stem":436,"icon":55},"\u002Fen\u002Fpaginate\u002Foverview","en\u002F9.paginate\u002F1.overview",{"title":438,"path":439,"stem":440,"icon":19},"Cursor pagination","\u002Fen\u002Fpaginate\u002Fcursor","en\u002F9.paginate\u002F2.cursor",{"id":442,"title":267,"body":443,"description":1522,"extension":1523,"links":1524,"meta":1525,"navigation":1526,"path":268,"seo":1527,"stem":269,"__hash__":1528},"docs_en\u002Fen\u002F3.kit\u002F5.realtime.md",{"type":444,"value":445,"toc":1513},"minimark",[446,461,466,487,500,504,693,707,711,732,749,939,945,954,1071,1080,1084,1087,1134,1138,1270,1295,1299,1454,1467,1471,1509],[447,448,449,450,455,456,460],"p",{},"The ",[451,452,454],"a",{"href":453},"\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 ",[451,457,459],{"href":458},"\u002Fen\u002Fnotify\u002Fdatabase#database","database channel",", not a replacement for it. This page covers the gateway that holds those connections.",[462,463,465],"h2",{"id":464},"why-a-separate-gateway","Why a separate gateway",[447,467,468,469,473,474,477,478,481,482,486],{},"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: ",[470,471,472],"code",{},"notify\u002Fbroadcast"," is a tiny publisher any process imports (no server, no held state); ",[470,475,476],{},"realtime"," is a dedicated binary (",[470,479,480],{},"cmd\u002Frealtime",", scaffolded by ",[451,483,484],{"href":310},[470,485,309],{},") that only holds connections and relays what gets published.",[447,488,489,490,492,493,495,496,499],{},"The driver seam is deliberate: nothing outside ",[470,491,472],{}," and ",[470,494,476],{}," imports ",[470,497,498],{},"centrifuge"," directly, so a different transport can replace it later without touching application code.",[462,501,503],{"id":502},"publishing","Publishing",[505,506,511],"pre",{"className":507,"code":508,"language":509,"meta":510,"style":510},"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","",[470,512,513,533,540,598,617,636,654,688],{"__ignoreMap":510},[514,515,518,522,526,530],"span",{"class":516,"line":517},"line",1,[514,519,521],{"class":520},"s7zQu","import",[514,523,525],{"class":524},"sMK4o"," \"",[514,527,529],{"class":528},"sBMFI","github.com\u002Fgp-system\u002Fnotify\u002Fbroadcast",[514,531,532],{"class":524},"\"\n",[514,534,536],{"class":516,"line":535},2,[514,537,539],{"emptyLinePlaceholder":538},true,"\n",[514,541,543,547,550,553,556,560,563,566,569,572,574,576,578,581,584,586,588,590,592,595],{"class":516,"line":542},3,[514,544,546],{"class":545},"sTEyZ","pub ",[514,548,549],{"class":524},":=",[514,551,552],{"class":545}," broadcast",[514,554,555],{"class":524},".",[514,557,559],{"class":558},"s2Zo4","MustNewPublisher",[514,561,562],{"class":524},"(",[514,564,565],{"class":545},"ctx",[514,567,568],{"class":524},",",[514,570,571],{"class":545}," cfg",[514,573,555],{"class":524},[514,575,267],{"class":545},[514,577,555],{"class":524},[514,579,580],{"class":558},"BroadcastValkey",[514,582,583],{"class":524},"(),",[514,585,571],{"class":545},[514,587,555],{"class":524},[514,589,267],{"class":545},[514,591,555],{"class":524},[514,593,594],{"class":545},"Broadcast",[514,596,597],{"class":524},")\n",[514,599,601,604,606,609,611,614],{"class":516,"line":600},4,[514,602,603],{"class":545},"notifier ",[514,605,549],{"class":524},[514,607,608],{"class":545}," notify",[514,610,555],{"class":524},[514,612,613],{"class":558},"NewHub",[514,615,616],{"class":524},"(\n",[514,618,620,623,625,628,630,633],{"class":516,"line":619},5,[514,621,622],{"class":545},"    notify",[514,624,555],{"class":524},[514,626,627],{"class":558},"NewMailChannel",[514,629,562],{"class":524},[514,631,632],{"class":545},"mailer",[514,634,635],{"class":524},"),\n",[514,637,639,642,644,647,649,652],{"class":516,"line":638},6,[514,640,641],{"class":545},"    notifydatabase",[514,643,555],{"class":524},[514,645,646],{"class":558},"NewChannel",[514,648,562],{"class":524},[514,650,651],{"class":545},"store",[514,653,635],{"class":524},[514,655,657,659,661,664,666,669,671,673,675,677,679,681,683,686],{"class":516,"line":656},7,[514,658,622],{"class":545},[514,660,555],{"class":524},[514,662,663],{"class":558},"NewBroadcastChannel",[514,665,562],{"class":524},[514,667,668],{"class":545},"pub",[514,670,568],{"class":524},[514,672,571],{"class":545},[514,674,555],{"class":524},[514,676,267],{"class":545},[514,678,555],{"class":524},[514,680,594],{"class":545},[514,682,555],{"class":524},[514,684,685],{"class":545},"ChannelPrefix",[514,687,635],{"class":524},[514,689,691],{"class":516,"line":690},8,[514,692,597],{"class":524},[447,694,695,698,699,702,703,706],{},[470,696,697],{},"Publisher"," is a publish-only ",[470,700,701],{},"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 ",[470,704,705],{},"notify.Hub.Send"," already enforces (database channel before broadcast, same delivery ID in both).",[462,708,710],{"id":709},"the-gateway","The gateway",[447,712,713,715,716,719,720,723,724,727,728,731],{},[470,714,480],{}," 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 ",[470,717,718],{},"centrifugal\u002Fcentrifuge",", the library behind Centrifugo and Grafana Live) under ",[470,721,722],{},"\u002Frealtime\u002F*",", authenticates each connection's JWT in the connect frame (no separate ",[470,725,726],{},"\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 ",[470,729,730],{},"user:\u003Csubject>"," channel. No client-side subscribe logic needed for the common case.",[447,733,734,735,740,741,744,745,748],{},"Shared topic channels are opt-in and explicitly authorized: each connection's JWT resolves to an ",[451,736,737],{"href":94},[470,738,739],{},"rbac.Identity",", and a topic is only ever subscribable if some ",[470,742,743],{},"Allow","\u002F",[470,746,747],{},"AllowPrefix"," callback vouches for that identity.",[505,750,753],{"className":507,"code":751,"filename":752,"language":509,"meta":510,"style":510},"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",[470,754,755,811,866,926,934],{"__ignoreMap":510},[514,756,757,760,762,765,767,770,772,774,776,778,780,782,784,787,791,794,796,798,801,804,808],{"class":516,"line":517},[514,758,759],{"class":545},"err ",[514,761,549],{"class":524},[514,763,764],{"class":545}," realtime",[514,766,555],{"class":524},[514,768,769],{"class":558},"Run",[514,771,562],{"class":524},[514,773,565],{"class":545},[514,775,568],{"class":524},[514,777,571],{"class":545},[514,779,555],{"class":524},[514,781,267],{"class":545},[514,783,568],{"class":524},[514,785,786],{"class":524}," func(",[514,788,790],{"class":789},"sHdIc","topics",[514,792,793],{"class":524}," *",[514,795,476],{"class":528},[514,797,555],{"class":524},[514,799,800],{"class":528},"TopicAuth",[514,802,803],{"class":524},")",[514,805,807],{"class":806},"spNyl"," error",[514,809,810],{"class":524}," {\n",[514,812,813,816,818,820,822,825,829,831,833,835,838,840,843,845,848,850,853,856,859,863],{"class":516,"line":535},[514,814,815],{"class":545},"    topics",[514,817,555],{"class":524},[514,819,743],{"class":558},[514,821,562],{"class":524},[514,823,824],{"class":524},"\"",[514,826,828],{"class":827},"sfazB","announcements",[514,830,824],{"class":524},[514,832,568],{"class":524},[514,834,786],{"class":524},[514,836,837],{"class":789},"id",[514,839,793],{"class":524},[514,841,842],{"class":528},"rbac",[514,844,555],{"class":524},[514,846,847],{"class":528},"Identity",[514,849,803],{"class":524},[514,851,852],{"class":806}," bool",[514,854,855],{"class":524}," {",[514,857,858],{"class":520}," return",[514,860,862],{"class":861},"sfNiH"," true",[514,864,865],{"class":524}," })\n",[514,867,868,870,872,874,876,878,881,883,885,887,889,891,893,895,897,899,901,903,905,908,910,913,915,917,920,922,924],{"class":516,"line":542},[514,869,815],{"class":545},[514,871,555],{"class":524},[514,873,743],{"class":558},[514,875,562],{"class":524},[514,877,824],{"class":524},[514,879,880],{"class":827},"admin-alerts",[514,882,824],{"class":524},[514,884,568],{"class":524},[514,886,786],{"class":524},[514,888,837],{"class":789},[514,890,793],{"class":524},[514,892,842],{"class":528},[514,894,555],{"class":524},[514,896,847],{"class":528},[514,898,803],{"class":524},[514,900,852],{"class":806},[514,902,855],{"class":524},[514,904,858],{"class":520},[514,906,907],{"class":545}," id",[514,909,555],{"class":524},[514,911,912],{"class":558},"HasRole",[514,914,562],{"class":524},[514,916,824],{"class":524},[514,918,919],{"class":827},"admin",[514,921,824],{"class":524},[514,923,803],{"class":524},[514,925,865],{"class":524},[514,927,928,931],{"class":516,"line":600},[514,929,930],{"class":520},"    return",[514,932,933],{"class":524}," nil\n",[514,935,936],{"class":516,"line":619},[514,937,938],{"class":524},"})\n",[447,940,941,942,944],{},"A topic with no registered ",[470,943,743],{}," callback is never subscribable.",[447,946,947,948,950,951,953],{},"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 ",[470,949,743],{}," at startup is not possible: use ",[470,952,747],{},"\ninstead, whose callback receives the full topic string so it can look up\nwhich specific instance is being requested.",[505,955,957],{"className":507,"code":956,"language":509,"meta":510,"style":510},"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",[470,958,959,1004,1039,1067],{"__ignoreMap":510},[514,960,961,963,965,967,969,971,974,976,978,980,982,984,986,988,990,992,995,998,1000,1002],{"class":516,"line":517},[514,962,790],{"class":545},[514,964,555],{"class":524},[514,966,747],{"class":558},[514,968,562],{"class":524},[514,970,824],{"class":524},[514,972,973],{"class":827},"project:",[514,975,824],{"class":524},[514,977,568],{"class":524},[514,979,786],{"class":524},[514,981,837],{"class":789},[514,983,793],{"class":524},[514,985,842],{"class":528},[514,987,555],{"class":524},[514,989,847],{"class":528},[514,991,568],{"class":524},[514,993,994],{"class":789}," topic",[514,996,997],{"class":806}," string",[514,999,803],{"class":524},[514,1001,852],{"class":806},[514,1003,810],{"class":524},[514,1005,1006,1009,1011,1014,1016,1019,1021,1024,1026,1029,1031,1033,1035,1037],{"class":516,"line":535},[514,1007,1008],{"class":545},"    projectID",[514,1010,568],{"class":524},[514,1012,1013],{"class":545}," _ ",[514,1015,549],{"class":524},[514,1017,1018],{"class":545}," strings",[514,1020,555],{"class":524},[514,1022,1023],{"class":558},"CutPrefix",[514,1025,562],{"class":524},[514,1027,1028],{"class":545},"topic",[514,1030,568],{"class":524},[514,1032,525],{"class":524},[514,1034,973],{"class":827},[514,1036,824],{"class":524},[514,1038,597],{"class":524},[514,1040,1041,1043,1046,1048,1051,1053,1055,1057,1060,1062,1065],{"class":516,"line":542},[514,1042,930],{"class":520},[514,1044,1045],{"class":545}," projectAccess",[514,1047,555],{"class":524},[514,1049,1050],{"class":558},"CanView",[514,1052,562],{"class":524},[514,1054,837],{"class":545},[514,1056,555],{"class":524},[514,1058,1059],{"class":545},"Subject",[514,1061,568],{"class":524},[514,1063,1064],{"class":545}," projectID",[514,1066,597],{"class":524},[514,1068,1069],{"class":516,"line":600},[514,1070,938],{"class":524},[447,1072,1073,1074,1076,1077,1079],{},"An exact ",[470,1075,743],{}," match always takes priority over a matching ",[470,1078,747],{},";\namong prefixes, the first one registered that matches wins.",[462,1081,1083],{"id":1082},"scaling-instances-come-and-go-freely","Scaling: instances come and go freely",[447,1085,1086],{},"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.",[1088,1089,1090,1102,1119,1125],"ul",{},[1091,1092,1093,1097,1098,1101],"li",{},[1094,1095,1096],"strong",{},"Scale up",": ",[470,1099,1100],{},"docker compose up -d --scale realtime=N","; a new instance starts with zero state and accumulates subscriptions only as clients land on it.",[1091,1103,1104,1097,1107,1110,1111,1114,1115,1118],{},[1094,1105,1106],{},"Scale down \u002F rolling restart",[470,1108,1109],{},"SIGTERM"," flips a draining flag (",[470,1112,1113],{},"\u002Frealtime\u002Fhealthz"," and new connect attempts return 503, so the load balancer's health check evicts the instance), ",[470,1116,1117],{},"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.",[1091,1120,1121,1124],{},[1094,1122,1123],{},"Crash",": the client side is identical to a graceful restart, just without the drain step.",[1091,1126,1127,1130,1131,1133],{},[1094,1128,1129],{},"Valkey restart",": reconnect and resubscribe are ",[470,1132,498],{},"'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.",[462,1135,1137],{"id":1136},"config","Config",[1139,1140,1141,1157],"table",{},[1142,1143,1144],"thead",{},[1145,1146,1147,1151,1154],"tr",{},[1148,1149,1150],"th",{},"Variable",[1148,1152,1153],{},"Default",[1148,1155,1156],{},"Meaning",[1158,1159,1160,1176,1191,1206,1222,1238,1255],"tbody",{},[1145,1161,1162,1168,1173],{},[1163,1164,1165],"td",{},[470,1166,1167],{},"REALTIME_LISTEN_ADDR",[1163,1169,1170],{},[470,1171,1172],{},":3000",[1163,1174,1175],{},"container-internal listen address",[1145,1177,1178,1183,1188],{},[1163,1179,1180],{},[470,1181,1182],{},"REALTIME_DRAIN_TIMEOUT",[1163,1184,1185],{},[470,1186,1187],{},"20s",[1163,1189,1190],{},"graceful-shutdown budget; keep under the deployment's stop-grace-period",[1145,1192,1193,1198,1203],{},[1163,1194,1195],{},[470,1196,1197],{},"REALTIME_READ_HEADER_TIMEOUT",[1163,1199,1200],{},[470,1201,1202],{},"5s",[1163,1204,1205],{},"Slowloris defense on the connect handshake",[1145,1207,1208,1213,1216],{},[1163,1209,1210],{},[470,1211,1212],{},"REALTIME_CHANNEL_PREFIX",[1163,1214,1215],{},"the project name",[1163,1217,1218,1219],{},"Valkey channel namespace shared with every ",[470,1220,1221],{},"notify\u002Fbroadcast.Publisher",[1145,1223,1224,1229,1235],{},[1163,1225,1226],{},[470,1227,1228],{},"REALTIME_MAX_CONNECTIONS",[1163,1230,1231,1234],{},[470,1232,1233],{},"0"," (unlimited)",[1163,1236,1237],{},"per-instance stream cap; over it, connect attempts get 503",[1145,1239,1240,1245,1248],{},[1163,1241,1242],{},[470,1243,1244],{},"VALKEY_*",[1163,1246,1247],{},"inherited",[1163,1249,1250,1251,1254],{},"reused from ",[470,1252,1253],{},"queue.Config",": the gateway's broker is the same Valkey the queue uses",[1145,1256,1257,1262,1264],{},[1163,1258,1259],{},[470,1260,1261],{},"JWT_*",[1163,1263,1247],{},[1163,1265,1250,1266,1269],{},[470,1267,1268],{},"auth.Config",": the same token issuer every module trusts",[447,1271,1272,1273,1278,1279,1282,1283,1286,1287,1291,1292,555],{},"See the ",[451,1274,1275,1277],{"href":310},[470,1276,309],{}," reference"," for what gets scaffolded (the binary, ",[470,1280,1281],{},"compose.yml","'s Traefik-routed service, ",[470,1284,1285],{},".env.example",") and the ",[451,1288,1289],{"href":305},[470,1290,304],{}," page for wiring the broadcast channel into a notification's ",[470,1293,1294],{},"Via",[462,1296,1298],{"id":1297},"client","Client",[505,1300,1304],{"className":1301,"code":1302,"language":1303,"meta":510,"style":510},"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",[470,1305,1306,1329,1333,1363,1392,1399,1430,1435,1441],{"__ignoreMap":510},[514,1307,1308,1310,1312,1315,1318,1321,1324,1326],{"class":516,"line":517},[514,1309,521],{"class":520},[514,1311,855],{"class":524},[514,1313,1314],{"class":545}," Centrifuge",[514,1316,1317],{"class":524}," }",[514,1319,1320],{"class":520}," from",[514,1322,1323],{"class":524}," '",[514,1325,498],{"class":827},[514,1327,1328],{"class":524},"'\n",[514,1330,1331],{"class":516,"line":535},[514,1332,539],{"emptyLinePlaceholder":538},[514,1334,1335,1338,1341,1344,1347,1349,1351,1354,1357,1359,1361],{"class":516,"line":542},[514,1336,1337],{"class":806},"const",[514,1339,1340],{"class":545}," client ",[514,1342,1343],{"class":524},"=",[514,1345,1346],{"class":524}," new",[514,1348,1314],{"class":558},[514,1350,562],{"class":545},[514,1352,1353],{"class":524},"'",[514,1355,1356],{"class":827},"wss:\u002F\u002Fyour-app.example\u002Frealtime\u002Fconnection\u002Fwebsocket",[514,1358,1353],{"class":524},[514,1360,568],{"class":524},[514,1362,810],{"class":524},[514,1364,1365,1368,1371,1374,1377,1380,1383,1386,1388],{"class":516,"line":600},[514,1366,1367],{"class":558},"    getToken",[514,1369,1370],{"class":524},":",[514,1372,1373],{"class":806}," async",[514,1375,1376],{"class":524}," ()",[514,1378,1379],{"class":806}," =>",[514,1381,1382],{"class":558}," fetchAccessToken",[514,1384,1385],{"class":545},"()",[514,1387,568],{"class":524},[514,1389,1391],{"class":1390},"sHwdD"," \u002F\u002F the same JWT the API uses\n",[514,1393,1394,1397],{"class":516,"line":619},[514,1395,1396],{"class":524},"}",[514,1398,597],{"class":545},[514,1400,1401,1403,1405,1408,1410,1412,1415,1417,1419,1422,1424,1426,1428],{"class":516,"line":638},[514,1402,1297],{"class":545},[514,1404,555],{"class":524},[514,1406,1407],{"class":558},"on",[514,1409,562],{"class":545},[514,1411,1353],{"class":524},[514,1413,1414],{"class":827},"publication",[514,1416,1353],{"class":524},[514,1418,568],{"class":524},[514,1420,1421],{"class":524}," (",[514,1423,565],{"class":789},[514,1425,803],{"class":524},[514,1427,1379],{"class":806},[514,1429,810],{"class":524},[514,1431,1432],{"class":516,"line":656},[514,1433,1434],{"class":1390},"    \u002F\u002F ctx.data: { id, name, payload, at }, id ties back to the database channel's row\n",[514,1436,1437,1439],{"class":516,"line":690},[514,1438,1396],{"class":524},[514,1440,597],{"class":545},[514,1442,1444,1446,1448,1451],{"class":516,"line":1443},9,[514,1445,1297],{"class":545},[514,1447,555],{"class":524},[514,1449,1450],{"class":558},"connect",[514,1452,1453],{"class":545},"()\n",[447,1455,1456,1459,1460,1463,1464,1466],{},[470,1457,1458],{},"centrifuge-js"," handles reconnect backoff and token refresh; the server-side subscription to the recipient's own channel means the client needs no explicit ",[470,1461,1462],{},"subscribe"," call for notifications addressed to it. On reconnect, fetch unread notifications from your module's own inbox endpoint (built on the ",[451,1465,459],{"href":458},") to backfill anything missed while disconnected. The socket is a live poke, the database is the source of truth.",[462,1468,1470],{"id":1469},"related-pages","Related pages",[1088,1472,1473,1480,1496],{},[1091,1474,1475,1479],{},[451,1476,1477],{"href":144},[470,1478,472],{},": the publish-only side any process imports.",[1091,1481,1482,1487,1488,1490,1491,744,1493,1495],{},[451,1483,1484],{"href":94},[470,1485,1486],{},"auth\u002Frbac",": the ",[470,1489,847],{}," a connection's JWT resolves to, and what ",[470,1492,743],{},[470,1494,747],{}," check against.",[1091,1497,1498,1500,1501,1504,1505,1508],{},[451,1499,252],{"href":253},": the shared ",[470,1502,1503],{},"app.Run"," skeleton ",[470,1506,1507],{},"realtime.Run"," builds on.",[1510,1511,1512],"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":510,"searchDepth":535,"depth":535,"links":1514},[1515,1516,1517,1518,1519,1520,1521],{"id":464,"depth":535,"text":465},{"id":502,"depth":535,"text":503},{"id":709,"depth":535,"text":710},{"id":1082,"depth":535,"text":1083},{"id":1136,"depth":535,"text":1137},{"id":1297,"depth":535,"text":1298},{"id":1469,"depth":535,"text":1470},"A dedicated, horizontally scalable gateway that pushes notifications live to connected clients: server→client only, built on centrifuge.","md",null,{},{"icon":70},{"title":267,"description":1522},"cCp0tGyZV8as4uZNIU8ZE4PUFJK1yi7xs_iab_E-9oU",[1530,1532],{"title":262,"path":263,"stem":264,"description":1531,"icon":265,"children":-1},"The background-work chassis, asynq server, outbox relay and schedule runner in one binary, under a shared graceful shutdown.",{"title":271,"path":272,"stem":273,"description":1533,"icon":274,"children":-1},"Timestamp-prefixed SQL files embedded into the binary, with a generated cmd\u002Fmigrate runner, no external tool.",1785445893568]