[{"data":1,"prerenderedAt":971},["ShallowReactive",2],{"\u002Fpost\u002Fnotifications":3},{"id":4,"title":5,"body":6,"createdAt":963,"description":964,"extension":965,"meta":966,"navigation":74,"path":967,"seo":968,"stem":969,"__hash__":970},"posts\u002Fpost\u002Fnotifications.md","Real-Time Notifications with Neo4j GraphQL",{"type":7,"value":8,"toc":953},"minimark",[9,13,18,21,34,38,41,45,50,178,182,458,462,465,896,900,903,906,909,915,946,949],[10,11,12],"p",{},"I recently built a notification system using Neo4j's GraphQL library and Change Data Capture. It turned out way cleaner than expected, so sharing what I learned.",[14,15,17],"h2",{"id":16},"the-problem","The Problem",[10,19,20],{},"Users want instant notifications when someone comments on their posts. My first approach was handling notifications directly in the API handler. There were some downsides to this approach:",[22,23,24,28,31],"ul",{},[25,26,27],"li",{},"Slow API responses waiting for notification processing",[25,29,30],{},"Brittle - email failures broke comment creation",[25,32,33],{},"Messy coupling of business and notification logic",[14,35,37],{"id":36},"the-solution","The Solution",[10,39,40],{},"Neo4j's GraphQL subscriptions with Change Data Capture. Instead of blocking the API, let the database emit events and handle notifications separately.",[14,42,44],{"id":43},"how-it-works","How It Works",[46,47,49],"h3",{"id":48},"schema-setup","Schema Setup",[51,52,57],"pre",{"className":53,"code":54,"language":55,"meta":56,"style":56},"language-typescript shiki shiki-themes github-light github-dark","extend schema @subscription\n\ntype Notification {\n  id: ID! @id\n  createdAt: DateTime! @timestamp(operations: [CREATE])\n  read: Boolean\n  text: String\n}\n\ntype User {\n  Notifications: [Notification!]! @relationship(type: \"HAS_NOTIFICATION\", direction: OUT)\n}\n\ntype DiscussionChannel {\n  SubscribedToNotifications: [User!]!\n    @relationship(type: \"SUBSCRIBED_TO_NOTIFICATIONS\", direction: IN)\n}\n","typescript","",[58,59,60,69,76,90,96,102,108,114,120,125,135,141,146,151,161,167,173],"code",{"__ignoreMap":56},[61,62,65],"span",{"class":63,"line":64},"line",1,[61,66,68],{"class":67},"sVt8B","extend schema @subscription\n",[61,70,72],{"class":63,"line":71},2,[61,73,75],{"emptyLinePlaceholder":74},true,"\n",[61,77,79,83,87],{"class":63,"line":78},3,[61,80,82],{"class":81},"szBVR","type",[61,84,86],{"class":85},"sScJk"," Notification",[61,88,89],{"class":67}," {\n",[61,91,93],{"class":63,"line":92},4,[61,94,95],{"class":67},"  id: ID! @id\n",[61,97,99],{"class":63,"line":98},5,[61,100,101],{"class":67},"  createdAt: DateTime! @timestamp(operations: [CREATE])\n",[61,103,105],{"class":63,"line":104},6,[61,106,107],{"class":67},"  read: Boolean\n",[61,109,111],{"class":63,"line":110},7,[61,112,113],{"class":67},"  text: String\n",[61,115,117],{"class":63,"line":116},8,[61,118,119],{"class":67},"}\n",[61,121,123],{"class":63,"line":122},9,[61,124,75],{"emptyLinePlaceholder":74},[61,126,128,130,133],{"class":63,"line":127},10,[61,129,82],{"class":81},[61,131,132],{"class":85}," User",[61,134,89],{"class":67},[61,136,138],{"class":63,"line":137},11,[61,139,140],{"class":67},"  Notifications: [Notification!]! @relationship(type: \"HAS_NOTIFICATION\", direction: OUT)\n",[61,142,144],{"class":63,"line":143},12,[61,145,119],{"class":67},[61,147,149],{"class":63,"line":148},13,[61,150,75],{"emptyLinePlaceholder":74},[61,152,154,156,159],{"class":63,"line":153},14,[61,155,82],{"class":81},[61,157,158],{"class":85}," DiscussionChannel",[61,160,89],{"class":67},[61,162,164],{"class":63,"line":163},15,[61,165,166],{"class":67},"  SubscribedToNotifications: [User!]!\n",[61,168,170],{"class":63,"line":169},16,[61,171,172],{"class":67},"    @relationship(type: \"SUBSCRIBED_TO_NOTIFICATIONS\", direction: IN)\n",[61,174,176],{"class":63,"line":175},17,[61,177,119],{"class":67},[46,179,181],{"id":180},"event-processing","Event Processing",[51,183,185],{"className":53,"code":184,"language":55,"meta":56,"style":56},"private async processCommentNotification(commentId: string) {\n  const fullComment = await CommentModel.find({\n    where: { id: commentId },\n    selectionSet: `{\n      id text\n      DiscussionChannel { SubscribedToNotifications { username } Discussion { title } }\n      Event { SubscribedToNotifications { username } title }\n      ParentComment { SubscribedToNotifications { username } }\n    }`\n  });\n\n  \u002F\u002F Generate appropriate email content\n  let emailContent;\n  if (fullComment.DiscussionChannel) {\n    emailContent = createCommentNotificationEmail(\n      fullComment.text, fullComment.DiscussionChannel.Discussion.title,\n      commenterUsername, channelName, discussionId, commentId\n    );\n    await this.processDiscussionCommentNotification(fullComment, emailContent);\n  } else if (fullComment.Event) {\n    emailContent = createEventCommentNotificationEmail(\n      fullComment.text, fullComment.Event.title,\n      commenterUsername, channelName, eventId, commentId\n    );\n    await this.processEventCommentNotification(fullComment, emailContent);\n  } else if (fullComment.ParentComment) {\n    emailContent = createCommentReplyNotificationEmail(\n      fullComment.text, contentTitle, commenterUsername, contentUrl\n    );\n    await this.processCommentReplyNotification(fullComment, emailContent);\n  }\n}\n",[58,186,187,198,222,227,236,241,246,251,256,261,266,270,276,284,292,306,311,316,322,340,355,367,373,379,384,398,410,422,428,433,447,453],{"__ignoreMap":56},[61,188,189,192,195],{"class":63,"line":64},[61,190,191],{"class":67},"private async ",[61,193,194],{"class":85},"processCommentNotification",[61,196,197],{"class":67},"(commentId: string) {\n",[61,199,200,203,207,210,213,216,219],{"class":63,"line":71},[61,201,202],{"class":81},"  const",[61,204,206],{"class":205},"sj4cs"," fullComment",[61,208,209],{"class":81}," =",[61,211,212],{"class":81}," await",[61,214,215],{"class":67}," CommentModel.",[61,217,218],{"class":85},"find",[61,220,221],{"class":67},"({\n",[61,223,224],{"class":63,"line":78},[61,225,226],{"class":67},"    where: { id: commentId },\n",[61,228,229,232],{"class":63,"line":92},[61,230,231],{"class":67},"    selectionSet: ",[61,233,235],{"class":234},"sZZnC","`{\n",[61,237,238],{"class":63,"line":98},[61,239,240],{"class":234},"      id text\n",[61,242,243],{"class":63,"line":104},[61,244,245],{"class":234},"      DiscussionChannel { SubscribedToNotifications { username } Discussion { title } }\n",[61,247,248],{"class":63,"line":110},[61,249,250],{"class":234},"      Event { SubscribedToNotifications { username } title }\n",[61,252,253],{"class":63,"line":116},[61,254,255],{"class":234},"      ParentComment { SubscribedToNotifications { username } }\n",[61,257,258],{"class":63,"line":122},[61,259,260],{"class":234},"    }`\n",[61,262,263],{"class":63,"line":127},[61,264,265],{"class":67},"  });\n",[61,267,268],{"class":63,"line":137},[61,269,75],{"emptyLinePlaceholder":74},[61,271,272],{"class":63,"line":143},[61,273,275],{"class":274},"sJ8bj","  \u002F\u002F Generate appropriate email content\n",[61,277,278,281],{"class":63,"line":148},[61,279,280],{"class":81},"  let",[61,282,283],{"class":67}," emailContent;\n",[61,285,286,289],{"class":63,"line":153},[61,287,288],{"class":81},"  if",[61,290,291],{"class":67}," (fullComment.DiscussionChannel) {\n",[61,293,294,297,300,303],{"class":63,"line":163},[61,295,296],{"class":67},"    emailContent ",[61,298,299],{"class":81},"=",[61,301,302],{"class":85}," createCommentNotificationEmail",[61,304,305],{"class":67},"(\n",[61,307,308],{"class":63,"line":169},[61,309,310],{"class":67},"      fullComment.text, fullComment.DiscussionChannel.Discussion.title,\n",[61,312,313],{"class":63,"line":175},[61,314,315],{"class":67},"      commenterUsername, channelName, discussionId, commentId\n",[61,317,319],{"class":63,"line":318},18,[61,320,321],{"class":67},"    );\n",[61,323,325,328,331,334,337],{"class":63,"line":324},19,[61,326,327],{"class":81},"    await",[61,329,330],{"class":205}," this",[61,332,333],{"class":67},".",[61,335,336],{"class":85},"processDiscussionCommentNotification",[61,338,339],{"class":67},"(fullComment, emailContent);\n",[61,341,343,346,349,352],{"class":63,"line":342},20,[61,344,345],{"class":67},"  } ",[61,347,348],{"class":81},"else",[61,350,351],{"class":81}," if",[61,353,354],{"class":67}," (fullComment.Event) {\n",[61,356,358,360,362,365],{"class":63,"line":357},21,[61,359,296],{"class":67},[61,361,299],{"class":81},[61,363,364],{"class":85}," createEventCommentNotificationEmail",[61,366,305],{"class":67},[61,368,370],{"class":63,"line":369},22,[61,371,372],{"class":67},"      fullComment.text, fullComment.Event.title,\n",[61,374,376],{"class":63,"line":375},23,[61,377,378],{"class":67},"      commenterUsername, channelName, eventId, commentId\n",[61,380,382],{"class":63,"line":381},24,[61,383,321],{"class":67},[61,385,387,389,391,393,396],{"class":63,"line":386},25,[61,388,327],{"class":81},[61,390,330],{"class":205},[61,392,333],{"class":67},[61,394,395],{"class":85},"processEventCommentNotification",[61,397,339],{"class":67},[61,399,401,403,405,407],{"class":63,"line":400},26,[61,402,345],{"class":67},[61,404,348],{"class":81},[61,406,351],{"class":81},[61,408,409],{"class":67}," (fullComment.ParentComment) {\n",[61,411,413,415,417,420],{"class":63,"line":412},27,[61,414,296],{"class":67},[61,416,299],{"class":81},[61,418,419],{"class":85}," createCommentReplyNotificationEmail",[61,421,305],{"class":67},[61,423,425],{"class":63,"line":424},28,[61,426,427],{"class":67},"      fullComment.text, contentTitle, commenterUsername, contentUrl\n",[61,429,431],{"class":63,"line":430},29,[61,432,321],{"class":67},[61,434,436,438,440,442,445],{"class":63,"line":435},30,[61,437,327],{"class":81},[61,439,330],{"class":205},[61,441,333],{"class":67},[61,443,444],{"class":85},"processCommentReplyNotification",[61,446,339],{"class":67},[61,448,450],{"class":63,"line":449},31,[61,451,452],{"class":67},"  }\n",[61,454,456],{"class":63,"line":455},32,[61,457,119],{"class":67},[46,459,461],{"id":460},"bulk-notifications-email","Bulk Notifications + Email",[10,463,464],{},"The system handles both in-app notifications and emails in a single batch operation:",[51,466,468],{"className":53,"code":467,"language":55,"meta":56,"style":56},"private async createBatchNotifications(entityType, entityId, notificationText, commenterUsername, emailContent?) {\n  \u002F\u002F 1. Get subscribers with email addresses\n  const entity = await EntityModel.find({\n    where: { id: entityId },\n    selectionSet: `{\n      SubscribedToNotifications {\n        username\n        Email { address }\n      }\n    }`\n  });\n\n  const usersToNotify = entity.SubscribedToNotifications\n    .filter(user => user.username !== commenterUsername);\n\n  \u002F\u002F 2. Send batch emails first\n  if (emailContent) {\n    await this.sendBatchEmails(usersToNotify, emailContent);\n  }\n\n  \u002F\u002F 3. Create in-app notifications\n  const cypherQuery = `\n    MATCH (entity:${entityType} {id: $entityId})\n    MATCH (entity)\u003C-[:SUBSCRIBED_TO_NOTIFICATIONS]-(user:User)\n    WHERE user.username \u003C> $commenterUsername\n    CREATE (notification:Notification {\n      id: randomUUID(),\n      createdAt: datetime(),\n      read: false,\n      text: $notificationText\n    })\n    CREATE (user)-[:HAS_NOTIFICATION]->(notification)\n  `;\n\n  await session.run(cypherQuery, { entityId, commenterUsername, notificationText });\n}\n\nprivate async sendBatchEmails(usersToNotify, emailContent) {\n  try {\n    const emailsToSend = usersToNotify\n      .filter(user => user.email)\n      .map(user => ({\n        to: user.email,\n        from: process.env.SENDGRID_FROM_EMAIL,\n        subject: emailContent.subject,\n        text: emailContent.plainText,\n        html: emailContent.html\n      }));\n\n    await sgMail.send(emailsToSend);\n  } catch (error) {\n    console.error('Email sending failed:', error);\n    \u002F\u002F Continue with in-app notifications even if emails fail\n  }\n}\n",[58,469,470,486,491,507,512,518,523,528,533,538,542,546,550,560,581,585,590,602,616,620,624,629,639,650,655,660,665,670,675,680,685,690,695,704,709,715,720,725,735,741,755,774,791,797,809,815,821,827,833,838,852,858,880,886,891],{"__ignoreMap":56},[61,471,472,474,477,480,483],{"class":63,"line":64},[61,473,191],{"class":67},[61,475,476],{"class":85},"createBatchNotifications",[61,478,479],{"class":67},"(entityType, entityId, notificationText, commenterUsername, emailContent",[61,481,482],{"class":81},"?",[61,484,485],{"class":67},") {\n",[61,487,488],{"class":63,"line":71},[61,489,490],{"class":274},"  \u002F\u002F 1. Get subscribers with email addresses\n",[61,492,493,496,498,500,503,505],{"class":63,"line":78},[61,494,495],{"class":67},"  const entity ",[61,497,299],{"class":81},[61,499,212],{"class":81},[61,501,502],{"class":67}," EntityModel.",[61,504,218],{"class":85},[61,506,221],{"class":67},[61,508,509],{"class":63,"line":92},[61,510,511],{"class":67},"    where: { id: entityId },\n",[61,513,514,516],{"class":63,"line":98},[61,515,231],{"class":67},[61,517,235],{"class":234},[61,519,520],{"class":63,"line":104},[61,521,522],{"class":234},"      SubscribedToNotifications {\n",[61,524,525],{"class":63,"line":110},[61,526,527],{"class":234},"        username\n",[61,529,530],{"class":63,"line":116},[61,531,532],{"class":234},"        Email { address }\n",[61,534,535],{"class":63,"line":122},[61,536,537],{"class":234},"      }\n",[61,539,540],{"class":63,"line":127},[61,541,260],{"class":234},[61,543,544],{"class":63,"line":137},[61,545,265],{"class":67},[61,547,548],{"class":63,"line":143},[61,549,75],{"emptyLinePlaceholder":74},[61,551,552,555,557],{"class":63,"line":148},[61,553,554],{"class":67},"  const usersToNotify ",[61,556,299],{"class":81},[61,558,559],{"class":67}," entity.SubscribedToNotifications\n",[61,561,562,565,569,572,575,578],{"class":63,"line":153},[61,563,564],{"class":67},"    .filter(",[61,566,568],{"class":567},"s4XuR","user",[61,570,571],{"class":81}," =>",[61,573,574],{"class":67}," user.username ",[61,576,577],{"class":81},"!==",[61,579,580],{"class":67}," commenterUsername);\n",[61,582,583],{"class":63,"line":163},[61,584,75],{"emptyLinePlaceholder":74},[61,586,587],{"class":63,"line":169},[61,588,589],{"class":274},"  \u002F\u002F 2. Send batch emails first\n",[61,591,592,594,597,600],{"class":63,"line":175},[61,593,288],{"class":85},[61,595,596],{"class":67}," (",[61,598,599],{"class":567},"emailContent",[61,601,485],{"class":67},[61,603,604,606,608,610,613],{"class":63,"line":318},[61,605,327],{"class":81},[61,607,330],{"class":205},[61,609,333],{"class":67},[61,611,612],{"class":85},"sendBatchEmails",[61,614,615],{"class":67},"(usersToNotify, emailContent);\n",[61,617,618],{"class":63,"line":324},[61,619,452],{"class":67},[61,621,622],{"class":63,"line":342},[61,623,75],{"emptyLinePlaceholder":74},[61,625,626],{"class":63,"line":357},[61,627,628],{"class":274},"  \u002F\u002F 3. Create in-app notifications\n",[61,630,631,634,636],{"class":63,"line":369},[61,632,633],{"class":67},"  const cypherQuery ",[61,635,299],{"class":81},[61,637,638],{"class":234}," `\n",[61,640,641,644,647],{"class":63,"line":375},[61,642,643],{"class":234},"    MATCH (entity:${",[61,645,646],{"class":67},"entityType",[61,648,649],{"class":234},"} {id: $entityId})\n",[61,651,652],{"class":63,"line":381},[61,653,654],{"class":234},"    MATCH (entity)\u003C-[:SUBSCRIBED_TO_NOTIFICATIONS]-(user:User)\n",[61,656,657],{"class":63,"line":386},[61,658,659],{"class":234},"    WHERE user.username \u003C> $commenterUsername\n",[61,661,662],{"class":63,"line":400},[61,663,664],{"class":234},"    CREATE (notification:Notification {\n",[61,666,667],{"class":63,"line":412},[61,668,669],{"class":234},"      id: randomUUID(),\n",[61,671,672],{"class":63,"line":424},[61,673,674],{"class":234},"      createdAt: datetime(),\n",[61,676,677],{"class":63,"line":430},[61,678,679],{"class":234},"      read: false,\n",[61,681,682],{"class":63,"line":435},[61,683,684],{"class":234},"      text: $notificationText\n",[61,686,687],{"class":63,"line":449},[61,688,689],{"class":234},"    })\n",[61,691,692],{"class":63,"line":455},[61,693,694],{"class":234},"    CREATE (user)-[:HAS_NOTIFICATION]->(notification)\n",[61,696,698,701],{"class":63,"line":697},33,[61,699,700],{"class":234},"  `",[61,702,703],{"class":67},";\n",[61,705,707],{"class":63,"line":706},34,[61,708,75],{"emptyLinePlaceholder":74},[61,710,712],{"class":63,"line":711},35,[61,713,714],{"class":67},"  await session.run(cypherQuery, { entityId, commenterUsername, notificationText });\n",[61,716,718],{"class":63,"line":717},36,[61,719,119],{"class":67},[61,721,723],{"class":63,"line":722},37,[61,724,75],{"emptyLinePlaceholder":74},[61,726,728,730,732],{"class":63,"line":727},38,[61,729,191],{"class":67},[61,731,612],{"class":85},[61,733,734],{"class":67},"(usersToNotify, emailContent) {\n",[61,736,738],{"class":63,"line":737},39,[61,739,740],{"class":67},"  try {\n",[61,742,744,747,750,752],{"class":63,"line":743},40,[61,745,746],{"class":81},"    const",[61,748,749],{"class":205}," emailsToSend",[61,751,209],{"class":81},[61,753,754],{"class":67}," usersToNotify\n",[61,756,758,761,764,767,769,771],{"class":63,"line":757},41,[61,759,760],{"class":67},"      .",[61,762,763],{"class":85},"filter",[61,765,766],{"class":67},"(",[61,768,568],{"class":567},[61,770,571],{"class":81},[61,772,773],{"class":67}," user.email)\n",[61,775,777,779,782,784,786,788],{"class":63,"line":776},42,[61,778,760],{"class":67},[61,780,781],{"class":85},"map",[61,783,766],{"class":67},[61,785,568],{"class":567},[61,787,571],{"class":81},[61,789,790],{"class":67}," ({\n",[61,792,794],{"class":63,"line":793},43,[61,795,796],{"class":67},"        to: user.email,\n",[61,798,800,803,806],{"class":63,"line":799},44,[61,801,802],{"class":67},"        from: process.env.",[61,804,805],{"class":205},"SENDGRID_FROM_EMAIL",[61,807,808],{"class":67},",\n",[61,810,812],{"class":63,"line":811},45,[61,813,814],{"class":67},"        subject: emailContent.subject,\n",[61,816,818],{"class":63,"line":817},46,[61,819,820],{"class":67},"        text: emailContent.plainText,\n",[61,822,824],{"class":63,"line":823},47,[61,825,826],{"class":67},"        html: emailContent.html\n",[61,828,830],{"class":63,"line":829},48,[61,831,832],{"class":67},"      }));\n",[61,834,836],{"class":63,"line":835},49,[61,837,75],{"emptyLinePlaceholder":74},[61,839,841,843,846,849],{"class":63,"line":840},50,[61,842,327],{"class":81},[61,844,845],{"class":67}," sgMail.",[61,847,848],{"class":85},"send",[61,850,851],{"class":67},"(emailsToSend);\n",[61,853,855],{"class":63,"line":854},51,[61,856,857],{"class":67},"  } catch (error) {\n",[61,859,861,864,867,869,872,875,877],{"class":63,"line":860},52,[61,862,863],{"class":67},"    console.",[61,865,866],{"class":85},"error",[61,868,766],{"class":67},[61,870,871],{"class":234},"'Email sending failed:'",[61,873,874],{"class":67},", ",[61,876,866],{"class":567},[61,878,879],{"class":67},");\n",[61,881,883],{"class":63,"line":882},53,[61,884,885],{"class":274},"    \u002F\u002F Continue with in-app notifications even if emails fail\n",[61,887,889],{"class":63,"line":888},54,[61,890,452],{"class":67},[61,892,894],{"class":63,"line":893},55,[61,895,119],{"class":67},[14,897,899],{"id":898},"why-this-works","Why This Works",[10,901,902],{},"Your APIs stay fast because notifications happen asynchronously in the background. Bulk operations make it efficient because creating 100 notifications takes about the same time as creating one.",[10,904,905],{},"CDC guarantees you won't lose events since they're captured at the database level. If email sending fails, in-app notifications still work because they're handled separately.",[10,907,908],{},"The architecture stays clean with notification logic isolated in its own service. TypeScript provides type safety, and each component can be tested independently.",[10,910,911],{},[912,913,914],"strong",{},"The Complete Flow",[916,917,918,928,934,940],"ol",{},[25,919,920,923,924,927],{},[912,921,922],{},"Comment created"," → Neo4j CDC automatically triggers a ",[58,925,926],{},"commentCreated"," event when someone adds a comment",[25,929,930,933],{},[912,931,932],{},"Event received"," → The notification service receives this event, fetches the comment details, and generates the appropriate email content",[25,935,936,939],{},[912,937,938],{},"Batch processing"," → The system processes everything in batches, using a single operation to send emails and create in-app notifications simultaneously",[25,941,942,945],{},[912,943,944],{},"Graceful failures"," → When email problems occur, they don't affect the in-app notifications because these are handled as separate operations",[10,947,948],{},"The key advantage is batching - one SendGrid API call can notify hundreds of users at once, and email failures stay isolated from your core notification system.",[950,951,952],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":56,"searchDepth":71,"depth":71,"links":954},[955,956,957,962],{"id":16,"depth":71,"text":17},{"id":36,"depth":71,"text":37},{"id":43,"depth":71,"text":44,"children":958},[959,960,961],{"id":48,"depth":78,"text":49},{"id":180,"depth":78,"text":181},{"id":460,"depth":78,"text":461},{"id":898,"depth":71,"text":899},"2025-07-28T08:04:39.315Z","How I built asynchronous, batched notifications with Neo4j GraphQL subscriptions, Change Data Capture, and SendGrid.","md",{},"\u002Fpost\u002Fnotifications",{"title":5,"description":964},"post\u002Fnotifications","CbtMf_-E0ob7SQh7jsYEpkeH4hJxNVfyY23EBFmE0ik",1785377801502]