[{"data":1,"prerenderedAt":2238},["ShallowReactive",2],{"navigation_docs_en":3,"-en-storage-mail-notifications":342,"-en-storage-mail-notifications-surround":2233},[4,45,61,96,132,166,196,217,241,265],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","\u002Fen\u002Fgetting-started","en\u002F1.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Coming from Laravel","\u002Fen\u002Fgetting-started\u002Fcoming-from-laravel","en\u002F1.getting-started\u002F2.coming-from-laravel","i-lucide-arrow-right-left",{"title":21,"path":22,"stem":23,"icon":19},"Coming from Symfony","\u002Fen\u002Fgetting-started\u002Fcoming-from-symfony","en\u002F1.getting-started\u002F3.coming-from-symfony",{"title":25,"path":26,"stem":27,"icon":28},"Installation","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F1.getting-started\u002F4.installation","i-lucide-download",{"title":30,"path":31,"stem":32,"icon":33},"Your first module","\u002Fen\u002Fgetting-started\u002Ffirst-module","en\u002F1.getting-started\u002F5.first-module","i-lucide-package-plus",{"title":35,"path":36,"stem":37,"icon":38},"Project structure","\u002Fen\u002Fgetting-started\u002Fproject-structure","en\u002F1.getting-started\u002F6.project-structure","i-lucide-folder-tree",{"title":40,"path":41,"stem":42,"icon":43},"The shop sample application","\u002Fen\u002Fgetting-started\u002Fsample-app","en\u002F1.getting-started\u002F7.sample-app","i-lucide-shopping-cart",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F10.reference",[51,56],{"title":52,"path":53,"stem":54,"icon":55},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F10.reference\u002F1.configuration","i-lucide-settings",{"title":57,"path":58,"stem":59,"icon":60},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F10.reference\u002F2.dependencies","i-lucide-package",{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F2.concepts",[67,72,77,82,86,91],{"title":68,"path":69,"stem":70,"icon":71},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F2.concepts\u002F1.architecture","i-lucide-layers",{"title":73,"path":74,"stem":75,"icon":76},"Application lifecycle","\u002Fen\u002Fconcepts\u002Fapplication-lifecycle","en\u002F2.concepts\u002F2.application-lifecycle","i-lucide-power",{"title":78,"path":79,"stem":80,"icon":81},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F2.concepts\u002F3.error-model","i-lucide-shield-alert",{"title":52,"path":83,"stem":84,"icon":85},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F2.concepts\u002F4.configuration","i-lucide-settings-2",{"title":87,"path":88,"stem":89,"icon":90},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F2.concepts\u002F5.codegen-pipeline","i-lucide-file-json",{"title":92,"path":93,"stem":94,"icon":95},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F2.concepts\u002F6.design-patterns","i-lucide-puzzle",{"title":97,"icon":98,"path":99,"stem":100,"children":101,"page":44},"HTTP & Routing","i-lucide-globe","\u002Fen\u002Fhttp","en\u002F3.http",[102,107,112,117,122,127],{"title":103,"path":104,"stem":105,"icon":106},"The server core","\u002Fen\u002Fhttp\u002Fserver","en\u002F3.http\u002F1.server","i-lucide-server",{"title":108,"path":109,"stem":110,"icon":111},"The Fiber engine","\u002Fen\u002Fhttp\u002Ffiber","en\u002F3.http\u002F2.fiber","i-lucide-zap",{"title":113,"path":114,"stem":115,"icon":116},"The chi engine","\u002Fen\u002Fhttp\u002Fchi","en\u002F3.http\u002F3.chi","i-lucide-route",{"title":118,"path":119,"stem":120,"icon":121},"Error responses","\u002Fen\u002Fhttp\u002Ferror-responses","en\u002F3.http\u002F4.error-responses","i-lucide-octagon-alert",{"title":123,"path":124,"stem":125,"icon":126},"Validation","\u002Fen\u002Fhttp\u002Fvalidation","en\u002F3.http\u002F5.validation","i-lucide-badge-check",{"title":128,"path":129,"stem":130,"icon":131},"Pagination","\u002Fen\u002Fhttp\u002Fpagination","en\u002F3.http\u002F6.pagination","i-lucide-list-ordered",{"title":133,"icon":134,"path":135,"stem":136,"children":137,"page":44},"Database","i-lucide-database","\u002Fen\u002Fdatabase","en\u002F4.database",[138,142,147,151,156,161],{"title":139,"path":140,"stem":141,"icon":134},"Overview","\u002Fen\u002Fdatabase\u002Foverview","en\u002F4.database\u002F1.overview",{"title":143,"path":144,"stem":145,"icon":146},"pgx","\u002Fen\u002Fdatabase\u002Fpgx","en\u002F4.database\u002F2.pgx","i-lucide-plug",{"title":148,"path":149,"stem":150,"icon":71},"bun","\u002Fen\u002Fdatabase\u002Fbun","en\u002F4.database\u002F3.bun",{"title":152,"path":153,"stem":154,"icon":155},"Transactions","\u002Fen\u002Fdatabase\u002Ftransactions","en\u002F4.database\u002F4.transactions","i-lucide-git-merge",{"title":157,"path":158,"stem":159,"icon":160},"Migrations","\u002Fen\u002Fdatabase\u002Fmigrations","en\u002F4.database\u002F5.migrations","i-lucide-file-stack",{"title":162,"path":163,"stem":164,"icon":165},"Seeders","\u002Fen\u002Fdatabase\u002Fseeding","en\u002F4.database\u002F6.seeding","i-lucide-sprout",{"title":167,"icon":168,"path":169,"stem":170,"children":171,"page":44},"Async & Background","i-lucide-workflow","\u002Fen\u002Fasync","en\u002F5.async",[172,176,181,186,191],{"title":173,"path":174,"stem":175,"icon":131},"Queue","\u002Fen\u002Fasync\u002Fqueue","en\u002F5.async\u002F1.queue",{"title":177,"path":178,"stem":179,"icon":180},"Events","\u002Fen\u002Fasync\u002Fevents","en\u002F5.async\u002F2.events","i-lucide-radio",{"title":182,"path":183,"stem":184,"icon":185},"Outbox","\u002Fen\u002Fasync\u002Foutbox","en\u002F5.async\u002F3.outbox","i-lucide-inbox",{"title":187,"path":188,"stem":189,"icon":190},"Scheduling","\u002Fen\u002Fasync\u002Fscheduler","en\u002F5.async\u002F4.scheduler","i-lucide-calendar-clock",{"title":192,"path":193,"stem":194,"icon":195},"Worker","\u002Fen\u002Fasync\u002Fworker","en\u002F5.async\u002F5.worker","i-lucide-cog",{"title":197,"icon":198,"path":199,"stem":200,"children":201,"page":44},"Security","i-lucide-shield-check","\u002Fen\u002Fsecurity","en\u002F6.security",[202,207,212],{"title":203,"path":204,"stem":205,"icon":206},"Authentication","\u002Fen\u002Fsecurity\u002Fauthentication","en\u002F6.security\u002F1.authentication","i-lucide-key-round",{"title":208,"path":209,"stem":210,"icon":211},"RBAC","\u002Fen\u002Fsecurity\u002Frbac","en\u002F6.security\u002F2.rbac","i-lucide-users",{"title":213,"path":214,"stem":215,"icon":216},"Policies","\u002Fen\u002Fsecurity\u002Fpolicies","en\u002F6.security\u002F3.policies","i-lucide-gavel",{"title":218,"icon":219,"path":220,"stem":221,"children":222,"page":44},"Observability","i-lucide-activity","\u002Fen\u002Fobservability","en\u002F7.observability",[223,226,231,236],{"title":139,"path":224,"stem":225,"icon":219},"\u002Fen\u002Fobservability\u002Foverview","en\u002F7.observability\u002F1.overview",{"title":227,"path":228,"stem":229,"icon":230},"Logging","\u002Fen\u002Fobservability\u002Flogging","en\u002F7.observability\u002F2.logging","i-lucide-scroll-text",{"title":232,"path":233,"stem":234,"icon":235},"OpenTelemetry","\u002Fen\u002Fobservability\u002Fopentelemetry","en\u002F7.observability\u002F3.opentelemetry","i-lucide-radar",{"title":237,"path":238,"stem":239,"icon":240},"Sentry","\u002Fen\u002Fobservability\u002Fsentry","en\u002F7.observability\u002F4.sentry","i-lucide-bug",{"title":242,"icon":243,"path":244,"stem":245,"children":246,"page":44},"Storage & Mail","i-lucide-hard-drive","\u002Fen\u002Fstorage-mail","en\u002F8.storage-mail",[247,251,256,261],{"title":248,"path":249,"stem":250,"icon":243},"Storage","\u002Fen\u002Fstorage-mail\u002Fstorage","en\u002F8.storage-mail\u002F1.storage",{"title":252,"path":253,"stem":254,"icon":255},"Email","\u002Fen\u002Fstorage-mail\u002Fmail","en\u002F8.storage-mail\u002F2.mail","i-lucide-mail",{"title":257,"path":258,"stem":259,"icon":260},"Notifications","\u002Fen\u002Fstorage-mail\u002Fnotifications","en\u002F8.storage-mail\u002F3.notifications","i-lucide-bell",{"title":262,"path":263,"stem":264,"icon":180},"Realtime","\u002Fen\u002Fstorage-mail\u002Frealtime","en\u002F8.storage-mail\u002F4.realtime",{"title":266,"icon":267,"path":268,"stem":269,"children":270,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F9.cli",[271,274,279,284,289,294,299,303,308,313,318,323,328,333,337],{"title":139,"path":272,"stem":273,"icon":267},"\u002Fen\u002Fcli\u002Foverview","en\u002F9.cli\u002F1.overview",{"title":275,"path":276,"stem":277,"icon":278},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F9.cli\u002F10.add-db","i-lucide-database-zap",{"title":280,"path":281,"stem":282,"icon":283},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F9.cli\u002F11.add-compose","i-lucide-container",{"title":285,"path":286,"stem":287,"icon":288},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F9.cli\u002F12.add-mail","i-lucide-mail-plus",{"title":290,"path":291,"stem":292,"icon":293},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F9.cli\u002F13.add-notification","i-lucide-bell-plus",{"title":295,"path":296,"stem":297,"icon":298},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F9.cli\u002F14.add-realtime","i-lucide-radio-tower",{"title":300,"path":301,"stem":302,"icon":165},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F9.cli\u002F15.add-seeder",{"title":304,"path":305,"stem":306,"icon":307},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F9.cli\u002F2.new-project","i-lucide-folder-plus",{"title":309,"path":310,"stem":311,"icon":312},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F9.cli\u002F3.new-module","i-lucide-blocks",{"title":314,"path":315,"stem":316,"icon":317},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F9.cli\u002F4.add-surface","i-lucide-layers-2",{"title":319,"path":320,"stem":321,"icon":322},"add core","\u002Fen\u002Fcli\u002Fadd-core","en\u002F9.cli\u002F5.add-core","i-lucide-box",{"title":324,"path":325,"stem":326,"icon":327},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F9.cli\u002F6.add-handler","i-lucide-webhook",{"title":329,"path":330,"stem":331,"icon":332},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F9.cli\u002F7.new-migration","i-lucide-file-plus",{"title":334,"path":335,"stem":336,"icon":195},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F9.cli\u002F8.worker-generators",{"title":338,"path":339,"stem":340,"icon":341},"upgrade templates","\u002Fen\u002Fcli\u002Fupgrade-templates","en\u002F9.cli\u002F9.upgrade-templates","i-lucide-refresh-cw",{"id":343,"title":257,"body":344,"description":2226,"extension":2227,"links":2228,"meta":2229,"navigation":2230,"path":258,"seo":2231,"stem":259,"__hash__":2232},"docs_en\u002Fen\u002F8.storage-mail\u002F3.notifications.md",{"type":345,"value":346,"toc":2213},"minimark",[347,361,366,386,390,553,571,574,685,699,703,835,842,884,900,904,908,930,965,992,995,1096,1107,1221,1250,1292,1307,1311,1331,1336,1376,1418,1422,1474,1491,1495,1513,1893,2201,2209],[348,349,350,351,355,356,360],"p",{},"A notification is content addressed to a ",[352,353,354],"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 ",[357,358,359],"a",{"href":263},"realtime gateway",". Which channels fire depends entirely on what the notification itself declares.",[362,363,365],"h2",{"id":364},"the-third-deliberate-abstraction","The third deliberate abstraction",[348,367,368,369,372,373,376,377,381,382,385],{},"Alongside ",[357,370,371],{"href":249},"storage"," and ",[357,374,375],{"href":253},"mail",", ",[378,379,380],"code",{},"notify"," is the kit's third conscious exception to ",[357,383,384],{"href":69},"\"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.",[362,387,389],{"id":388},"the-core-types","The core types",[391,392,397],"pre",{"className":393,"code":394,"language":395,"meta":396,"style":396},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import \"github.com\u002Fgp-system\u002Fgpsystem\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","go","",[378,398,399,419,426,441,455,495,501,506,518,532,540,548],{"__ignoreMap":396},[400,401,404,408,412,416],"span",{"class":402,"line":403},"line",1,[400,405,407],{"class":406},"s7zQu","import",[400,409,411],{"class":410},"sMK4o"," \"",[400,413,415],{"class":414},"sBMFI","github.com\u002Fgp-system\u002Fgpsystem\u002Fnotify",[400,417,418],{"class":410},"\"\n",[400,420,422],{"class":402,"line":421},2,[400,423,425],{"emptyLinePlaceholder":424},true,"\n",[400,427,429,432,435,438],{"class":402,"line":428},3,[400,430,431],{"class":410},"type",[400,433,434],{"class":414}," Notification",[400,436,437],{"class":410}," interface",[400,439,440],{"class":410}," {\n",[400,442,444,448,451],{"class":402,"line":443},4,[400,445,447],{"class":446},"s2Zo4","    NotificationName",[400,449,450],{"class":410},"()",[400,452,454],{"class":453},"spNyl"," string\n",[400,456,458,461,464,468,471,474,477,480,483,486,489,492],{"class":402,"line":457},5,[400,459,460],{"class":446},"    Via",[400,462,463],{"class":410},"(",[400,465,467],{"class":466},"sHdIc","ctx",[400,469,470],{"class":414}," context",[400,472,473],{"class":410},".",[400,475,476],{"class":414},"Context",[400,478,479],{"class":410},",",[400,481,482],{"class":466}," r",[400,484,485],{"class":414}," Recipient",[400,487,488],{"class":410},")",[400,490,491],{"class":410}," []",[400,493,494],{"class":453},"string\n",[400,496,498],{"class":402,"line":497},6,[400,499,500],{"class":410},"}\n",[400,502,504],{"class":402,"line":503},7,[400,505,425],{"emptyLinePlaceholder":424},[400,507,509,511,513,516],{"class":402,"line":508},8,[400,510,431],{"class":410},[400,512,485],{"class":414},[400,514,515],{"class":410}," struct",[400,517,440],{"class":410},[400,519,521,525,528],{"class":402,"line":520},9,[400,522,524],{"class":523},"sTEyZ","    ID    ",[400,526,527],{"class":453},"string",[400,529,531],{"class":530},"sHwdD"," \u002F\u002F stable recipient id (an rbac.Identity.Subject in kit-issued JWTs)\n",[400,533,535,538],{"class":402,"line":534},10,[400,536,537],{"class":523},"    Email ",[400,539,494],{"class":453},[400,541,543,546],{"class":402,"line":542},11,[400,544,545],{"class":523},"    Name  ",[400,547,494],{"class":453},[400,549,551],{"class":402,"line":550},12,[400,552,500],{"class":410},[348,554,555,558,559,562,563,566,567,570],{},[378,556,557],{},"NotificationName"," is a stable, dotted identifier (",[378,560,561],{},"\"contact.admin_notification\"","): the database channel persists it, spans and logs carry it, the same convention as ",[378,564,565],{},"events.Event.EventName",". ",[378,568,569],{},"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.",[348,572,573],{},"Per-channel content is a separate, optional interface the channel type-asserts for:",[391,575,577],{"className":393,"code":576,"language":395,"meta":396,"style":396},"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",[378,578,579,590,631,635,646,681],{"__ignoreMap":396},[400,580,581,583,586,588],{"class":402,"line":403},[400,582,431],{"class":410},[400,584,585],{"class":414}," Mailable",[400,587,437],{"class":410},[400,589,440],{"class":410},[400,591,592,595,597,599,601,603,605,607,609,611,613,616,618,620,623,625,628],{"class":402,"line":421},[400,593,594],{"class":446},"    ToMail",[400,596,463],{"class":410},[400,598,467],{"class":466},[400,600,470],{"class":414},[400,602,473],{"class":410},[400,604,476],{"class":414},[400,606,479],{"class":410},[400,608,482],{"class":466},[400,610,485],{"class":414},[400,612,488],{"class":410},[400,614,615],{"class":410}," (*",[400,617,375],{"class":414},[400,619,473],{"class":410},[400,621,622],{"class":414},"Message",[400,624,479],{"class":410},[400,626,627],{"class":453}," error",[400,629,630],{"class":410},")\n",[400,632,633],{"class":402,"line":428},[400,634,500],{"class":410},[400,636,637,639,642,644],{"class":402,"line":443},[400,638,431],{"class":410},[400,640,641],{"class":414}," Databasable",[400,643,437],{"class":410},[400,645,440],{"class":410},[400,647,648,651,653,655,657,659,661,663,665,667,669,672,675,677,679],{"class":402,"line":457},[400,649,650],{"class":446},"    ToDatabase",[400,652,463],{"class":410},[400,654,467],{"class":466},[400,656,470],{"class":414},[400,658,473],{"class":410},[400,660,476],{"class":414},[400,662,479],{"class":410},[400,664,482],{"class":466},[400,666,485],{"class":414},[400,668,488],{"class":410},[400,670,671],{"class":410}," (",[400,673,674],{"class":414},"any",[400,676,479],{"class":410},[400,678,627],{"class":453},[400,680,630],{"class":410},[400,682,683],{"class":402,"line":497},[400,684,500],{"class":410},[348,686,687,688,690,691,694,695,698],{},"If ",[378,689,569],{}," names a channel the notification does not implement the content interface for, that channel returns ",[378,692,693],{},"notify.ErrNoContent","; attempted regardless (see ",[378,696,697],{},"Hub.Send"," below), never silently skipped.",[362,700,702],{"id":701},"the-hub","The Hub",[391,704,706],{"className":393,"code":705,"language":395,"meta":396,"style":396},"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",[378,707,708,727,745,762,766],{"__ignoreMap":396},[400,709,710,713,716,719,721,724],{"class":402,"line":403},[400,711,712],{"class":523},"hub ",[400,714,715],{"class":410},":=",[400,717,718],{"class":523}," notify",[400,720,473],{"class":410},[400,722,723],{"class":446},"NewHub",[400,725,726],{"class":410},"(\n",[400,728,729,732,734,737,739,742],{"class":402,"line":421},[400,730,731],{"class":523},"    notify",[400,733,473],{"class":410},[400,735,736],{"class":446},"NewMailChannel",[400,738,463],{"class":410},[400,740,741],{"class":523},"mailer",[400,743,744],{"class":410},"),\n",[400,746,747,750,752,755,757,760],{"class":402,"line":428},[400,748,749],{"class":523},"    notifydatabase",[400,751,473],{"class":410},[400,753,754],{"class":446},"NewChannel",[400,756,463],{"class":410},[400,758,759],{"class":523},"store",[400,761,744],{"class":410},[400,763,764],{"class":402,"line":443},[400,765,630],{"class":410},[400,767,768,771,773,776,778,781,783,785,787,789,791,794,797,800,803,805,809,812,814,817,819,821,824,826,829,832],{"class":402,"line":457},[400,769,770],{"class":523},"err ",[400,772,715],{"class":410},[400,774,775],{"class":523}," hub",[400,777,473],{"class":410},[400,779,780],{"class":446},"Send",[400,782,463],{"class":410},[400,784,467],{"class":523},[400,786,479],{"class":410},[400,788,718],{"class":414},[400,790,473],{"class":410},[400,792,793],{"class":414},"Recipient",[400,795,796],{"class":410},"{",[400,798,799],{"class":523},"ID",[400,801,802],{"class":410},":",[400,804,411],{"class":410},[400,806,808],{"class":807},"sfazB","u1",[400,810,811],{"class":410},"\"",[400,813,479],{"class":410},[400,815,816],{"class":523}," Email",[400,818,802],{"class":410},[400,820,411],{"class":410},[400,822,823],{"class":807},"user@example.com",[400,825,811],{"class":410},[400,827,828],{"class":410},"},",[400,830,831],{"class":414}," OrderShipped",[400,833,834],{"class":410},"{...})\n",[348,836,837,839,840,802],{},[378,838,723],{}," panics on a duplicate channel name: a boot-time wiring mistake, not a runtime condition. ",[378,841,780],{},[843,844,845,855,866,877],"ol",{},[846,847,848,849,671,851,854],"li",{},"Stamps a delivery ID into ",[378,850,467],{},[378,852,853],{},"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.",[846,856,857,858,860,861,865],{},"Attempts every channel ",[378,859,569],{}," names, ",[862,863,864],"strong",{},"in order",", synchronously, in the caller's goroutine.",[846,867,868,869,872,873,876],{},"A channel name with no registered ",[378,870,871],{},"Channel"," is skipped silently (discard semantics, the same \"not configured\" story ",[378,874,875],{},"smtp.NewIfConfigured"," already has).",[846,878,879,880,883],{},"Every named channel is attempted regardless of an earlier failure; errors are joined (",[378,881,882],{},"errors.Join","), each wrapped with the channel and notification name.",[348,885,886,887,889,890,892,893,896,897,899],{},"There is no built-in queued mode: ",[378,888,380],{}," follows the same rule ",[378,891,375],{}," does. Dispatch an ",[357,894,895],{"href":178},"event"," and call ",[378,898,697],{}," 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.",[362,901,903],{"id":902},"channels","Channels",[905,906,907],"h3",{"id":375},"Mail",[391,909,911],{"className":393,"code":910,"language":395,"meta":396,"style":396},"notify.NewMailChannel(mailer) \u002F\u002F mailer: any mail.Mailer (SMTP, discard, memory)\n",[378,912,913],{"__ignoreMap":396},[400,914,915,917,919,921,923,925,927],{"class":402,"line":403},[400,916,380],{"class":523},[400,918,473],{"class":410},[400,920,736],{"class":446},[400,922,463],{"class":410},[400,924,741],{"class":523},[400,926,488],{"class":410},[400,928,929],{"class":530}," \u002F\u002F mailer: any mail.Mailer (SMTP, discard, memory)\n",[348,931,932,933,936,937,941,942,944,945,948,949,952,953,956,957,960,961,964],{},"Delegates to the same kit ",[378,934,935],{},"mail.Mailer"," ",[357,938,939],{"href":286},[378,940,285],{}," wires: a project's mail transport is shared between direct ",[378,943,935],{}," sends and notifications. If ",[378,946,947],{},"ToMail","'s returned ",[378,950,951],{},"*mail.Message"," has no ",[378,954,955],{},"To"," set, the channel routes it to ",[378,958,959],{},"r.Email","\u002F",[378,962,963],{},"r.Name"," automatically.",[348,966,967,372,969,971,972,974,975,977,978,980,981,960,984,987,988,991],{},[378,968,285],{},[378,970,290],{}," are independent and compose: use ",[378,973,285],{}," for content sent straight to an address with no recipient concept (a contact-form confirmation); use ",[378,976,290],{}," for content addressed to a recipient, possibly on several channels. A notification's ",[378,979,947],{}," reuses the same ",[378,982,983],{},"mail.Message",[378,985,986],{},"mail.Template"," builders a mail notifier's ",[378,989,990],{},"Notify"," does.",[905,993,133],{"id":994},"database",[391,996,998],{"className":393,"code":997,"language":395,"meta":396,"style":396},"import notifydatabase \"github.com\u002Fgp-system\u002Fgpsystem\u002Fnotify\u002Fdatabase\"\n\nstore := notifydatabase.NewStore(pg.NewDB(pool))       \u002F\u002F pgx\nstore := notifydatabasebunx.NewStore(bunDB)             \u002F\u002F bun, via notify\u002Fdatabase\u002Fbunx\nchannel := notifydatabase.NewChannel(store)\n",[378,999,1000,1014,1018,1054,1077],{"__ignoreMap":396},[400,1001,1002,1004,1007,1009,1012],{"class":402,"line":403},[400,1003,407],{"class":406},[400,1005,1006],{"class":523}," notifydatabase ",[400,1008,811],{"class":410},[400,1010,1011],{"class":414},"github.com\u002Fgp-system\u002Fgpsystem\u002Fnotify\u002Fdatabase",[400,1013,418],{"class":410},[400,1015,1016],{"class":402,"line":421},[400,1017,425],{"emptyLinePlaceholder":424},[400,1019,1020,1023,1025,1028,1030,1033,1035,1038,1040,1043,1045,1048,1051],{"class":402,"line":428},[400,1021,1022],{"class":523},"store ",[400,1024,715],{"class":410},[400,1026,1027],{"class":523}," notifydatabase",[400,1029,473],{"class":410},[400,1031,1032],{"class":446},"NewStore",[400,1034,463],{"class":410},[400,1036,1037],{"class":523},"pg",[400,1039,473],{"class":410},[400,1041,1042],{"class":446},"NewDB",[400,1044,463],{"class":410},[400,1046,1047],{"class":523},"pool",[400,1049,1050],{"class":410},"))",[400,1052,1053],{"class":530},"       \u002F\u002F pgx\n",[400,1055,1056,1058,1060,1063,1065,1067,1069,1072,1074],{"class":402,"line":443},[400,1057,1022],{"class":523},[400,1059,715],{"class":410},[400,1061,1062],{"class":523}," notifydatabasebunx",[400,1064,473],{"class":410},[400,1066,1032],{"class":446},[400,1068,463],{"class":410},[400,1070,1071],{"class":523},"bunDB",[400,1073,488],{"class":410},[400,1075,1076],{"class":530},"             \u002F\u002F bun, via notify\u002Fdatabase\u002Fbunx\n",[400,1078,1079,1082,1084,1086,1088,1090,1092,1094],{"class":402,"line":457},[400,1080,1081],{"class":523},"channel ",[400,1083,715],{"class":410},[400,1085,1027],{"class":523},[400,1087,473],{"class":410},[400,1089,754],{"class":446},[400,1091,463],{"class":410},[400,1093,759],{"class":523},[400,1095,630],{"class":410},[348,1097,1098,1099,1102,1103,1106],{},"Persists ",[378,1100,1101],{},"Databasable"," notifications into a ",[378,1104,1105],{},"notifications"," table:",[391,1108,1112],{"className":1109,"code":1110,"language":1111,"meta":396,"style":396},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","CREATE TABLE notifications (\n    id           uuid        PRIMARY KEY,\n    recipient_id text        NOT NULL,\n    name         text        NOT NULL,\n    payload      jsonb       NOT NULL DEFAULT '{}'::jsonb,\n    read_at      timestamptz,\n    created_at   timestamptz NOT NULL DEFAULT now()\n);\n","sql",[378,1113,1114,1129,1140,1153,1165,1188,1198,1216],{"__ignoreMap":396},[400,1115,1116,1120,1123,1126],{"class":402,"line":403},[400,1117,1119],{"class":1118},"sbssI","CREATE",[400,1121,1122],{"class":1118}," TABLE",[400,1124,1125],{"class":446}," notifications",[400,1127,1128],{"class":523}," (\n",[400,1130,1131,1134,1137],{"class":402,"line":421},[400,1132,1133],{"class":523},"    id           uuid        ",[400,1135,1136],{"class":453},"PRIMARY KEY",[400,1138,1139],{"class":523},",\n",[400,1141,1142,1145,1148,1151],{"class":402,"line":428},[400,1143,1144],{"class":523},"    recipient_id ",[400,1146,1147],{"class":453},"text",[400,1149,1150],{"class":1118},"        NOT NULL",[400,1152,1139],{"class":523},[400,1154,1155,1158,1161,1163],{"class":402,"line":443},[400,1156,1157],{"class":1118},"    name",[400,1159,1160],{"class":453},"         text",[400,1162,1150],{"class":1118},[400,1164,1139],{"class":523},[400,1166,1167,1170,1173,1176,1179,1182,1185],{"class":402,"line":457},[400,1168,1169],{"class":523},"    payload      jsonb       ",[400,1171,1172],{"class":1118},"NOT NULL",[400,1174,1175],{"class":453}," DEFAULT",[400,1177,1178],{"class":410}," '",[400,1180,1181],{"class":807},"{}",[400,1183,1184],{"class":410},"'",[400,1186,1187],{"class":523},"::jsonb,\n",[400,1189,1190,1193,1196],{"class":402,"line":497},[400,1191,1192],{"class":523},"    read_at      ",[400,1194,1195],{"class":453},"timestamptz",[400,1197,1139],{"class":523},[400,1199,1200,1203,1205,1208,1210,1213],{"class":402,"line":503},[400,1201,1202],{"class":523},"    created_at   ",[400,1204,1195],{"class":453},[400,1206,1207],{"class":1118}," NOT NULL",[400,1209,1175],{"class":453},[400,1211,1212],{"class":1118}," now",[400,1214,1215],{"class":410},"()\n",[400,1217,1218],{"class":402,"line":508},[400,1219,1220],{"class":523},");\n",[348,1222,1223,1224,1227,1228,1230,1231,1234,1235,1238,1239,1242,1243,1246,1247,1249],{},"No ",[378,1225,1226],{},"notifiable_type"," column (the kit has no polymorphic user model, a recipient is just a ",[378,1229,527],{}," id), no ",[378,1232,1233],{},"updated_at"," (rows only ever gain ",[378,1236,1237],{},"read_at","). The channel requires ",[378,1240,1241],{},"Recipient.ID","; an on-demand send (no ID) errors on this channel: name only ",[378,1244,1245],{},"notify.ChannelMail"," in ",[378,1248,569],{}," for those.",[348,1251,1252,1253,1256,1257,376,1260,671,1263,1266,1267,376,1270,376,1273,1276,1277,1280,1281,1284,1285,1287,1288,1291],{},"The ",[378,1254,1255],{},"Store"," interface covers the universal 90%: ",[378,1258,1259],{},"Insert",[378,1261,1262],{},"List",[378,1264,1265],{},"ListOptions{UnreadOnly, Limit, Offset}","), ",[378,1268,1269],{},"CountUnread",[378,1271,1272],{},"MarkRead",[378,1274,1275],{},"MarkAllRead",". Every read\u002Fupdate takes ",[378,1278,1279],{},"recipientID"," explicitly, so a handler built on top can never touch another recipient's rows by accident. Building the actual HTTP inbox (response DTOs, the ",[378,1282,1283],{},"rbac.Identity.Subject"," → ",[378,1286,1279],{}," guard) is your module's own territory: the kit stops at the Store, the same boundary the ",[357,1289,1290],{"href":183},"outbox"," draws around its own table.",[348,1293,1294,1295,1298,1299,1302,1303,1306],{},"There is no ",[378,1296,1297],{},"NOTIFY_*"," env config: channel enablement ",[352,1300,1301],{},"is"," the wiring; the ",[378,1304,1305],{},"Hub"," contains exactly the channels the entry point constructs.",[905,1308,1310],{"id":1309},"broadcast","Broadcast",[348,1312,1313,1314,1316,1317,1320,1321,1323,1324,1326,1327,1330],{},"See ",[357,1315,262],{"href":263}," for the full channel: it delivers ",[378,1318,1319],{},"Broadcastable"," (or, failing that, ",[378,1322,1101],{},") notifications to the recipient's own live connection through the ",[357,1325,359],{"href":296},", and only wires in once ",[378,1328,1329],{},"gpsystem add realtime"," has run.",[362,1332,1334],{"id":1333},"add-notification",[378,1335,290],{},[391,1337,1341],{"className":1338,"code":1339,"language":1340,"meta":396,"style":396},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","gpsystem add notification \u003Cmodule> \u003Cname>\n","sh",[378,1342,1343],{"__ignoreMap":396},[400,1344,1345,1348,1351,1354,1357,1360,1363,1366,1368,1371,1373],{"class":402,"line":403},[400,1346,1347],{"class":414},"gpsystem",[400,1349,1350],{"class":807}," add",[400,1352,1353],{"class":807}," notification",[400,1355,1356],{"class":410}," \u003C",[400,1358,1359],{"class":807},"modul",[400,1361,1362],{"class":523},"e",[400,1364,1365],{"class":410},">",[400,1367,1356],{"class":410},[400,1369,1370],{"class":807},"nam",[400,1372,1362],{"class":523},[400,1374,1375],{"class":410},">\n",[348,1377,1378,1379,1382,1383,1385,1386,1389,1390,1392,1393,960,1395,1389,1397,1400,1401,1404,1405,1408,1409,1411,1412,1417],{},"Generates ",[378,1380,1381],{},"internal\u002Fmodules\u002F\u003Cmodule>\u002Fnotifications\u002F\u003Cname>.go",": an empty payload struct, ",[378,1384,569],{}," defaulting to ",[378,1387,1388],{},"[database, mail]",", a ",[378,1391,947],{}," building on the kit's ",[378,1394,983],{},[378,1396,986],{},[378,1398,1399],{},"ToDatabase"," returning the notification itself, plus its ",[378,1402,1403],{},"templates\u002F\u003Cname>.{html,txt}.tmpl",". On the project's first notification, wires a shared ",[378,1406,1407],{},"notify.Sender"," (mail + database channels, plus broadcast once the realtime gateway is present) into every entry point and adds the ",[378,1410,1105],{}," table migration. See the ",[357,1413,1414,1416],{"href":291},[378,1415,290],{}," reference"," for the exact generated shape and the wiring anchors.",[362,1419,1421],{"id":1420},"testing","Testing",[391,1423,1425],{"className":393,"code":1424,"language":395,"meta":396,"style":396},"mem := notify.NewMemory(notify.ChannelDatabase) \u002F\u002F captures Deliveries() for any channel name\nhub := notify.NewHub(mem)\n",[378,1426,1427,1455],{"__ignoreMap":396},[400,1428,1429,1432,1434,1436,1438,1441,1443,1445,1447,1450,1452],{"class":402,"line":403},[400,1430,1431],{"class":523},"mem ",[400,1433,715],{"class":410},[400,1435,718],{"class":523},[400,1437,473],{"class":410},[400,1439,1440],{"class":446},"NewMemory",[400,1442,463],{"class":410},[400,1444,380],{"class":523},[400,1446,473],{"class":410},[400,1448,1449],{"class":523},"ChannelDatabase",[400,1451,488],{"class":410},[400,1453,1454],{"class":530}," \u002F\u002F captures Deliveries() for any channel name\n",[400,1456,1457,1459,1461,1463,1465,1467,1469,1472],{"class":402,"line":421},[400,1458,712],{"class":523},[400,1460,715],{"class":410},[400,1462,718],{"class":523},[400,1464,473],{"class":410},[400,1466,723],{"class":446},[400,1468,463],{"class":410},[400,1470,1471],{"class":523},"mem",[400,1473,630],{"class":410},[348,1475,1476,1477,372,1480,1483,1484,1487,1488,1490],{},"For content-rendering coverage, prefer the real channels over real drivers: ",[378,1478,1479],{},"notify.NewMailChannel(mail.NewMemory())",[378,1481,1482],{},"notifydatabase.NewChannel(notifydatabase.NewMemoryStore())",": ",[378,1485,1486],{},"MemoryStore"," implements the full ",[378,1489,1255],{}," (including the read side), so a product can test its inbox handlers without Postgres.",[362,1492,1494],{"id":1493},"worked-example-the-contact-email-flow","Worked example: the contact-email flow",[348,1496,1252,1497,1501,1502,1505,1506,1509,1510,1512],{},[357,1498,1500],{"href":1499},"\u002Fen\u002Fstorage-mail\u002Fmail#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 ",[378,1503,1504],{},"notify.Notification"," addressed to the admin account (subject ",[378,1507,1508],{},"\"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 ",[378,1511,935],{}," send:",[391,1514,1517],{"className":393,"code":1515,"filename":1516,"language":395,"meta":396,"style":396},"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",[378,1518,1519,1527,1531,1542,1555,1559,1563,1599,1603,1641,1669,1673,1677,1729,1745,1768,1805,1826,1831,1836],{"__ignoreMap":396},[400,1520,1521,1524],{"class":402,"line":403},[400,1522,1523],{"class":410},"package",[400,1525,1526],{"class":414}," notifications\n",[400,1528,1529],{"class":402,"line":421},[400,1530,425],{"emptyLinePlaceholder":424},[400,1532,1533,1535,1538,1540],{"class":402,"line":428},[400,1534,431],{"class":410},[400,1536,1537],{"class":414}," AdminNotification",[400,1539,515],{"class":410},[400,1541,440],{"class":410},[400,1543,1544,1547,1549,1552],{"class":402,"line":443},[400,1545,1546],{"class":414},"    contactmail",[400,1548,473],{"class":410},[400,1550,1551],{"class":414},"ContactNotification",[400,1553,1554],{"class":530}," \u002F\u002F the same payload the requester e-mail renders\n",[400,1556,1557],{"class":402,"line":457},[400,1558,500],{"class":410},[400,1560,1561],{"class":402,"line":497},[400,1562,425],{"emptyLinePlaceholder":424},[400,1564,1565,1568,1570,1573,1575,1578,1580,1583,1586,1589,1591,1594,1596],{"class":402,"line":503},[400,1566,1567],{"class":410},"func",[400,1569,671],{"class":410},[400,1571,1572],{"class":414},"AdminNotification",[400,1574,488],{"class":410},[400,1576,1577],{"class":446}," NotificationName",[400,1579,450],{"class":410},[400,1581,1582],{"class":453}," string",[400,1584,1585],{"class":410}," {",[400,1587,1588],{"class":406}," return",[400,1590,411],{"class":410},[400,1592,1593],{"class":807},"contact.admin_notification",[400,1595,811],{"class":410},[400,1597,1598],{"class":410}," }\n",[400,1600,1601],{"class":402,"line":508},[400,1602,425],{"emptyLinePlaceholder":424},[400,1604,1605,1607,1609,1611,1613,1616,1618,1621,1623,1625,1627,1629,1631,1633,1635,1637,1639],{"class":402,"line":520},[400,1606,1567],{"class":410},[400,1608,671],{"class":410},[400,1610,1572],{"class":414},[400,1612,488],{"class":410},[400,1614,1615],{"class":446}," Via",[400,1617,463],{"class":410},[400,1619,1620],{"class":414},"context",[400,1622,473],{"class":410},[400,1624,476],{"class":414},[400,1626,479],{"class":410},[400,1628,718],{"class":414},[400,1630,473],{"class":410},[400,1632,793],{"class":414},[400,1634,488],{"class":410},[400,1636,491],{"class":410},[400,1638,527],{"class":453},[400,1640,440],{"class":410},[400,1642,1643,1646,1648,1650,1652,1654,1656,1658,1660,1662,1664,1667],{"class":402,"line":534},[400,1644,1645],{"class":406},"    return",[400,1647,491],{"class":410},[400,1649,527],{"class":453},[400,1651,796],{"class":410},[400,1653,380],{"class":523},[400,1655,473],{"class":410},[400,1657,1449],{"class":523},[400,1659,479],{"class":410},[400,1661,718],{"class":523},[400,1663,473],{"class":410},[400,1665,1666],{"class":523},"ChannelMail",[400,1668,500],{"class":410},[400,1670,1671],{"class":402,"line":542},[400,1672,500],{"class":410},[400,1674,1675],{"class":402,"line":550},[400,1676,425],{"emptyLinePlaceholder":424},[400,1678,1680,1682,1684,1687,1689,1691,1694,1696,1698,1700,1702,1704,1706,1708,1710,1712,1714,1717,1719,1721,1723,1725,1727],{"class":402,"line":1679},13,[400,1681,1567],{"class":410},[400,1683,671],{"class":410},[400,1685,1686],{"class":466},"n ",[400,1688,1572],{"class":414},[400,1690,488],{"class":410},[400,1692,1693],{"class":446}," ToMail",[400,1695,463],{"class":410},[400,1697,1620],{"class":414},[400,1699,473],{"class":410},[400,1701,476],{"class":414},[400,1703,479],{"class":410},[400,1705,718],{"class":414},[400,1707,473],{"class":410},[400,1709,793],{"class":414},[400,1711,488],{"class":410},[400,1713,615],{"class":410},[400,1715,1716],{"class":414},"kitmail",[400,1718,473],{"class":410},[400,1720,622],{"class":414},[400,1722,479],{"class":410},[400,1724,627],{"class":453},[400,1726,488],{"class":410},[400,1728,440],{"class":410},[400,1730,1732,1734,1737,1739,1742],{"class":402,"line":1731},14,[400,1733,1645],{"class":406},[400,1735,1736],{"class":523}," kitmail",[400,1738,473],{"class":410},[400,1740,1741],{"class":446},"NewMessage",[400,1743,1744],{"class":410},"().\n",[400,1746,1748,1751,1753,1756,1758,1761,1763,1765],{"class":402,"line":1747},15,[400,1749,1750],{"class":446},"        WithSubject",[400,1752,463],{"class":410},[400,1754,1755],{"class":446},"adminSubjectLine",[400,1757,463],{"class":410},[400,1759,1760],{"class":523},"n",[400,1762,473],{"class":410},[400,1764,1551],{"class":523},[400,1766,1767],{"class":410},")).\n",[400,1769,1771,1774,1776,1778,1780,1783,1785,1788,1790,1792,1795,1797,1799,1802],{"class":402,"line":1770},16,[400,1772,1773],{"class":446},"        WithBody",[400,1775,463],{"class":410},[400,1777,1716],{"class":523},[400,1779,473],{"class":410},[400,1781,1782],{"class":446},"Template",[400,1784,463],{"class":410},[400,1786,1787],{"class":523},"templatesFS",[400,1789,479],{"class":410},[400,1791,411],{"class":410},[400,1793,1794],{"class":807},"templates\u002Fadminnotification.html.tmpl",[400,1796,811],{"class":410},[400,1798,479],{"class":410},[400,1800,1801],{"class":523}," n",[400,1803,1804],{"class":410},").\n",[400,1806,1808,1811,1813,1815,1818,1820,1823],{"class":402,"line":1807},17,[400,1809,1810],{"class":446},"            WithTextTemplate",[400,1812,463],{"class":410},[400,1814,811],{"class":410},[400,1816,1817],{"class":807},"templates\u002Fadminnotification.txt.tmpl",[400,1819,811],{"class":410},[400,1821,1822],{"class":410},")),",[400,1824,1825],{"class":410}," nil\n",[400,1827,1829],{"class":402,"line":1828},18,[400,1830,500],{"class":410},[400,1832,1834],{"class":402,"line":1833},19,[400,1835,425],{"emptyLinePlaceholder":424},[400,1837,1839,1841,1843,1845,1847,1849,1852,1854,1856,1858,1860,1862,1864,1866,1868,1870,1872,1874,1876,1878,1880,1882,1884,1886,1888,1891],{"class":402,"line":1838},20,[400,1840,1567],{"class":410},[400,1842,671],{"class":410},[400,1844,1686],{"class":466},[400,1846,1572],{"class":414},[400,1848,488],{"class":410},[400,1850,1851],{"class":446}," ToDatabase",[400,1853,463],{"class":410},[400,1855,1620],{"class":414},[400,1857,473],{"class":410},[400,1859,476],{"class":414},[400,1861,479],{"class":410},[400,1863,718],{"class":414},[400,1865,473],{"class":410},[400,1867,793],{"class":414},[400,1869,488],{"class":410},[400,1871,671],{"class":410},[400,1873,674],{"class":414},[400,1875,479],{"class":410},[400,1877,627],{"class":453},[400,1879,488],{"class":410},[400,1881,1585],{"class":410},[400,1883,1588],{"class":406},[400,1885,1801],{"class":523},[400,1887,479],{"class":410},[400,1889,1890],{"class":410}," nil",[400,1892,1598],{"class":410},[391,1894,1897],{"className":393,"code":1895,"filename":1896,"language":395,"meta":396,"style":396},"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",[378,1898,1899,1946,1972,2003,2047,2051,2076,2118,2138,2143,2167,2192,2197],{"__ignoreMap":396},[400,1900,1901,1903,1906,1908,1911,1914,1916,1919,1921,1924,1926,1928,1930,1932,1935,1937,1940,1942,1944],{"class":402,"line":403},[400,1902,1567],{"class":410},[400,1904,1905],{"class":446}," sendContactEmail",[400,1907,463],{"class":410},[400,1909,1910],{"class":466},"deps",[400,1912,1913],{"class":414}," contact",[400,1915,473],{"class":410},[400,1917,1918],{"class":414},"Dependencies",[400,1920,488],{"class":410},[400,1922,1923],{"class":410}," func(",[400,1925,1620],{"class":414},[400,1927,473],{"class":410},[400,1929,476],{"class":414},[400,1931,479],{"class":410},[400,1933,1934],{"class":414}," contactevents",[400,1936,473],{"class":410},[400,1938,1939],{"class":414},"ContactSubmitted",[400,1941,488],{"class":410},[400,1943,627],{"class":453},[400,1945,440],{"class":410},[400,1947,1948,1951,1953,1956,1958,1961,1963,1965,1967,1970],{"class":402,"line":421},[400,1949,1950],{"class":523},"    requesterNotifier ",[400,1952,715],{"class":410},[400,1954,1955],{"class":523}," contactmail",[400,1957,473],{"class":410},[400,1959,1960],{"class":446},"New",[400,1962,463],{"class":410},[400,1964,1910],{"class":523},[400,1966,473],{"class":410},[400,1968,1969],{"class":523},"Mailer",[400,1971,630],{"class":410},[400,1973,1974,1976,1978,1980,1982,1984,1986,1988,1991,1993,1995,1997,1999,2001],{"class":402,"line":428},[400,1975,1645],{"class":406},[400,1977,1923],{"class":410},[400,1979,467],{"class":466},[400,1981,470],{"class":414},[400,1983,473],{"class":410},[400,1985,476],{"class":414},[400,1987,479],{"class":410},[400,1989,1990],{"class":466}," ev",[400,1992,1934],{"class":414},[400,1994,473],{"class":410},[400,1996,1939],{"class":414},[400,1998,488],{"class":410},[400,2000,627],{"class":453},[400,2002,440],{"class":410},[400,2004,2005,2008,2010,2012,2014,2016,2018,2021,2023,2025,2027,2029,2031,2033,2035,2037,2039,2042,2045],{"class":402,"line":443},[400,2006,2007],{"class":523},"        payload ",[400,2009,715],{"class":410},[400,2011,1955],{"class":414},[400,2013,473],{"class":410},[400,2015,1551],{"class":414},[400,2017,796],{"class":410},[400,2019,2020],{"class":523},"Name",[400,2022,802],{"class":410},[400,2024,1990],{"class":523},[400,2026,473],{"class":410},[400,2028,2020],{"class":523},[400,2030,479],{"class":410},[400,2032,816],{"class":523},[400,2034,802],{"class":410},[400,2036,1990],{"class":523},[400,2038,473],{"class":410},[400,2040,2041],{"class":523},"Email ",[400,2043,2044],{"class":530},"\u002F* ... *\u002F",[400,2046,500],{"class":410},[400,2048,2049],{"class":402,"line":457},[400,2050,425],{"emptyLinePlaceholder":424},[400,2052,2053,2056,2058,2061,2063,2066,2068,2070,2072,2074],{"class":402,"line":497},[400,2054,2055],{"class":523},"        adminErr ",[400,2057,715],{"class":410},[400,2059,2060],{"class":523}," deps",[400,2062,473],{"class":410},[400,2064,2065],{"class":523},"Notifier",[400,2067,473],{"class":410},[400,2069,780],{"class":446},[400,2071,463],{"class":410},[400,2073,467],{"class":523},[400,2075,1139],{"class":410},[400,2077,2078,2081,2083,2085,2087,2089,2091,2093,2096,2098,2100,2102,2104,2106,2108,2110,2112,2115],{"class":402,"line":503},[400,2079,2080],{"class":414},"            notify",[400,2082,473],{"class":410},[400,2084,793],{"class":414},[400,2086,796],{"class":410},[400,2088,799],{"class":523},[400,2090,802],{"class":410},[400,2092,411],{"class":410},[400,2094,2095],{"class":807},"admin",[400,2097,811],{"class":410},[400,2099,479],{"class":410},[400,2101,816],{"class":523},[400,2103,802],{"class":410},[400,2105,2060],{"class":523},[400,2107,473],{"class":410},[400,2109,907],{"class":523},[400,2111,473],{"class":410},[400,2113,2114],{"class":523},"AdminEmail",[400,2116,2117],{"class":410},"},\n",[400,2119,2120,2123,2125,2127,2129,2131,2133,2136],{"class":402,"line":508},[400,2121,2122],{"class":414},"            contactnotifications",[400,2124,473],{"class":410},[400,2126,1572],{"class":414},[400,2128,796],{"class":410},[400,2130,1551],{"class":523},[400,2132,802],{"class":410},[400,2134,2135],{"class":523}," payload",[400,2137,2117],{"class":410},[400,2139,2140],{"class":402,"line":520},[400,2141,2142],{"class":410},"        )\n",[400,2144,2145,2148,2150,2153,2155,2157,2159,2161,2163,2165],{"class":402,"line":534},[400,2146,2147],{"class":523},"        requesterErr ",[400,2149,715],{"class":410},[400,2151,2152],{"class":523}," requesterNotifier",[400,2154,473],{"class":410},[400,2156,990],{"class":446},[400,2158,463],{"class":410},[400,2160,467],{"class":523},[400,2162,479],{"class":410},[400,2164,2135],{"class":523},[400,2166,630],{"class":410},[400,2168,2169,2172,2175,2177,2180,2182,2185,2187,2190],{"class":402,"line":542},[400,2170,2171],{"class":406},"        return",[400,2173,2174],{"class":523}," errors",[400,2176,473],{"class":410},[400,2178,2179],{"class":446},"Join",[400,2181,463],{"class":410},[400,2183,2184],{"class":523},"adminErr",[400,2186,479],{"class":410},[400,2188,2189],{"class":523}," requesterErr",[400,2191,630],{"class":410},[400,2193,2194],{"class":402,"line":550},[400,2195,2196],{"class":410},"    }\n",[400,2198,2199],{"class":402,"line":1679},[400,2200,500],{"class":410},[348,2202,2203,2205,2206,2208],{},[378,2204,1572],{}," embeds the same ",[378,2207,1551],{}," 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.",[2210,2211,2212],"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 .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}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 .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":396,"searchDepth":421,"depth":421,"links":2214},[2215,2216,2217,2218,2223,2224,2225],{"id":364,"depth":421,"text":365},{"id":388,"depth":421,"text":389},{"id":701,"depth":421,"text":702},{"id":902,"depth":421,"text":903,"children":2219},[2220,2221,2222],{"id":375,"depth":428,"text":907},{"id":994,"depth":428,"text":133},{"id":1309,"depth":428,"text":1310},{"id":1333,"depth":421,"text":290},{"id":1420,"depth":421,"text":1421},{"id":1493,"depth":421,"text":1494},"Multi-channel notifications: one payload, several channels (database, mail, broadcast), driven by a shared Hub.","md",null,{},{"icon":260},{"title":257,"description":2226},"0IL89WQc1FAb85tNAHL-VFqwMzgoATxgpIaypFmi-uQ",[2234,2236],{"title":252,"path":253,"stem":254,"description":2235,"icon":255,"children":-1},"Email sending: fluent Message builder, MJML templates, a memory mailer for tests.",{"title":262,"path":263,"stem":264,"description":2237,"icon":180,"children":-1},"A dedicated, horizontally scalable gateway that pushes notifications live to connected clients: server→client only, built on centrifuge.",1784668710463]