[{"data":1,"prerenderedAt":2192},["ShallowReactive",2],{"navigation_docs_en":3,"-en-notify-overview":441,"-en-notify-overview-surround":2187},[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":52,"body":443,"description":2180,"extension":2181,"links":2182,"meta":2183,"navigation":2184,"path":136,"seo":2185,"stem":137,"__hash__":2186},"docs_en\u002Fen\u002F14.notify\u002F1.overview.md",{"type":444,"value":445,"toc":2169},"minimark",[446,460,508,513,552,624,628,643,647,799,817,820,931,951,955,1085,1092,1133,1148,1152,1174,1208,1234,1248,1253,1291,1334,1337,1389,1414,1418,1437,1817,2127,2135,2139,2165],[447,448,449,450,454,455,459],"p",{},"A notification is content addressed to a ",[451,452,453],"em",{},"recipient"," (a user, not just an e-mail address) that can go out on more than one channel: persisted in a database inbox, sent by mail, pushed live over the ",[456,457,458],"a",{"href":268},"realtime gateway",". Which channels fire depends entirely on what the notification itself declares.",[447,461,462,465,466,469,470,469,473,476,477,481,482,486,487,491,492,496,497,500,501,504,505,507],{},[463,464,130],"code",{}," is a standalone Go module (",[463,467,468],{},"github.com\u002Fgp-system\u002Fnotify",", +",[463,471,472],{},"notify\u002Fdatabase",[463,474,475],{},"notify\u002Fbroadcast","). It builds on ",[456,478,479],{"href":119},[463,480,113],{}," for the mail channel and on ",[456,483,484],{"href":410},[463,485,404],{}," for the database channel, but deliberately does ",[488,489,490],"strong",{},"not"," depend on ",[456,493,494],{"href":53},[463,495,46],{},": broadcast is fire-and-forget pub\u002Fsub, and the durable database channel is a plain insert, so nothing here needs durable job queueing. Asynchronous delivery, when you want it, comes from dispatching an ",[456,498,499],{"href":68},"event"," and calling ",[463,502,503],{},"Hub.Send"," from a listener, the same way ",[463,506,113],{}," does.",[509,510,512],"h2",{"id":511},"install","Install",[514,515,520],"pre",{"className":516,"code":517,"language":518,"meta":519,"style":519},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","go get github.com\u002Fgp-system\u002Fnotify@v0.1.0 # the kit itself uses this tag\ngo get github.com\u002Fgp-system\u002Fnotify@latest\n","sh","",[463,521,522,542],{"__ignoreMap":519},[523,524,527,531,535,538],"span",{"class":525,"line":526},"line",1,[523,528,530],{"class":529},"sBMFI","go",[523,532,534],{"class":533},"sfazB"," get",[523,536,537],{"class":533}," github.com\u002Fgp-system\u002Fnotify@v0.1.0",[523,539,541],{"class":540},"sHwdD"," # the kit itself uses this tag\n",[523,543,545,547,549],{"class":525,"line":544},2,[523,546,530],{"class":529},[523,548,534],{"class":533},[523,550,551],{"class":533}," github.com\u002Fgp-system\u002Fnotify@latest\n",[553,554,555,568],"table",{},[556,557,558],"thead",{},[559,560,561,565],"tr",{},[562,563,564],"th",{},"Import path",[562,566,567],{},"What it adds",[569,570,571,593,611],"tbody",{},[559,572,573,578],{},[574,575,576],"td",{},[463,577,468],{},[574,579,580,581,584,585,588,589,592],{},"the ",[463,582,583],{},"Hub",", ",[463,586,587],{},"Notification","\u002F",[463,590,591],{},"Recipient",", the mail channel",[559,594,595,605],{},[574,596,597,600,601,604],{},[463,598,599],{},"github.com\u002Fgp-system\u002Fnotify\u002Fdatabase"," (+",[463,602,603],{},"\u002Fbunx",")",[574,606,607,608],{},"the durable database channel, see ",[456,609,610],{"href":140},"Database",[559,612,613,618],{},[574,614,615],{},[463,616,617],{},"github.com\u002Fgp-system\u002Fnotify\u002Fbroadcast",[574,619,620,621],{},"the live-push channel and gateway building blocks, see ",[456,622,623],{"href":144},"Broadcast",[509,625,627],{"id":626},"the-third-deliberate-abstraction","The third deliberate abstraction",[447,629,630,631,633,634,584,636,638,639,642],{},"Alongside ",[456,632,147],{"href":153}," and ",[456,635,113],{"href":119},[463,637,130],{}," is the kit's third conscious exception to ",[456,640,641],{"href":221},"\"centralize, don't abstract\"",": a first-party system meant to make building products faster, not a wrapper hiding a single third-party API. It stays thin: one type assertion per channel per send, zero reflection.",[509,644,646],{"id":645},"the-core-types","The core types",[514,648,651],{"className":649,"code":650,"language":530,"meta":519,"style":519},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import \"github.com\u002Fgp-system\u002Fnotify\"\n\ntype Notification interface {\n    NotificationName() string\n    Via(ctx context.Context, r Recipient) []string\n}\n\ntype Recipient struct {\n    ID    string \u002F\u002F stable recipient id (an rbac.Identity.Subject in kit-issued JWTs)\n    Email string\n    Name  string\n}\n",[463,652,653,668,674,689,703,742,748,753,765,778,786,794],{"__ignoreMap":519},[523,654,655,659,663,665],{"class":525,"line":526},[523,656,658],{"class":657},"s7zQu","import",[523,660,662],{"class":661},"sMK4o"," \"",[523,664,468],{"class":529},[523,666,667],{"class":661},"\"\n",[523,669,670],{"class":525,"line":544},[523,671,673],{"emptyLinePlaceholder":672},true,"\n",[523,675,677,680,683,686],{"class":525,"line":676},3,[523,678,679],{"class":661},"type",[523,681,682],{"class":529}," Notification",[523,684,685],{"class":661}," interface",[523,687,688],{"class":661}," {\n",[523,690,692,696,699],{"class":525,"line":691},4,[523,693,695],{"class":694},"s2Zo4","    NotificationName",[523,697,698],{"class":661},"()",[523,700,702],{"class":701},"spNyl"," string\n",[523,704,706,709,712,716,719,722,725,728,731,734,736,739],{"class":525,"line":705},5,[523,707,708],{"class":694},"    Via",[523,710,711],{"class":661},"(",[523,713,715],{"class":714},"sHdIc","ctx",[523,717,718],{"class":529}," context",[523,720,721],{"class":661},".",[523,723,724],{"class":529},"Context",[523,726,727],{"class":661},",",[523,729,730],{"class":714}," r",[523,732,733],{"class":529}," Recipient",[523,735,604],{"class":661},[523,737,738],{"class":661}," []",[523,740,741],{"class":701},"string\n",[523,743,745],{"class":525,"line":744},6,[523,746,747],{"class":661},"}\n",[523,749,751],{"class":525,"line":750},7,[523,752,673],{"emptyLinePlaceholder":672},[523,754,756,758,760,763],{"class":525,"line":755},8,[523,757,679],{"class":661},[523,759,733],{"class":529},[523,761,762],{"class":661}," struct",[523,764,688],{"class":661},[523,766,768,772,775],{"class":525,"line":767},9,[523,769,771],{"class":770},"sTEyZ","    ID    ",[523,773,774],{"class":701},"string",[523,776,777],{"class":540}," \u002F\u002F stable recipient id (an rbac.Identity.Subject in kit-issued JWTs)\n",[523,779,781,784],{"class":525,"line":780},10,[523,782,783],{"class":770},"    Email ",[523,785,741],{"class":701},[523,787,789,792],{"class":525,"line":788},11,[523,790,791],{"class":770},"    Name  ",[523,793,741],{"class":701},[523,795,797],{"class":525,"line":796},12,[523,798,747],{"class":661},[447,800,801,804,805,808,809,812,813,816],{},[463,802,803],{},"NotificationName"," is a stable, dotted identifier (",[463,806,807],{},"\"contact.admin_notification\"","): the database channel persists it, spans and logs carry it, the same convention as ",[463,810,811],{},"events.Event.EventName",". ",[463,814,815],{},"Via"," is evaluated per send, so the same notification type can route differently for different recipients, e.g. database-only when the recipient has no e-mail on file.",[447,818,819],{},"Per-channel content is a separate, optional interface the channel type-asserts for:",[514,821,823],{"className":649,"code":822,"language":530,"meta":519,"style":519},"type Mailable interface {\n    ToMail(ctx context.Context, r Recipient) (*mail.Message, error)\n}\ntype Databasable interface {\n    ToDatabase(ctx context.Context, r Recipient) (any, error)\n}\n",[463,824,825,836,877,881,892,927],{"__ignoreMap":519},[523,826,827,829,832,834],{"class":525,"line":526},[523,828,679],{"class":661},[523,830,831],{"class":529}," Mailable",[523,833,685],{"class":661},[523,835,688],{"class":661},[523,837,838,841,843,845,847,849,851,853,855,857,859,862,864,866,869,871,874],{"class":525,"line":544},[523,839,840],{"class":694},"    ToMail",[523,842,711],{"class":661},[523,844,715],{"class":714},[523,846,718],{"class":529},[523,848,721],{"class":661},[523,850,724],{"class":529},[523,852,727],{"class":661},[523,854,730],{"class":714},[523,856,733],{"class":529},[523,858,604],{"class":661},[523,860,861],{"class":661}," (*",[523,863,113],{"class":529},[523,865,721],{"class":661},[523,867,868],{"class":529},"Message",[523,870,727],{"class":661},[523,872,873],{"class":701}," error",[523,875,876],{"class":661},")\n",[523,878,879],{"class":525,"line":676},[523,880,747],{"class":661},[523,882,883,885,888,890],{"class":525,"line":691},[523,884,679],{"class":661},[523,886,887],{"class":529}," Databasable",[523,889,685],{"class":661},[523,891,688],{"class":661},[523,893,894,897,899,901,903,905,907,909,911,913,915,918,921,923,925],{"class":525,"line":705},[523,895,896],{"class":694},"    ToDatabase",[523,898,711],{"class":661},[523,900,715],{"class":714},[523,902,718],{"class":529},[523,904,721],{"class":661},[523,906,724],{"class":529},[523,908,727],{"class":661},[523,910,730],{"class":714},[523,912,733],{"class":529},[523,914,604],{"class":661},[523,916,917],{"class":661}," (",[523,919,920],{"class":529},"any",[523,922,727],{"class":661},[523,924,873],{"class":701},[523,926,876],{"class":661},[523,928,929],{"class":525,"line":744},[523,930,747],{"class":661},[447,932,933,934,937,938,940,941,943,944,947,948,950],{},"There's a third, ",[463,935,936],{},"Broadcastable",", covered on the ",[456,939,623],{"href":144}," page alongside the channel that consumes it. If ",[463,942,815],{}," names a channel the notification does not implement the content interface for, that channel returns ",[463,945,946],{},"notify.ErrNoContent","; attempted regardless (see ",[463,949,503],{}," below), never silently skipped.",[509,952,954],{"id":953},"the-hub","The Hub",[514,956,958],{"className":649,"code":957,"language":530,"meta":519,"style":519},"hub := notify.NewHub(\n    notify.NewMailChannel(mailer),\n    notifydatabase.NewChannel(store),\n)\nerr := hub.Send(ctx, notify.Recipient{ID: \"u1\", Email: \"user@example.com\"}, OrderShipped{...})\n",[463,959,960,979,997,1014,1018],{"__ignoreMap":519},[523,961,962,965,968,971,973,976],{"class":525,"line":526},[523,963,964],{"class":770},"hub ",[523,966,967],{"class":661},":=",[523,969,970],{"class":770}," notify",[523,972,721],{"class":661},[523,974,975],{"class":694},"NewHub",[523,977,978],{"class":661},"(\n",[523,980,981,984,986,989,991,994],{"class":525,"line":544},[523,982,983],{"class":770},"    notify",[523,985,721],{"class":661},[523,987,988],{"class":694},"NewMailChannel",[523,990,711],{"class":661},[523,992,993],{"class":770},"mailer",[523,995,996],{"class":661},"),\n",[523,998,999,1002,1004,1007,1009,1012],{"class":525,"line":676},[523,1000,1001],{"class":770},"    notifydatabase",[523,1003,721],{"class":661},[523,1005,1006],{"class":694},"NewChannel",[523,1008,711],{"class":661},[523,1010,1011],{"class":770},"store",[523,1013,996],{"class":661},[523,1015,1016],{"class":525,"line":691},[523,1017,876],{"class":661},[523,1019,1020,1023,1025,1028,1030,1033,1035,1037,1039,1041,1043,1045,1048,1051,1054,1056,1059,1062,1064,1067,1069,1071,1074,1076,1079,1082],{"class":525,"line":705},[523,1021,1022],{"class":770},"err ",[523,1024,967],{"class":661},[523,1026,1027],{"class":770}," hub",[523,1029,721],{"class":661},[523,1031,1032],{"class":694},"Send",[523,1034,711],{"class":661},[523,1036,715],{"class":770},[523,1038,727],{"class":661},[523,1040,970],{"class":529},[523,1042,721],{"class":661},[523,1044,591],{"class":529},[523,1046,1047],{"class":661},"{",[523,1049,1050],{"class":770},"ID",[523,1052,1053],{"class":661},":",[523,1055,662],{"class":661},[523,1057,1058],{"class":533},"u1",[523,1060,1061],{"class":661},"\"",[523,1063,727],{"class":661},[523,1065,1066],{"class":770}," Email",[523,1068,1053],{"class":661},[523,1070,662],{"class":661},[523,1072,1073],{"class":533},"user@example.com",[523,1075,1061],{"class":661},[523,1077,1078],{"class":661},"},",[523,1080,1081],{"class":529}," OrderShipped",[523,1083,1084],{"class":661},"{...})\n",[447,1086,1087,1089,1090,1053],{},[463,1088,975],{}," panics on a duplicate channel name: a boot-time wiring mistake, not a runtime condition. ",[463,1091,1032],{},[1093,1094,1095,1105,1115,1126],"ol",{},[1096,1097,1098,1099,917,1101,1104],"li",{},"Stamps a delivery ID into ",[463,1100,715],{},[463,1102,1103],{},"notify.IDFromContext","): the database channel uses it as the row's primary key, the broadcast channel's envelope carries the same ID, so a client can reconcile a live push against a database row without refetching.",[1096,1106,1107,1108,1110,1111,1114],{},"Attempts every channel ",[463,1109,815],{}," names, ",[488,1112,1113],{},"in order",", synchronously, in the caller's goroutine.",[1096,1116,1117,1118,1121,1122,1125],{},"A channel name with no registered ",[463,1119,1120],{},"Channel"," is skipped silently (discard semantics, the same \"not configured\" story ",[463,1123,1124],{},"smtp.NewIfConfigured"," already has).",[1096,1127,1128,1129,1132],{},"Every named channel is attempted regardless of an earlier failure; errors are joined (",[463,1130,1131],{},"errors.Join","), each wrapped with the channel and notification name.",[447,1134,1135,1136,1138,1139,1141,1142,1144,1145,1147],{},"There is no built-in queued mode: ",[463,1137,130],{}," follows the same rule ",[463,1140,113],{}," does. Dispatch an ",[456,1143,499],{"href":68}," and call ",[463,1146,503],{}," from a listener for asynchronous delivery, with the existing at-least-once retry and idempotency guarantees. A listener redelivery can double-insert a database notification; that is an accepted, documented caveat, the same one events already carry.",[509,1149,1151],{"id":1150},"the-mail-channel","The mail channel",[514,1153,1155],{"className":649,"code":1154,"language":530,"meta":519,"style":519},"notify.NewMailChannel(mailer) \u002F\u002F mailer: any mail.Mailer (SMTP, discard, memory)\n",[463,1156,1157],{"__ignoreMap":519},[523,1158,1159,1161,1163,1165,1167,1169,1171],{"class":525,"line":526},[523,1160,130],{"class":770},[523,1162,721],{"class":661},[523,1164,988],{"class":694},[523,1166,711],{"class":661},[523,1168,993],{"class":770},[523,1170,604],{"class":661},[523,1172,1173],{"class":540}," \u002F\u002F mailer: any mail.Mailer (SMTP, discard, memory)\n",[447,1175,1176,1177,1180,1181,1185,1186,1188,1189,1192,1193,1196,1197,1200,1201,588,1204,1207],{},"Delegates to the same kit ",[463,1178,1179],{},"mail.Mailer"," ",[456,1182,1183],{"href":300},[463,1184,299],{}," wires: a project's mail transport is shared between direct ",[463,1187,1179],{}," sends and notifications. If ",[463,1190,1191],{},"ToMail","'s returned ",[463,1194,1195],{},"*mail.Message"," has no ",[463,1198,1199],{},"To"," set, the channel routes it to ",[463,1202,1203],{},"r.Email",[463,1205,1206],{},"r.Name"," automatically.",[447,1209,1210,633,1212,1214,1215,1217,1218,1220,1221,1223,1224,588,1227,1230,1231,507],{},[463,1211,299],{},[463,1213,304],{}," are independent and compose: use ",[463,1216,299],{}," for content sent straight to an address with no recipient concept (a contact-form confirmation); use ",[463,1219,304],{}," for content addressed to a recipient, possibly on several channels. A notification's ",[463,1222,1191],{}," reuses the same ",[463,1225,1226],{},"mail.Message",[463,1228,1229],{},"mail.Template"," builders a mail notifier's ",[463,1232,1233],{},"Notify",[447,1235,1236,1237,1239,1240,1242,1243,1245,1246,721],{},"For the database channel (",[463,1238,472],{},"), see the ",[456,1241,610],{"href":140}," page; for the broadcast channel (",[463,1244,475],{},"), see ",[456,1247,623],{"href":144},[509,1249,1251],{"id":1250},"add-notification",[463,1252,304],{},[514,1254,1256],{"className":516,"code":1255,"language":518,"meta":519,"style":519},"gpsystem add notification \u003Cmodule> \u003Cname>\n",[463,1257,1258],{"__ignoreMap":519},[523,1259,1260,1263,1266,1269,1272,1275,1278,1281,1283,1286,1288],{"class":525,"line":526},[523,1261,1262],{"class":529},"gpsystem",[523,1264,1265],{"class":533}," add",[523,1267,1268],{"class":533}," notification",[523,1270,1271],{"class":661}," \u003C",[523,1273,1274],{"class":533},"modul",[523,1276,1277],{"class":770},"e",[523,1279,1280],{"class":661},">",[523,1282,1271],{"class":661},[523,1284,1285],{"class":533},"nam",[523,1287,1277],{"class":770},[523,1289,1290],{"class":661},">\n",[447,1292,1293,1294,1297,1298,1300,1301,1304,1305,1307,1308,588,1310,1304,1312,1315,1316,1319,1320,1323,1324,1327,1328,1333],{},"Generates ",[463,1295,1296],{},"internal\u002Fmodules\u002F\u003Cmodule>\u002Fnotifications\u002F\u003Cname>.go",": an empty payload struct, ",[463,1299,815],{}," defaulting to ",[463,1302,1303],{},"[database, mail]",", a ",[463,1306,1191],{}," building on the kit's ",[463,1309,1226],{},[463,1311,1229],{},[463,1313,1314],{},"ToDatabase"," returning the notification itself, plus its ",[463,1317,1318],{},"templates\u002F\u003Cname>.{html,txt}.tmpl",". On the project's first notification, wires a shared ",[463,1321,1322],{},"notify.Sender"," (mail + database channels, plus broadcast once the realtime gateway is present) into every entry point and adds the ",[463,1325,1326],{},"notifications"," table migration. See the ",[456,1329,1330,1332],{"href":305},[463,1331,304],{}," reference"," for the exact generated shape and the wiring anchors.",[509,1335,169],{"id":1336},"testing",[514,1338,1340],{"className":649,"code":1339,"language":530,"meta":519,"style":519},"mem := notify.NewMemory(notify.ChannelDatabase) \u002F\u002F captures Deliveries() for any channel name\nhub := notify.NewHub(mem)\n",[463,1341,1342,1370],{"__ignoreMap":519},[523,1343,1344,1347,1349,1351,1353,1356,1358,1360,1362,1365,1367],{"class":525,"line":526},[523,1345,1346],{"class":770},"mem ",[523,1348,967],{"class":661},[523,1350,970],{"class":770},[523,1352,721],{"class":661},[523,1354,1355],{"class":694},"NewMemory",[523,1357,711],{"class":661},[523,1359,130],{"class":770},[523,1361,721],{"class":661},[523,1363,1364],{"class":770},"ChannelDatabase",[523,1366,604],{"class":661},[523,1368,1369],{"class":540}," \u002F\u002F captures Deliveries() for any channel name\n",[523,1371,1372,1374,1376,1378,1380,1382,1384,1387],{"class":525,"line":544},[523,1373,964],{"class":770},[523,1375,967],{"class":661},[523,1377,970],{"class":770},[523,1379,721],{"class":661},[523,1381,975],{"class":694},[523,1383,711],{"class":661},[523,1385,1386],{"class":770},"mem",[523,1388,876],{"class":661},[447,1390,1391,1392,633,1395,1398,1399,1402,1403,1406,1407,633,1410,1413],{},"For content-rendering coverage, prefer the real channels over real drivers: ",[463,1393,1394],{},"notify.NewMailChannel(mail.NewMemory())",[463,1396,1397],{},"notifydatabase.NewChannel(notifydatabase.NewMemoryStore())",": ",[463,1400,1401],{},"MemoryStore"," implements the full ",[463,1404,1405],{},"Store"," (including the read side), so a product can test its inbox handlers without Postgres. See ",[456,1408,610],{"href":1409},"\u002Fen\u002Fnotify\u002Fdatabase#memorystore",[456,1411,623],{"href":1412},"\u002Fen\u002Fnotify\u002Fbroadcast#memory"," for the per-channel test doubles.",[509,1415,1417],{"id":1416},"worked-example-the-contact-email-flow","Worked example: the contact-email flow",[447,1419,1420,1421,1425,1426,1429,1430,1433,1434,1436],{},"The ",[456,1422,1424],{"href":1423},"\u002Fen\u002Fmail\u002Foverview#worked-example-the-contact-email-flow","mail page's worked example"," sends a contact-form submission's admin notification and requester confirmation as two direct mails. Recast as a notification, the admin side becomes a ",[463,1427,1428],{},"notify.Notification"," addressed to the admin account (subject ",[463,1431,1432],{},"\"admin\""," in a single-admin project), delivered on both the database channel (a persisted admin inbox) and the mail channel (the same address the direct send used); while the requester confirmation, which has no recipient concept beyond the form's e-mail address, stays a direct ",[463,1435,1179],{}," send:",[514,1438,1441],{"className":649,"code":1439,"filename":1440,"language":530,"meta":519,"style":519},"package notifications\n\ntype AdminNotification struct {\n    contactmail.ContactNotification \u002F\u002F the same payload the requester e-mail renders\n}\n\nfunc (AdminNotification) NotificationName() string { return \"contact.admin_notification\" }\n\nfunc (AdminNotification) Via(context.Context, notify.Recipient) []string {\n    return []string{notify.ChannelDatabase, notify.ChannelMail}\n}\n\nfunc (n AdminNotification) ToMail(context.Context, notify.Recipient) (*kitmail.Message, error) {\n    return kitmail.NewMessage().\n        WithSubject(adminSubjectLine(n.ContactNotification)).\n        WithBody(kitmail.Template(templatesFS, \"templates\u002Fadminnotification.html.tmpl\", n).\n            WithTextTemplate(\"templates\u002Fadminnotification.txt.tmpl\")), nil\n}\n\nfunc (n AdminNotification) ToDatabase(context.Context, notify.Recipient) (any, error) { return n, nil }\n","internal\u002Fmodules\u002Fcontact\u002Fnotifications\u002Fadminnotification.go",[463,1442,1443,1451,1455,1466,1479,1483,1487,1523,1527,1565,1593,1597,1601,1653,1669,1692,1729,1750,1755,1760],{"__ignoreMap":519},[523,1444,1445,1448],{"class":525,"line":526},[523,1446,1447],{"class":661},"package",[523,1449,1450],{"class":529}," notifications\n",[523,1452,1453],{"class":525,"line":544},[523,1454,673],{"emptyLinePlaceholder":672},[523,1456,1457,1459,1462,1464],{"class":525,"line":676},[523,1458,679],{"class":661},[523,1460,1461],{"class":529}," AdminNotification",[523,1463,762],{"class":661},[523,1465,688],{"class":661},[523,1467,1468,1471,1473,1476],{"class":525,"line":691},[523,1469,1470],{"class":529},"    contactmail",[523,1472,721],{"class":661},[523,1474,1475],{"class":529},"ContactNotification",[523,1477,1478],{"class":540}," \u002F\u002F the same payload the requester e-mail renders\n",[523,1480,1481],{"class":525,"line":705},[523,1482,747],{"class":661},[523,1484,1485],{"class":525,"line":744},[523,1486,673],{"emptyLinePlaceholder":672},[523,1488,1489,1492,1494,1497,1499,1502,1504,1507,1510,1513,1515,1518,1520],{"class":525,"line":750},[523,1490,1491],{"class":661},"func",[523,1493,917],{"class":661},[523,1495,1496],{"class":529},"AdminNotification",[523,1498,604],{"class":661},[523,1500,1501],{"class":694}," NotificationName",[523,1503,698],{"class":661},[523,1505,1506],{"class":701}," string",[523,1508,1509],{"class":661}," {",[523,1511,1512],{"class":657}," return",[523,1514,662],{"class":661},[523,1516,1517],{"class":533},"contact.admin_notification",[523,1519,1061],{"class":661},[523,1521,1522],{"class":661}," }\n",[523,1524,1525],{"class":525,"line":755},[523,1526,673],{"emptyLinePlaceholder":672},[523,1528,1529,1531,1533,1535,1537,1540,1542,1545,1547,1549,1551,1553,1555,1557,1559,1561,1563],{"class":525,"line":767},[523,1530,1491],{"class":661},[523,1532,917],{"class":661},[523,1534,1496],{"class":529},[523,1536,604],{"class":661},[523,1538,1539],{"class":694}," Via",[523,1541,711],{"class":661},[523,1543,1544],{"class":529},"context",[523,1546,721],{"class":661},[523,1548,724],{"class":529},[523,1550,727],{"class":661},[523,1552,970],{"class":529},[523,1554,721],{"class":661},[523,1556,591],{"class":529},[523,1558,604],{"class":661},[523,1560,738],{"class":661},[523,1562,774],{"class":701},[523,1564,688],{"class":661},[523,1566,1567,1570,1572,1574,1576,1578,1580,1582,1584,1586,1588,1591],{"class":525,"line":780},[523,1568,1569],{"class":657},"    return",[523,1571,738],{"class":661},[523,1573,774],{"class":701},[523,1575,1047],{"class":661},[523,1577,130],{"class":770},[523,1579,721],{"class":661},[523,1581,1364],{"class":770},[523,1583,727],{"class":661},[523,1585,970],{"class":770},[523,1587,721],{"class":661},[523,1589,1590],{"class":770},"ChannelMail",[523,1592,747],{"class":661},[523,1594,1595],{"class":525,"line":788},[523,1596,747],{"class":661},[523,1598,1599],{"class":525,"line":796},[523,1600,673],{"emptyLinePlaceholder":672},[523,1602,1604,1606,1608,1611,1613,1615,1618,1620,1622,1624,1626,1628,1630,1632,1634,1636,1638,1641,1643,1645,1647,1649,1651],{"class":525,"line":1603},13,[523,1605,1491],{"class":661},[523,1607,917],{"class":661},[523,1609,1610],{"class":714},"n ",[523,1612,1496],{"class":529},[523,1614,604],{"class":661},[523,1616,1617],{"class":694}," ToMail",[523,1619,711],{"class":661},[523,1621,1544],{"class":529},[523,1623,721],{"class":661},[523,1625,724],{"class":529},[523,1627,727],{"class":661},[523,1629,970],{"class":529},[523,1631,721],{"class":661},[523,1633,591],{"class":529},[523,1635,604],{"class":661},[523,1637,861],{"class":661},[523,1639,1640],{"class":529},"kitmail",[523,1642,721],{"class":661},[523,1644,868],{"class":529},[523,1646,727],{"class":661},[523,1648,873],{"class":701},[523,1650,604],{"class":661},[523,1652,688],{"class":661},[523,1654,1656,1658,1661,1663,1666],{"class":525,"line":1655},14,[523,1657,1569],{"class":657},[523,1659,1660],{"class":770}," kitmail",[523,1662,721],{"class":661},[523,1664,1665],{"class":694},"NewMessage",[523,1667,1668],{"class":661},"().\n",[523,1670,1672,1675,1677,1680,1682,1685,1687,1689],{"class":525,"line":1671},15,[523,1673,1674],{"class":694},"        WithSubject",[523,1676,711],{"class":661},[523,1678,1679],{"class":694},"adminSubjectLine",[523,1681,711],{"class":661},[523,1683,1684],{"class":770},"n",[523,1686,721],{"class":661},[523,1688,1475],{"class":770},[523,1690,1691],{"class":661},")).\n",[523,1693,1695,1698,1700,1702,1704,1707,1709,1712,1714,1716,1719,1721,1723,1726],{"class":525,"line":1694},16,[523,1696,1697],{"class":694},"        WithBody",[523,1699,711],{"class":661},[523,1701,1640],{"class":770},[523,1703,721],{"class":661},[523,1705,1706],{"class":694},"Template",[523,1708,711],{"class":661},[523,1710,1711],{"class":770},"templatesFS",[523,1713,727],{"class":661},[523,1715,662],{"class":661},[523,1717,1718],{"class":533},"templates\u002Fadminnotification.html.tmpl",[523,1720,1061],{"class":661},[523,1722,727],{"class":661},[523,1724,1725],{"class":770}," n",[523,1727,1728],{"class":661},").\n",[523,1730,1732,1735,1737,1739,1742,1744,1747],{"class":525,"line":1731},17,[523,1733,1734],{"class":694},"            WithTextTemplate",[523,1736,711],{"class":661},[523,1738,1061],{"class":661},[523,1740,1741],{"class":533},"templates\u002Fadminnotification.txt.tmpl",[523,1743,1061],{"class":661},[523,1745,1746],{"class":661},")),",[523,1748,1749],{"class":661}," nil\n",[523,1751,1753],{"class":525,"line":1752},18,[523,1754,747],{"class":661},[523,1756,1758],{"class":525,"line":1757},19,[523,1759,673],{"emptyLinePlaceholder":672},[523,1761,1763,1765,1767,1769,1771,1773,1776,1778,1780,1782,1784,1786,1788,1790,1792,1794,1796,1798,1800,1802,1804,1806,1808,1810,1812,1815],{"class":525,"line":1762},20,[523,1764,1491],{"class":661},[523,1766,917],{"class":661},[523,1768,1610],{"class":714},[523,1770,1496],{"class":529},[523,1772,604],{"class":661},[523,1774,1775],{"class":694}," ToDatabase",[523,1777,711],{"class":661},[523,1779,1544],{"class":529},[523,1781,721],{"class":661},[523,1783,724],{"class":529},[523,1785,727],{"class":661},[523,1787,970],{"class":529},[523,1789,721],{"class":661},[523,1791,591],{"class":529},[523,1793,604],{"class":661},[523,1795,917],{"class":661},[523,1797,920],{"class":529},[523,1799,727],{"class":661},[523,1801,873],{"class":701},[523,1803,604],{"class":661},[523,1805,1509],{"class":661},[523,1807,1512],{"class":657},[523,1809,1725],{"class":770},[523,1811,727],{"class":661},[523,1813,1814],{"class":661}," nil",[523,1816,1522],{"class":661},[514,1818,1821],{"className":649,"code":1819,"filename":1820,"language":530,"meta":519,"style":519},"func sendContactEmail(deps contact.Dependencies) func(context.Context, contactevents.ContactSubmitted) error {\n    requesterNotifier := contactmail.New(deps.Mailer)\n    return func(ctx context.Context, ev contactevents.ContactSubmitted) error {\n        payload := contactmail.ContactNotification{Name: ev.Name, Email: ev.Email \u002F* ... *\u002F}\n\n        adminErr := deps.Notifier.Send(ctx,\n            notify.Recipient{ID: \"admin\", Email: deps.Mail.AdminEmail},\n            contactnotifications.AdminNotification{ContactNotification: payload},\n        )\n        requesterErr := requesterNotifier.Notify(ctx, payload)\n        return errors.Join(adminErr, requesterErr)\n    }\n}\n","internal\u002Fmodules\u002Fcontact\u002Flisteners\u002Fsendcontactemail.go",[463,1822,1823,1870,1896,1927,1971,1975,2001,2044,2064,2069,2093,2118,2123],{"__ignoreMap":519},[523,1824,1825,1827,1830,1832,1835,1838,1840,1843,1845,1848,1850,1852,1854,1856,1859,1861,1864,1866,1868],{"class":525,"line":526},[523,1826,1491],{"class":661},[523,1828,1829],{"class":694}," sendContactEmail",[523,1831,711],{"class":661},[523,1833,1834],{"class":714},"deps",[523,1836,1837],{"class":529}," contact",[523,1839,721],{"class":661},[523,1841,1842],{"class":529},"Dependencies",[523,1844,604],{"class":661},[523,1846,1847],{"class":661}," func(",[523,1849,1544],{"class":529},[523,1851,721],{"class":661},[523,1853,724],{"class":529},[523,1855,727],{"class":661},[523,1857,1858],{"class":529}," contactevents",[523,1860,721],{"class":661},[523,1862,1863],{"class":529},"ContactSubmitted",[523,1865,604],{"class":661},[523,1867,873],{"class":701},[523,1869,688],{"class":661},[523,1871,1872,1875,1877,1880,1882,1885,1887,1889,1891,1894],{"class":525,"line":544},[523,1873,1874],{"class":770},"    requesterNotifier ",[523,1876,967],{"class":661},[523,1878,1879],{"class":770}," contactmail",[523,1881,721],{"class":661},[523,1883,1884],{"class":694},"New",[523,1886,711],{"class":661},[523,1888,1834],{"class":770},[523,1890,721],{"class":661},[523,1892,1893],{"class":770},"Mailer",[523,1895,876],{"class":661},[523,1897,1898,1900,1902,1904,1906,1908,1910,1912,1915,1917,1919,1921,1923,1925],{"class":525,"line":676},[523,1899,1569],{"class":657},[523,1901,1847],{"class":661},[523,1903,715],{"class":714},[523,1905,718],{"class":529},[523,1907,721],{"class":661},[523,1909,724],{"class":529},[523,1911,727],{"class":661},[523,1913,1914],{"class":714}," ev",[523,1916,1858],{"class":529},[523,1918,721],{"class":661},[523,1920,1863],{"class":529},[523,1922,604],{"class":661},[523,1924,873],{"class":701},[523,1926,688],{"class":661},[523,1928,1929,1932,1934,1936,1938,1940,1942,1945,1947,1949,1951,1953,1955,1957,1959,1961,1963,1966,1969],{"class":525,"line":691},[523,1930,1931],{"class":770},"        payload ",[523,1933,967],{"class":661},[523,1935,1879],{"class":529},[523,1937,721],{"class":661},[523,1939,1475],{"class":529},[523,1941,1047],{"class":661},[523,1943,1944],{"class":770},"Name",[523,1946,1053],{"class":661},[523,1948,1914],{"class":770},[523,1950,721],{"class":661},[523,1952,1944],{"class":770},[523,1954,727],{"class":661},[523,1956,1066],{"class":770},[523,1958,1053],{"class":661},[523,1960,1914],{"class":770},[523,1962,721],{"class":661},[523,1964,1965],{"class":770},"Email ",[523,1967,1968],{"class":540},"\u002F* ... *\u002F",[523,1970,747],{"class":661},[523,1972,1973],{"class":525,"line":705},[523,1974,673],{"emptyLinePlaceholder":672},[523,1976,1977,1980,1982,1985,1987,1990,1992,1994,1996,1998],{"class":525,"line":744},[523,1978,1979],{"class":770},"        adminErr ",[523,1981,967],{"class":661},[523,1983,1984],{"class":770}," deps",[523,1986,721],{"class":661},[523,1988,1989],{"class":770},"Notifier",[523,1991,721],{"class":661},[523,1993,1032],{"class":694},[523,1995,711],{"class":661},[523,1997,715],{"class":770},[523,1999,2000],{"class":661},",\n",[523,2002,2003,2006,2008,2010,2012,2014,2016,2018,2021,2023,2025,2027,2029,2031,2033,2036,2038,2041],{"class":525,"line":750},[523,2004,2005],{"class":529},"            notify",[523,2007,721],{"class":661},[523,2009,591],{"class":529},[523,2011,1047],{"class":661},[523,2013,1050],{"class":770},[523,2015,1053],{"class":661},[523,2017,662],{"class":661},[523,2019,2020],{"class":533},"admin",[523,2022,1061],{"class":661},[523,2024,727],{"class":661},[523,2026,1066],{"class":770},[523,2028,1053],{"class":661},[523,2030,1984],{"class":770},[523,2032,721],{"class":661},[523,2034,2035],{"class":770},"Mail",[523,2037,721],{"class":661},[523,2039,2040],{"class":770},"AdminEmail",[523,2042,2043],{"class":661},"},\n",[523,2045,2046,2049,2051,2053,2055,2057,2059,2062],{"class":525,"line":755},[523,2047,2048],{"class":529},"            contactnotifications",[523,2050,721],{"class":661},[523,2052,1496],{"class":529},[523,2054,1047],{"class":661},[523,2056,1475],{"class":770},[523,2058,1053],{"class":661},[523,2060,2061],{"class":770}," payload",[523,2063,2043],{"class":661},[523,2065,2066],{"class":525,"line":767},[523,2067,2068],{"class":661},"        )\n",[523,2070,2071,2074,2076,2079,2081,2083,2085,2087,2089,2091],{"class":525,"line":780},[523,2072,2073],{"class":770},"        requesterErr ",[523,2075,967],{"class":661},[523,2077,2078],{"class":770}," requesterNotifier",[523,2080,721],{"class":661},[523,2082,1233],{"class":694},[523,2084,711],{"class":661},[523,2086,715],{"class":770},[523,2088,727],{"class":661},[523,2090,2061],{"class":770},[523,2092,876],{"class":661},[523,2094,2095,2098,2101,2103,2106,2108,2111,2113,2116],{"class":525,"line":788},[523,2096,2097],{"class":657},"        return",[523,2099,2100],{"class":770}," errors",[523,2102,721],{"class":661},[523,2104,2105],{"class":694},"Join",[523,2107,711],{"class":661},[523,2109,2110],{"class":770},"adminErr",[523,2112,727],{"class":661},[523,2114,2115],{"class":770}," requesterErr",[523,2117,876],{"class":661},[523,2119,2120],{"class":525,"line":796},[523,2121,2122],{"class":661},"    }\n",[523,2124,2125],{"class":525,"line":1603},[523,2126,747],{"class":661},[447,2128,2129,2131,2132,2134],{},[463,2130,1496],{}," embeds the same ",[463,2133,1475],{}," payload the requester e-mail renders, so both surfaces stay in sync from one source of truth. The admin now gets a persisted, queryable notification history for free, with no change to the requester-facing side at all.",[509,2136,2138],{"id":2137},"related-pages","Related pages",[2140,2141,2142,2147,2152,2157],"ul",{},[1096,2143,2144,2146],{},[456,2145,610],{"href":140},": the durable, queryable inbox channel.",[1096,2148,2149,2151],{},[456,2150,623],{"href":144},": the live-push channel and its Valkey wiring.",[1096,2153,2154,2156],{},[456,2155,267],{"href":268},": the gateway that holds client connections for the broadcast channel.",[1096,2158,2159,2161,2162,2164],{},[456,2160,2035],{"href":119},": the ",[463,2163,1893],{}," this module's mail channel delegates to.",[2166,2167,2168],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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 .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 .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 .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}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 .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}",{"title":519,"searchDepth":544,"depth":544,"links":2170},[2171,2172,2173,2174,2175,2176,2177,2178,2179],{"id":511,"depth":544,"text":512},{"id":626,"depth":544,"text":627},{"id":645,"depth":544,"text":646},{"id":953,"depth":544,"text":954},{"id":1150,"depth":544,"text":1151},{"id":1250,"depth":544,"text":304},{"id":1336,"depth":544,"text":169},{"id":1416,"depth":544,"text":1417},{"id":2137,"depth":544,"text":2138},"Multi-channel notifications: one payload, several channels (database, mail, broadcast), driven by a shared Hub.","md",null,{},{"icon":131},{"title":52,"description":2180},"KqbsFpM_RTvEBNYZf1F4jekRHoMWQUnrNThzeT0heno",[2188,2190],{"title":126,"path":127,"stem":128,"description":2189,"icon":114,"children":-1},"MJML templating for mail.",{"title":139,"path":140,"stem":141,"description":2191,"icon":131,"children":-1},"The durable database notification channel.",1785445889855]