<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Knock</title>
    <description>The latest articles on DEV Community by Knock (@knocklabs).</description>
    <link>https://dev.to/knocklabs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F5731%2F013b9a10-0fea-4ca0-af04-dcd1cdabd8f5.png</url>
      <title>DEV Community: Knock</title>
      <link>https://dev.to/knocklabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/knocklabs"/>
    <language>en</language>
    <item>
      <title>We analyzed a billion email API requests—here's what we learned</title>
      <dc:creator>Jeff Everhart</dc:creator>
      <pubDate>Thu, 12 Mar 2026 18:07:59 +0000</pubDate>
      <link>https://dev.to/knocklabs/we-analyzed-a-billion-email-api-requests-heres-what-we-learned-j39</link>
      <guid>https://dev.to/knocklabs/we-analyzed-a-billion-email-api-requests-heres-what-we-learned-j39</guid>
      <description>&lt;p&gt;At Knock, we get asked "which email provider should I use?" all the time. &lt;/p&gt;

&lt;p&gt;Until now, comparing email provider performance has been harder than it should be. The existing resources are mostly vibes—"Best of" articles, anonymous Reddit threads, or LLM recommendations trained on that same content. Objective performance data is always missing.&lt;/p&gt;

&lt;p&gt;We believe evaluating a vendor should look the same as profiling a production system: data collected at scale, aggregated over time, under real-world conditions. So we turned to our own data. Every year, Knock sends billions of API requests to downstream email providers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filaymo12k0hu4r79mxr1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filaymo12k0hu4r79mxr1.png" alt="Email API response time leaderboard" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this data, we built &lt;a href="https://knock.app/email-api-benchmarks" rel="noopener noreferrer"&gt;email API benchmarks&lt;/a&gt;, a real-time dashboard that aggregates performance metrics across 10 major email providers. Here's what we learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  API response time: speed matters for transactional email
&lt;/h2&gt;

&lt;p&gt;Most email APIs had a median response time of less than 500ms, which is fast enough for most use cases. But the variance between providers tells a more interesting story.&lt;/p&gt;

&lt;p&gt;The three fastest providers on average were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid&lt;/strong&gt; at 22ms (p50)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark&lt;/strong&gt; at 33ms (p50)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend&lt;/strong&gt; at 79ms (p50)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Response time becomes critical for time-sensitive notifications like one-time passwords, fraud alerts, and two-factor authentication codes. At the p99 percentile, the long tail of requests where performance degrades, SendGrid maintained an average of 157ms, outperforming other providers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvpltrkichdqsdyv1ptm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvpltrkichdqsdyv1ptm.png" alt="SendGrid response time across percentiles" width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We measure response time as the time to receive a &lt;code&gt;200&lt;/code&gt; from a provider, computed across multiple percentiles (p50, p90, p95, and p99) using exact quantile calculations in ClickHouse. This includes HTTP connection pooling, latency, service time, and any automatic retries, making it a useful indicator of system resilience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error rates: reliability under real-world conditions
&lt;/h2&gt;

&lt;p&gt;Most email APIs have an average daily error rate close to 0.00%, which sounds good until you look at peak error rates. During our 90-day observation window, peak daily error rates were grouped between 0.02% and 3.41%, which is a significant spread.&lt;/p&gt;

&lt;p&gt;AWS SES stood out here, with the most days showing error rates below 0.01%, covering the entire 90-day window. This consistency matters when you're sending millions of transactional emails and can't afford degraded delivery. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjaxbn3sfr1x12bp2jfgx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjaxbn3sfr1x12bp2jfgx.png" alt="Daily error rate over 90 days" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We track error rate as the ratio of &lt;code&gt;5XX&lt;/code&gt; responses to total requests for each provider over the period. Since Knock's retry logic treats most &lt;code&gt;5XX&lt;/code&gt; codes as retriable, a single request can produce multiple error entries if the full backoff sequence runs—meaning our error rates may be higher than what you'd see without retry logic (but your emails get sent 😉).&lt;/p&gt;

&lt;h2&gt;
  
  
  Provider adoption trends
&lt;/h2&gt;

&lt;p&gt;Looking at both sending volume and channel growth on Knock's platform revealed some interesting trends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid&lt;/strong&gt; dominated by volume, with users sending over 500 million messages through the service in the last 90 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend&lt;/strong&gt; led in new channel connections, continuing its upward trend in adoption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This split between volume leaders and growth leaders suggests the market is evolving. Established providers maintain massive install bases, while newer developer-focused alternatives are capturing new projects.&lt;/p&gt;

&lt;p&gt;We rank providers from one to ten based on new channel connections created during the period, then compare the first and second halves of the window to detect directional trends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status page correlation: incident transparency matters
&lt;/h2&gt;

&lt;p&gt;On each provider detail page, we integrate with the provider's public status page via RSS or API and overlay reported incidents on the performance data we collect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcg1ux30tp1z2vb8sht91.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcg1ux30tp1z2vb8sht91.png" alt="SendGrid status page incidents" width="800" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our view is that observed degradation should correspond with a status page entry. The degree of correlation between measured performance and public incidents is a useful signal for how a provider handles incident management. If you've ever been on-call, this is something that matters when you're debugging production issues at 2am.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing at scale
&lt;/h2&gt;

&lt;p&gt;We've modeled each provider's price curve so you can see how costs scale with volume. The comparison view puts two providers side by side, making it straightforward to weigh pricing alongside performance when evaluating a switch or new integration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffd9ypf5qjet0x5bkqtrd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffd9ypf5qjet0x5bkqtrd.png" alt="Pricing comparison between Amazon SES and SendGrid" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At 5 million emails per month, pricing differences become significant. AWS SES remains the lowest-cost option at scale, while providers like SendGrid, Postmark, and Resend price higher but include additional features and support.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next for email API benchmarks
&lt;/h2&gt;

&lt;p&gt;We launched with this subset of metrics because they were the most useful and the easiest for us to obtain. We're already exploring the best way to include deliverability and time-to-inbox metrics.&lt;/p&gt;

&lt;p&gt;Knock collects a wide variety of downstream message events, like opens, reads, and link clicks. We'd love to hear from the developer community about what would be valuable to see in a resource like this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;If you're evaluating email providers today, &lt;a href="https://knock.app/email-api-benchmarks" rel="noopener noreferrer"&gt;explore the benchmarks&lt;/a&gt; and see how your current provider stacks up.&lt;/p&gt;

&lt;p&gt;And if you're looking for a notification platform that gives you the flexibility to use any email provider—or switch between them without changing your code—&lt;a href="https://dashboard.knock.app/signup" rel="noopener noreferrer"&gt;sign up for Knock&lt;/a&gt; for free.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What email provider are you currently using? Have you experienced the performance differences we measured? Drop a comment below with your experiences.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>api</category>
      <category>performance</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Building a translation CI/CD pipeline with Lingo.dev</title>
      <dc:creator>Jeff Everhart</dc:creator>
      <pubDate>Tue, 04 Mar 2025 19:03:15 +0000</pubDate>
      <link>https://dev.to/knocklabs/building-a-translation-cicd-pipeline-with-lingodev-4926</link>
      <guid>https://dev.to/knocklabs/building-a-translation-cicd-pipeline-with-lingodev-4926</guid>
      <description>&lt;p&gt;A common challenge for applications serving a global user base is providing content and notifications in multiple languages. Manual translation is not only time-consuming but prone to errors and inconsistencies in style and tone of voice.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk you through how to build an automated CI/CD translation pipeline using &lt;a href="https://lingo.dev/" rel="noopener noreferrer"&gt;Lingo.dev&lt;/a&gt; and Knock that will handle localized notifications for your app without requiring any manual translation work from your team. We’ll introduce some tools that can help you scale your application to a global audience, automate translations locally using Lingo.dev's CLI, and finally integrate that CLI into a CI/CD pipeline so that translations are automatically created when developers open a pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video walkthrough
&lt;/h2&gt;

&lt;p&gt;If you prefer to learn with video, watch it here.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/qQgoGLsdbJY"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools for scaling your application to a global audience
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://lingo.dev/" rel="noopener noreferrer"&gt;Lingo.dev&lt;/a&gt; is an AI-powered translation service that's relatively new but incredibly powerful. It allows you to specify a particular brand voice for your translations, which we've done for our sample application called Globe Wander, an adventure marketplace.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53m66ox9m7hn47zswno0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53m66ox9m7hn47zswno0.png" alt="Lingo.dev brand voice settings" width="800" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can fine-tune translations for your specific context and add custom words to your glossary to ensure consistent translations across your application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://knock.app/" rel="noopener noreferrer"&gt;Knock is a notification infrastructure&lt;/a&gt; platform with built-in localization support. When you attach a locale to a user object, Knock automatically handles localizing your notification templates when provided with the appropriate translation files. It uses translation tags in message templates to display the right content based on the user's preferred language.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1kmjxn2fivbx3u031za.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1kmjxn2fivbx3u031za.png" alt="Details of a Knock translation file" width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How localization works in Knock
&lt;/h2&gt;

&lt;p&gt;Let's take a closer look at how localization works in Knock. In our Knock dashboard, I've created a "New User Signup" workflow with a Postmark email template that uses translation filters in liquid. If we look at our Postmark email template, you'll see I'm using these translation filters that reference translated string stored in our translation files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1kmjxn2fivbx3u031za.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1kmjxn2fivbx3u031za.png" alt="Knock translation tags" width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you preview this template and switch between locales you can see how the message automatically changes to the appropriate language.&lt;/p&gt;

&lt;p&gt;When you pull translation files into your codebase with the &lt;a href="https://docs.knock.app/cli" rel="noopener noreferrer"&gt;Knock CLI&lt;/a&gt;, they’re organized in a specific folder structure. We have a main "translations" folder with subfolders for each locale like "en-US", "es" for Spanish, and "fr" for French. Inside each locale folder are JSON files containing translation keys and values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;translations/
├── en-US/
│   ├── cancellation.en-US.json
│   ├── en-US.json
│   ├── payments.en-US.json
│   └── shipping.en-US.json
├── es/
│   ├── cancellation.es.json
│   ├── es.json
│   ├── payments.es.json
│   └── shipping.es.json
└── fr/
    ├── cancellation.fr.json
    ├── fr.json
    ├── payments.fr.json
    └── shipping.fr.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also use namespacing to organize our translations by feature or notification type. This makes it easier to manage translations for different parts of your application, such as separating payment notifications from cancellation updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Translating files locally
&lt;/h2&gt;

&lt;p&gt;To set up our pipeline, we first need to install and authenticate with Lingo.dev, which you can do by running this auth command in your terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx lingo.dev@latest auth &lt;span class="nt"&gt;--login&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing the package, we run the initialization command which creates an "i18n.json" configuration file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx lingo.dev@latest init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this configuration file, we can specify our source locale as English and define our target locales as Spanish and French. We also configure the file paths for our translation buckets, pointing to our Knock translations directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"locale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"es"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fr"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buckets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"./knock/translations/[locale]/[locale].json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"./knock/translations/[locale]/*.[locale].json"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://lingo.dev/schema/i18n.json"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because Knock requires a specific file naming convention, I've created two include paths to target both the top-level translation file and any namespaced files. This &lt;code&gt;locale&lt;/code&gt; placeholder can be used to extract a locale from a file path name that can be used in the newly produced files.&lt;/p&gt;

&lt;p&gt;This helps ensure compatibility with Knock's expected structure and gives you flexibility if your translation files are stored differently.&lt;/p&gt;

&lt;p&gt;You can test Lingo.dev by running the &lt;code&gt;i18n&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx lingo.dev@latest i18n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the terminal you should be able to see the output. The CLI connects to Lingo.dev, translates all the files, and writes them to the paths specified in the config object. Lingo uses the &lt;code&gt;i18n.lock&lt;/code&gt; file to create checksums, so that it only translates files that have changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a CI/CD pipeline
&lt;/h2&gt;

&lt;p&gt;For production use, we want this process to run automatically whenever changes are made to our translation files, and ideally we’d push those updated translations back into Knock. I've created two GitHub Action workflows to handle different parts of this.&lt;/p&gt;

&lt;p&gt;The first workflow triggers when a pull request is opened to the main branch. It checks out the repository, installs dependencies, runs our translation process on any changed files, and then commits the translated files back to the PR branch. This way, reviewers can see both the original changes and the resulting translations before merging.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;i18n Knock Automation&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prepare_translations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prepare Translation Files&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout Repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.ref }}&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;# Ensure full branch history&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt; &lt;span class="c1"&gt;# Ensures a clean install using package-lock.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Process Translations&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx lingo.dev@latest i18n&lt;/span&gt; &lt;span class="c1"&gt;# Adjust to your relevant command&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;LINGODOTDEV_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LINGODOTDEV_API_KEY }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Commit and Push Changes&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;BRANCH_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.head_ref || github.ref_name }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.name "GitHub Actions"&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.email "actions@github.com"&lt;/span&gt;
          &lt;span class="s"&gt;git checkout "${BRANCH_NAME}"&lt;/span&gt;
          &lt;span class="s"&gt;git add .&lt;/span&gt;
          &lt;span class="s"&gt;git diff --quiet &amp;amp;&amp;amp; git diff --staged --quiet || git commit -m "chore: rename translation files"&lt;/span&gt;
          &lt;span class="s"&gt;git push origin "${BRANCH_NAME}"&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second workflow runs when a PR is merged to main. It uses the &lt;a href="https://docs.knock.app/cli" rel="noopener noreferrer"&gt;Knock CLI&lt;/a&gt; to upload all translation files to your Knock development environment and commit them, making them immediately available for use in testing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Push Translations to Knock&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;closed&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push_translations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event.pull_request.merged == &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Push Translations to Knock&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout Repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Knock CLI&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install -g @knocklabs/cli&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Push Translations to Knock&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;knock translation push --commit --all --translations-dir=./knock/translations --service-token=$KNOCK_SERVICE_TOKEN&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;KNOCK_SERVICE_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.KNOCK_SERVICE_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's see this in action. I'll add a new translation key for payment reasons to &lt;code&gt;payments.en-US.json&lt;/code&gt; with options like "no payment," "insufficient funds," and "invalid method."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hey, your payment was successful!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sorry, your payment didn't go through. Please contact support."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"no_payment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"We couldn't process your payment."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"insufficient_funds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You don't have enough funds in your account."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invalid_payment_method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The payment method you provided is invalid."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cancelled_by_customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You cancelled the order yourself."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cancelled_by_seller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The seller cancelled the order."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After committing these changes to a new branch and opening a PR, we can watch the automated translation process happen. Our GitHub Action starts running, connecting to Lingo.dev to translate our new content into Spanish and French.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8wwkbbi6rc3vm21jmhox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8wwkbbi6rc3vm21jmhox.png" alt="Translation GitHub action translate" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once complete, it commits these files back to our PR branch. Notice how it only updates files that have actually changed, thanks to the checksum system in the &lt;code&gt;i18n.lock&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5chfr4s6tet4dbgqg0u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5chfr4s6tet4dbgqg0u.png" alt="Translation GitHub action commit" width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pushing translations back to Knock
&lt;/h2&gt;

&lt;p&gt;After merging the PR, our second workflow automatically uploads the translations to Knock. In the &lt;code&gt;Actions&lt;/code&gt; tab of GitHub you can open up the Knock translations workflow and see that output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl1y43eeudyjknl1btech.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl1y43eeudyjknl1btech.png" alt="Knock translation CLI workflow" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now if we check our Knock dashboard under &lt;code&gt;Translations,&lt;/code&gt; we can confirm that our new payment reason translations have been uploaded in all three languages: our original English content, plus the AI-translated Spanish and French versions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fesfcups3ccdtl4it2ldb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fesfcups3ccdtl4it2ldb.png" alt="Knock translation AI generated" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This pipeline provides significant benefits for your development workflow. Developers can focus on adding content in one language, with translations handled automatically through CI/CD. You're able maintain a single template in Knock with dynamic content that changes based on the user's language preference. All changes are version-controlled alongside your code, providing a clear history of translations.&lt;/p&gt;

&lt;p&gt;While the setup I've shown is somewhat opinionated, you can adapt it to fit your specific workflow. Lingo.dev also offers their own GitHub Action, and one of the major advantages of using Knock for notifications is that you're not creating separate templates for each language. Instead, you scale through translation files in a much more manageable way than what many other notification providers offer.&lt;/p&gt;

&lt;p&gt;I encourage you to check out Lingo.dev for AI-based translations and Knock for notification infrastructure that makes localization straightforward and efficient.&lt;/p&gt;

&lt;p&gt;Thanks for reading! Happy coding!&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>i18</category>
      <category>ai</category>
      <category>cli</category>
    </item>
    <item>
      <title>Building a Notion-style activity feed with Next.js and shadcn/ui</title>
      <dc:creator>Jeff Everhart</dc:creator>
      <pubDate>Thu, 02 May 2024 14:59:31 +0000</pubDate>
      <link>https://dev.to/knocklabs/building-a-notion-style-activity-feed-with-nextjs-and-shadcnui-m4i</link>
      <guid>https://dev.to/knocklabs/building-a-notion-style-activity-feed-with-nextjs-and-shadcnui-m4i</guid>
      <description>&lt;p&gt;In this post, we'll explore how to build a custom in-app feed using the Knock JavaScript client. This project is modeled visually on the Notion in-app feed, and we'll use Next.js and shadcn/ui to build out the interface. We'll look at how to configure an in-app channel and workflow in Knock, fetch a user's feed from the Feed API, and update message engagement statuses as a user marks things as &lt;code&gt;read&lt;/code&gt; and &lt;code&gt;archived&lt;/code&gt;. Finally, we'll explore some of the small details that make Notion's feed such a great example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video walkthrough
&lt;/h2&gt;

&lt;p&gt;If you prefer to learn with video, watch it here.&lt;br&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/V-65frts9X0"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Before we dive in too deep, let's take a look at what the end product will look like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Ffeed-demo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Ffeed-demo.gif" alt="Example feed demo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll end up with a tabbed feed where users can mark things as read, archive them, and switch between different views of the feed. This whole example project can be downloaded from the Knock GitHub account in the &lt;code&gt;notion-feed-example&lt;/code&gt; &lt;a href="https://github.com/knocklabs/notion-feed-example" rel="noopener noreferrer"&gt;repo&lt;/a&gt;. It has two branches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main&lt;/code&gt;: The finished version&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start&lt;/code&gt;: What we'll be starting with for this project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll cover adding the functionality to read from the Feed API and update message engagement statuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloning the repository
&lt;/h2&gt;

&lt;p&gt;First, clone the repository from the GitHub repo using this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/knocklabs/notion-feed-example.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, install the dependencies and make a copy of the &lt;code&gt;.env.sample&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.sample .env.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we can open it up and see what values we need, as there are a few things we'll want to retrieve from the Knock dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Knock User ID&lt;/li&gt;
&lt;li&gt;Knock Feed Channel ID&lt;/li&gt;
&lt;li&gt;Public API Key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's head into the Knock dashboard to grab those values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the Knock client
&lt;/h2&gt;

&lt;p&gt;The first thing we'll grab is our &lt;a href="https://docs.knock.app/developer-tools/api-keys" rel="noopener noreferrer"&gt;public API key&lt;/a&gt;. Under the "Developers &amp;gt; API Keys" heading, we can copy the public key and paste it into our &lt;code&gt;.env.local&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Next, we'll need our Feed Channel ID. If you go to "Integrations" and find the &lt;a href="https://docs.knock.app/integrations/in-app/knock" rel="noopener noreferrer"&gt;in-app channel&lt;/a&gt; feed that gets created automatically (or another one you've created for your own projects), you can copy that ID and paste it into your environment file as well.&lt;/p&gt;

&lt;p&gt;Lastly, we'll need a user ID. Go ahead and copy a user ID and paste it into the &lt;code&gt;.env.local&lt;/code&gt; file. If you don't have a user, you can create one from the dashboard under "Users."&lt;/p&gt;

&lt;p&gt;Now we should be all set, as long as we've installed the project's dependencies. Let's fire up the development server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should start our local server on &lt;code&gt;localhost:3000&lt;/code&gt;. If you click the link to open it, you should see a project that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Ffeed-empty-state.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Ffeed-empty-state.png" alt="Feed empty state"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Right now, there's nothing in either of the tabs, and the inbox says we don't have any messages. That's what we'll be adding in the next steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeding messages
&lt;/h2&gt;

&lt;p&gt;Before we go any further, let's hop back into the Knock dashboard and take a look at an in-app workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a workflow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Fnew-workflow.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Fnew-workflow.gif" alt="Creating a workflow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you don't have one already, you can create one on the 'Workflows' screen of the dashboard. Then you can use the in-app channel that's created automatically when you create an account by dragging it onto the workflow canvas. You'll need to save your changes and commit them to the development branch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending test messages
&lt;/h3&gt;

&lt;p&gt;As you can see, we've got a really basic in-app workflow set up for this project:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Fworkflow-seeding.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Fworkflow-seeding.gif" alt="A demo of running in-app workflow triggers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we click into the workflow itself and go to the workflow editor canvas, we can see that we've got a simple message template. Let's go ahead and seed a bunch of messages by clicking "Run a test."&lt;/p&gt;

&lt;p&gt;You can pick a user ID, select an actor, and we'll pass in a different message as our &lt;code&gt;message&lt;/code&gt; property in our data payload.&lt;/p&gt;

&lt;p&gt;Click "Run test" a couple of times to seed that in-app feed with some stuff we can work with.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's worth noting that this example app doesn't actually go through the sending or triggering of workflows. It's just showing you how to read from an in-app feed. You can explore &lt;a href="https://docs.knock.app/send-notifications/triggering-workflows" rel="noopener noreferrer"&gt;our docs on triggering workflows&lt;/a&gt; to build that into your project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now that we have a couple of example messages in this in-app feed for our user, let's hop back into the code editor and figure out how we can read that user's Feed API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the Knock client in our app
&lt;/h2&gt;

&lt;p&gt;Open up the &lt;code&gt;ActivityFeed&lt;/code&gt; component. We'll configure a new instance of the Knock client. We can see that we're already importing a couple of things from the &lt;code&gt;@knocklabs/client&lt;/code&gt; package, which should have been installed as a project dependency.&lt;/p&gt;

&lt;p&gt;Right below &lt;code&gt;FeedItemCard&lt;/code&gt;, create a new constant variable called &lt;code&gt;knockClient&lt;/code&gt;. This will store a new configured instance of the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;knockClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Knock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_KNOCK_PUBLIC_API_KEY&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below that, since we've created a new instance of the Knock client, we also need to authenticate it by passing a user ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;knockClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_KNOCK_USER_ID&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we've created a new instance of the client and authenticated, we'll initialize a new feed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;knockFeed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;knockClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;feeds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_KNOCK_FEED_CHANNEL_ID&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;archived&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ties the feed to the channel ID we got in the first step. The second argument, which is optional, are feed client options. We're including two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;pageSize&lt;/code&gt;: How many items it will grab at one time (set to 20)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;archived&lt;/code&gt;: Set to &lt;code&gt;'include'&lt;/code&gt; so we get the most recent 20 items of our feed and include archived items as well&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the next step, we'll talk about how we can actually fetch items from that feed and tie those into the user interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetching feed items
&lt;/h2&gt;

&lt;p&gt;To begin the process of fetching, let's delete the &lt;code&gt;feedItems&lt;/code&gt; variable for now. We'll start by declaring some new constant variables using the &lt;code&gt;useState&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFeed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FeedStoreState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're telling TypeScript that this is a &lt;code&gt;FeedStoreState&lt;/code&gt; type, which we're importing from the &lt;code&gt;@knocklabs/client&lt;/code&gt; package. We'll initialize it to an empty object.&lt;/p&gt;

&lt;p&gt;Below that, we've got this &lt;code&gt;useState&lt;/code&gt; hook to set some local state for this component. But we also want to make some network calls using the &lt;code&gt;knockFeed&lt;/code&gt; to load the initial state of our feed and listen for updates. We'll use another React hook to do that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listenForUpdates&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchFeed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;feedState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setFeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feedState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nf"&gt;fetchFeed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's take a recap of what we're doing here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We used &lt;code&gt;useState&lt;/code&gt; to create some local state variables for the &lt;code&gt;ActivityFeed&lt;/code&gt; component.&lt;/li&gt;
&lt;li&gt;Inside the &lt;code&gt;useEffect&lt;/code&gt; hook, we're asking the &lt;code&gt;knockFeed&lt;/code&gt; client to listen for real-time updates.&lt;/li&gt;
&lt;li&gt;We create a function to fetch the feed initially.&lt;/li&gt;
&lt;li&gt;We fetch the feed, get the state that we get back, and set it to our local component state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's one additional thing we need to do: add event listeners to the &lt;code&gt;knockFeed&lt;/code&gt; so that when we get those real-time updates from &lt;code&gt;listenForUpdates()&lt;/code&gt; or make changes to the status of messages, we can reconcile those updates with our local state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;items.received.*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setFeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;items.*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setFeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Knock feed does a good job of keeping track of its own internal state. When there are changes to that internal state, we're just going to reconcile those with the state in React by calling &lt;code&gt;setFeed&lt;/code&gt; and overriding whatever feed was in that local variable.&lt;/p&gt;

&lt;p&gt;Now we've got a handle on getting those items from the API. In the next step, we'll make those items available to the interface so we can render some different components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering feed items
&lt;/h2&gt;

&lt;p&gt;Before we proceed, let's make sure we have event handlers for both &lt;code&gt;'items.received.realtime'&lt;/code&gt; and &lt;code&gt;'items.*'&lt;/code&gt;. This captures a wider array of changes that can happen to the Knock feeds that deal with both message statuses and receiving new messages.&lt;/p&gt;

&lt;p&gt;Even as we start updating the message engagement statuses on these items, we'll see that reflected when this event trigger fires, helping us reconcile our state as we make different changes to the feed.&lt;/p&gt;

&lt;p&gt;If we reference back to our user interface, we can see that we've got a couple of different options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inbox: Things that aren't archived&lt;/li&gt;
&lt;li&gt;Archive: Archived items&lt;/li&gt;
&lt;li&gt;All: All items&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll want to separate these things out and create different arrays for these different types of items. We'll use the &lt;code&gt;useMemo&lt;/code&gt; hook to compute some of these values for us every time the feed array changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;feedItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;archivedItems&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;feedItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FeedItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archivedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;archivedItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FeedItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archivedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;feedItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;archivedItems&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We create two arrays:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;feedItems&lt;/code&gt;: Items that haven't been archived yet&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;archivedItems&lt;/code&gt;: Items that have been archived&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We return these arrays from &lt;code&gt;useMemo&lt;/code&gt;, which will re-compute them whenever &lt;code&gt;feed&lt;/code&gt; changes.&lt;/p&gt;

&lt;p&gt;Now, let's implement the list of regular feed items (what we would find in the user's inbox). If we scroll down to our tabbed content, we can see that we're going to implement that here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;feedItems&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;feedItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FeedItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeedItemCard&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"p-4 text-gray-500"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We do a conditional check to make sure &lt;code&gt;feedItems&lt;/code&gt; has a length greater than 0. If it does, we map through &lt;code&gt;feedItems&lt;/code&gt; and return a &lt;code&gt;FeedItemCard&lt;/code&gt; for each one, passing in the &lt;code&gt;item&lt;/code&gt; and &lt;code&gt;knockFeed&lt;/code&gt; as props.&lt;/p&gt;

&lt;p&gt;Head back into you interface and double-check that everything's working as expected now. You should see &lt;code&gt;FeedItems&lt;/code&gt; displaying in your inbox tab.&lt;/p&gt;

&lt;p&gt;Let's do something similar with our archived items tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;archivedItems&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;archivedItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FeedItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeedItemCard&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"p-4 text-gray-500"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you go back to the interface, you should see any archived items there. If you archive an item, it should update and move to the "Archived" tab.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's worth noting that &lt;a href="https://docs.knock.app/send-notifications/message-statuses" rel="noopener noreferrer"&gt;message engagement statuses in Knock&lt;/a&gt; are mutually inclusive, meaning they can be in multiple different states at the same time. For example, an item can be both &lt;code&gt;unread&lt;/code&gt; and &lt;code&gt;archived&lt;/code&gt;. This gives you a lot of flexibility as the developer in how you want to model these engagement statuses in your own application.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The FeedItemCard component
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;FeedItemCard&lt;/code&gt; component is doing a lot of the heavy lifting in this example app. Let's explore that component next.&lt;/p&gt;

&lt;p&gt;We can see the props it takes: &lt;code&gt;item&lt;/code&gt; (the feed item) and &lt;code&gt;knockFeed&lt;/code&gt;. Inside the component, we do a number of things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract the different blocks attached to the feed item (like the &lt;code&gt;body&lt;/code&gt;, which is the message template we created as part of our workflow).&lt;/li&gt;
&lt;li&gt;Loop through the item's actors (the people generating the notification by triggering the workflow in Knock) and create the heading.&lt;/li&gt;
&lt;li&gt;Render the date using the &lt;code&gt;toLocaleDateString&lt;/code&gt; function and the &lt;code&gt;item.insertedAt&lt;/code&gt; property.&lt;/li&gt;
&lt;li&gt;Handle message engagement statuses.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the message engagement statuses, we conditionally render different buttons based on whether the item has been read or not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readAt&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Mark as Read&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Mark as Unread&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We pass the &lt;code&gt;knockFeed&lt;/code&gt; client into the component so we can attach each of those &lt;code&gt;onClick&lt;/code&gt; handlers to a particular method on &lt;code&gt;knockFeed&lt;/code&gt;, like &lt;code&gt;markAsArchived&lt;/code&gt;, &lt;code&gt;markAsUnarchived&lt;/code&gt;, &lt;code&gt;markAsRead&lt;/code&gt;, &lt;code&gt;markAsUnread&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;When we call these methods, the Knock feed updates for us locally inside our &lt;code&gt;ActivityFeed&lt;/code&gt; component because of the event listener we set up in our &lt;code&gt;useEffect&lt;/code&gt; hook. We told &lt;code&gt;knockFeed&lt;/code&gt; to listen to any of those lower-level item changes and then reset our feed state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional functionality
&lt;/h2&gt;

&lt;p&gt;There are a couple of other pieces of functionality we want to build into this, like the "Mark all as Read" and "Archive All" buttons in our inbox experience.&lt;/p&gt;

&lt;p&gt;To do that, we'll create two functions in our &lt;code&gt;ActivityFeed&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markAllAsRead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markAllAsRead&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;setFeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markAllAsArchived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markAllAsArchived&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;setFeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;knockFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These functions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use special bulk methods on the &lt;code&gt;knockFeed&lt;/code&gt; to make status updates to all &lt;code&gt;FeedItems&lt;/code&gt; in the current scope&lt;/li&gt;
&lt;li&gt;Manually set our feed state using &lt;code&gt;knockFeed.getState()&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then, we can add &lt;code&gt;onClick&lt;/code&gt; handlers to our buttons that call these functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;markAllAsRead&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Mark all as Read&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;markAllAsArchived&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Archive All&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you go back to the interface, these buttons should work as expected, allowing you to quickly clear out our notification feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polish and finishing touches
&lt;/h2&gt;

&lt;p&gt;There are a couple of things that Notion does that add an extra layer of polish to the in-app feed experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "New" unread icon
&lt;/h3&gt;

&lt;p&gt;If we look at our &lt;code&gt;FeedItemCard&lt;/code&gt; component, we can see that we're conditionally rendering a "New" icon based on whether the item has been read or not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read_at&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"new-icon"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is just a &lt;code&gt;div&lt;/code&gt; with some additional styling applied to it. In the example app, we apply these styles with the &lt;code&gt;styles&lt;/code&gt; attribute, but this is what a CSS class may look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.new-icon&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;131&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;226&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These styles come one-to-one from the Notion UI, with a few tweaks to the &lt;code&gt;left&lt;/code&gt; and &lt;code&gt;top&lt;/code&gt; properties because the avatar size was a little different using shadcn/ui.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Funread-icon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Funread-icon.png" alt="Unread icon"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is just one of those really nice features that calls attention to an unread item in a feed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Opacity treatment for read items
&lt;/h3&gt;

&lt;p&gt;There's another treatment that Notion applies to feed items that is really classy. If we look at our &lt;code&gt;FeedItemCard&lt;/code&gt; component, we can see that we're applying a 70% opacity to the element whenever it's marked as read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read_at&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opacity-70&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we look at the interface, we can see how this de-emphasizes the things we've already interacted with and makes the unread items stand out much more. We've got this double encoding of the unread status:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The "New" item icon&lt;/li&gt;
&lt;li&gt;100% opacity (vs. 70% for read items)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Fread-opacity.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-notion-style-feed-nextjs%2Fread-opacity.png" alt="read opacity"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you compare these things against each other, they both stand out really well. These are really tasteful affordances that Notion builds into their in-app feed experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Awesome! Thanks so much for reading. In this post, we covered how to build a Notion-style in-app feed experience using Knock's Vanilla JavaScript client. &lt;/p&gt;

&lt;p&gt;We looked at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating an in-app feed channel and workflow&lt;/li&gt;
&lt;li&gt;Fetching items from a user's feed&lt;/li&gt;
&lt;li&gt;Managing engagement statuses on feed messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The feed client gives developers pretty much unlimited flexibility in the types of experiences they can create, so we're excited to see what you'll build.&lt;/p&gt;

&lt;p&gt;You can get started with Knock for free by signing up &lt;a href="https://dashboard.knock.app/signup" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Knock on.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>javascript</category>
      <category>notion</category>
      <category>node</category>
    </item>
    <item>
      <title>Building a GitHub activity feed with Node.js and Socket.io</title>
      <dc:creator>Jeff Everhart</dc:creator>
      <pubDate>Fri, 26 Apr 2024 10:11:22 +0000</pubDate>
      <link>https://dev.to/knocklabs/building-a-github-activity-feed-with-nodejs-and-socketio-29ad</link>
      <guid>https://dev.to/knocklabs/building-a-github-activity-feed-with-nodejs-and-socketio-29ad</guid>
      <description>&lt;p&gt;In a previous article, we discussed &lt;a href="https://knock.app/blog/the-benefits-of-adding-an-activity-feed-to-your-product" rel="noopener noreferrer"&gt;the benefits of creating an activity feed for your product&lt;/a&gt;. In this post, we’ll cover how to build an activity feed directly into your product using Node.js and Socket.io.&lt;/p&gt;

&lt;p&gt;Let’s say we’re building a B2B SaaS app for developers, and it needs to show an activity feed of what is happening in their organization’s GitHub repo. Any commits pushed to the repository should be displayed in the app in real-time. That’s what we'll build in this post. You can find &lt;a href="https://github.com/JEverhart383/github-activity-feed" rel="noopener noreferrer"&gt;the completed project available on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We’ll use a library called Socket.io to implement WebSockets that send events in real-time from our backend Node.js server to our frontend JavaScript client whenever the server gets a message from GitHub that a repo has been updated. We’ll create webhooks to get that information from GitHub and set up our Node.js server to listen for these webhook events.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Creating your server&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;First, let’s create the Node.js server that listens for GitHub webhook events and updates connected clients in real-time using WebSockets via &lt;code&gt;socket.io&lt;/code&gt;. It will also serve a static HTML file for the client-side interface.&lt;/p&gt;

&lt;p&gt;To set up this project, create a new directory and initialize a Node.js project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;github-webhook-feed
&lt;span class="nb"&gt;cd &lt;/span&gt;github-webhook-feed
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the following packages using &lt;code&gt;npm&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;express body-parser socket.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a file called &lt;code&gt;index.js&lt;/code&gt; and include the following code for this server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bodyParser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body-parser&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;socketIo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;socket.io&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bodyParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// For parsing application/json&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;socketIo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Endpoint for GitHub Webhooks&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Check for a push event&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pusher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new_commit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Serve the client HTML file&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;connection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A user connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Server listening on port 3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down this code step-by-step. First, we have the module imports and initializations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bodyParser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body-parser&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;socketIo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;socket.io&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bodyParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// For parsing application/json&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;socketIo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we import &lt;code&gt;express&lt;/code&gt;. The &lt;a href="https://expressjs.com/" rel="noopener noreferrer"&gt;Express framework&lt;/a&gt; allows us to create routes that will respond to webhook POST requests and serve an HTML file when a GET request is made to the root of the site.&lt;/p&gt;

&lt;p&gt;Next, we import &lt;code&gt;body-parser&lt;/code&gt;, which is middleware for parsing incoming request bodies before your route handlers run, available under the &lt;code&gt;req.body&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;Then, we have the two imports we need to set up our WebSockets. The &lt;code&gt;http&lt;/code&gt; module creates a basic HTTP server, and &lt;code&gt;socket.io&lt;/code&gt;, a library for building real-time applications, enables real-time, bidirectional, and event-based communication between web clients and servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Express with Socket.IO
&lt;/h3&gt;

&lt;p&gt;Once we’ve imported all our modules, we create an instance of Express and assign it to a variable called &lt;code&gt;app&lt;/code&gt;, and then call the &lt;code&gt;.use()&lt;/code&gt; method with &lt;code&gt;body-parser&lt;/code&gt; as an argument, which tells Express to use the middleware to parse the JSON body of incoming requests.&lt;/p&gt;

&lt;p&gt;Then, we create an HTTP server and pass the Express &lt;code&gt;app&lt;/code&gt; as an argument and initialize a new instance of &lt;code&gt;socket.io&lt;/code&gt; by passing that HTTP server object into &lt;code&gt;socketIo&lt;/code&gt;. This allows &lt;code&gt;socket.io&lt;/code&gt; to work in conjunction with the Express app to handle both regular routes and WebSockets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating server endpoints
&lt;/h3&gt;

&lt;p&gt;Now that we’ve configured a server that can use both Express and Socket.io, let’s add the endpoints we’ll need to accept webhook payloads and serve our user interface.&lt;/p&gt;

&lt;h4&gt;
  
  
  Receiving webhook POST requests
&lt;/h4&gt;

&lt;p&gt;First, we’ll create our &lt;code&gt;/webhook&lt;/code&gt; endpoint using the &lt;code&gt;app.post&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Check for a push event&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pusher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new_commit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This listens for POST requests to the &lt;code&gt;/webhook&lt;/code&gt; path, and when a POST request is received it runs the callback function we provide. Inside the callback function, we check the request's body using the &lt;code&gt;req.body&lt;/code&gt; parameter. In this example, &lt;code&gt;req.body&lt;/code&gt; will contain event data from GitHub. If the event data includes a &lt;code&gt;pusher&lt;/code&gt; object, which indicates a push event, we’ll emit a &lt;code&gt;new_commit&lt;/code&gt; event to all connected WebSocket clients using &lt;code&gt;socket.io&lt;/code&gt; and pass in the GitHub event data.&lt;/p&gt;

&lt;h4&gt;
  
  
  Serving static HTML with GET handlers
&lt;/h4&gt;

&lt;p&gt;We’ll also need a front end to connect to our WebSockets, and we do that by serving the client HTML file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a route handler for GET requests to the root URL (&lt;code&gt;/&lt;/code&gt;). It serves the &lt;code&gt;index.html&lt;/code&gt; file located in the same directory using the &lt;code&gt;res.sendFile&lt;/code&gt; method in Express.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling WebSocket connections
&lt;/h3&gt;

&lt;p&gt;Next, we’ll add a function to create our &lt;a href="http://socket.io/" rel="noopener noreferrer"&gt;socket.io&lt;/a&gt; connection handler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;connection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A user connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This listens for &lt;code&gt;connection&lt;/code&gt; events on the &lt;code&gt;socket.io&lt;/code&gt; instance. Every time a new client connects, the callback function is executed, and the message "A user connected" is logged to the console.&lt;/p&gt;

&lt;p&gt;Finally, we start the server and listen on port 3000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Server listening on port 3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s set up our client code next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating your client
&lt;/h2&gt;

&lt;p&gt;Now, let’s move on to the frontend. Create an &lt;code&gt;index.html&lt;/code&gt; file in the same directory as your &lt;code&gt;index.js&lt;/code&gt; file and add this code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;GitHub Commit Feed&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/socket.io/socket.io.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Arial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f4f4f4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nc"&gt;.notification&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#ddd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt; &lt;span class="m"&gt;15px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nc"&gt;.notification-header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;GitHub Commit Feed&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;io&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new_commit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notificationDiv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;notificationDiv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notification-header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`New commit in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Commit by &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pusher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head_commit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

          &lt;span class="nx"&gt;notificationDiv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;notificationDiv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notificationDiv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few critical parts of this HTML file to highlight.&lt;/p&gt;

&lt;p&gt;The first is the &lt;code&gt;script&lt;/code&gt; tag in the &lt;code&gt;head&lt;/code&gt; of our HTML document that loads the Socket.IO client library. This script tag includes the &lt;a href="http://socket.io/" rel="noopener noreferrer"&gt;Socket.IO&lt;/a&gt; client library that will communicate with our &lt;a href="http://socket.io" rel="noopener noreferrer"&gt;socket.io&lt;/a&gt; server from the code above.&lt;/p&gt;

&lt;p&gt;We then have a few CSS styles for the document &lt;code&gt;body&lt;/code&gt;, and &lt;code&gt;notification&lt;/code&gt;, and &lt;code&gt;notification-header&lt;/code&gt; classes.&lt;/p&gt;

&lt;p&gt;Then, in our document’s &lt;code&gt;body&lt;/code&gt; section, we have some JavaScript that we’re using to update our feed. The JS is wrapped inside an event listener on the &lt;code&gt;DOMContentLoaded&lt;/code&gt; event, which runs after the DOM is fully loaded. This allows us to make sure the Socket.io client script is loaded before we use it in our code.&lt;/p&gt;

&lt;p&gt;We create a new variable called &lt;code&gt;socket&lt;/code&gt; and initialize it to a new &lt;code&gt;io()&lt;/code&gt; client object. This establishes a WebSocket connection to the server.&lt;/p&gt;

&lt;p&gt;From there we create a new event listener on &lt;code&gt;socket&lt;/code&gt; that listens for the &lt;code&gt;new_commit&lt;/code&gt; event we send from the server when a GitHub webhook is received. When a new commit event is received, the callback function is executed to handle the data.&lt;/p&gt;

&lt;p&gt;Inside the callback, we use some vanilla JavaScript to create the HTML structure for our notifications:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- A new `div` element with the class `notification` is created to represent a notification.
- Two more `div` elements are created within this notification `div` to display the repository name (with class `notification-header`) and the commit details.
- These elements are appended to the `notificationDiv`, and then `notificationDiv` is appended to the `body` of the document, making the commit data visible on the page.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The JS code in the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag dynamically updates the web page's content in real-time as new commit data is received from the server via the established WebSocket connection. Each new commit results in a new notification being added to the page with the details of the commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up your webhook
&lt;/h2&gt;

&lt;p&gt;To start our server locally, you can run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the moment, our webhook endpoint is at &lt;code&gt;http://localhost:3000/webhook&lt;/code&gt;. But we can’t give this URL to GitHub and expect anything to happen—GitHub can’t access our local server.&lt;/p&gt;

&lt;p&gt;The solution is to use a service like ngrok to expose our server to the internet. &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;Download and install ngrok&lt;/a&gt;, then, in a different terminal tab, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ngrok http 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will expose the server on that port to the internet and give you a public URL you can use that will look something like this:&lt;/p&gt;

&lt;p&gt;This URL is what you are going to give to GitHub. Follow the&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your GitHub repository.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Webhooks&lt;/strong&gt; &amp;gt; &lt;strong&gt;Add webhook&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Payload URL&lt;/strong&gt;, enter the ngrok URL followed by &lt;strong&gt;&lt;code&gt;/webhook&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;&lt;code&gt;application/json&lt;/code&gt;&lt;/strong&gt; for content type.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Just the push event&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add the webhook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Whenever a push is made to the repository, GitHub will send a POST request to your server.&lt;/p&gt;

&lt;p&gt;Let’s push some changes to a GitHub repository and see what happens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-a-github-activity-feed-with-nodejs-and-socket-io%2Fgithub-activity-feed-example.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Fbuilding-a-github-activity-feed-with-nodejs-and-socket-io%2Fgithub-activity-feed-example.gif" alt="A Gif of the feed updating on repo commit"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building more sophisticated feeds
&lt;/h2&gt;

&lt;p&gt;So, we have a basic feed, but what’s missing here? If you read the first part of this series on &lt;a href="https://knock.app/blog/the-benefits-of-adding-an-activity-feed-to-your-product" rel="noopener noreferrer"&gt;the benefits of creating an activity feed for your product&lt;/a&gt;, you know that a lot goes into making a &lt;em&gt;good&lt;/em&gt; activity feed.&lt;/p&gt;

&lt;p&gt;For a production-level feed, we’d likely need to consider the following things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This feed should be a separate component with different variants, like a full-page inbox or a popover inside a header. We might also want to style it differently for commits, PRs, merges, etc.&lt;/li&gt;
&lt;li&gt;We’d probably want to store the GitHub activity events in a database and pull from that to populate the frontend activity feed. Then, we could include logic to always show the last N events. Right now, refreshing the client loses the feed because we have no persistence for our activity feed.&lt;/li&gt;
&lt;li&gt;We'd want some way to manage message status, such as marking a notification as seen or dismissing notifications, as well as some mechanism for the user to personalize their feed experience.&lt;/li&gt;
&lt;li&gt;We'd have to manage the infrastructure for this extra server, and in this example we really only have one client user. It would require a lot more server-side logic to handle these updates across thousands of repositories and hundreds of organizations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Creating a basic activity feed is easy, but creating a usable activity feed that will scale with your product is harder. This is where services such as &lt;a href="https://knock.app/" rel="noopener noreferrer"&gt;Knock&lt;/a&gt; can help. Knock allows developers to include&lt;a href="https://knock.app/features/in-app-notifications" rel="noopener noreferrer"&gt; in-app notifications&lt;/a&gt; with pre-built components with no extra infrastructure and no custom code.&lt;/p&gt;

&lt;p&gt;If you want to learn more about Knock, our&lt;a href="https://docs.knock.app/" rel="noopener noreferrer"&gt; documentation&lt;/a&gt; will show you how to build something better than above with less code. You can also join our &lt;a href="https://knock.app/join-slack" rel="noopener noreferrer"&gt;Slack community&lt;/a&gt; to see how others use Knock, or &lt;a href="//mailto:hello@knock.app"&gt;contact us&lt;/a&gt; to chat about using Knock in your application.&lt;/p&gt;

</description>
      <category>node</category>
      <category>socketio</category>
      <category>github</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The benefits of adding an activity feed to your B2B product</title>
      <dc:creator>Jeff Everhart</dc:creator>
      <pubDate>Wed, 24 Apr 2024 11:44:55 +0000</pubDate>
      <link>https://dev.to/knocklabs/the-benefits-of-adding-an-activity-feed-to-your-b2b-product-1ba5</link>
      <guid>https://dev.to/knocklabs/the-benefits-of-adding-an-activity-feed-to-your-b2b-product-1ba5</guid>
      <description>&lt;p&gt;Activity feeds give users a real-time stream of updates about new content, actions, and changes in an application that are relevant to them.&lt;/p&gt;

&lt;p&gt;They are a great way to increase user engagement by helping them be more successful in your application. In the B2B SaaS space, where workflows are complex, and collaboration is often the primary goal, activity feeds can help provide a cohesive user experience. They serve as a centralized hub of notifications and updates that inform users of critical changes, keeping everyone in sync. Providing a snapshot of recent activities allows team members to quickly catch up on what they've missed, thus reducing the time spent searching for information and increasing productivity.&lt;/p&gt;

&lt;p&gt;However, integrating an activity feed into your product has its challenges. It requires a thoughtful approach to design and functionality to ensure that the feed enhances, rather than disrupts, the user experience. An effective activity feed must be finely tuned to deliver personalized and relevant information without overwhelming users. The goal is to create a seamless flow of information that supports the user's day-to-day tasks and boosts their overall success with your application.&lt;/p&gt;

&lt;p&gt;In this two-part series, we want to introduce you to the ideas underpinning activity feeds and why you should consider them for your application before showing you how to build an activity feed for your product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some B2B SaaS activity feeds for inspiration
&lt;/h2&gt;

&lt;p&gt;Let’s look at a few activity feed examples.&lt;/p&gt;

&lt;p&gt;First, we have this from HubSpot. Here, the activity feed shows how customers interact with an email campaign the HubSpot user created.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ufbl9QIf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/the-benefits-of-adding-an-activity-feed-to-your-product/hubspot-activity-feed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ufbl9QIf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/the-benefits-of-adding-an-activity-feed-to-your-product/hubspot-activity-feed.png" alt="HubSpot activity feed" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Source: &lt;a href="https://community.hubspot.com/t5/CRM/Admin-Seeing-Entire-Teams-activity-feed/m-p/756246"&gt;HubSpot&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The feed is organized chronologically with entries from "Yesterday" and "This month" as the primary time categories. Each entry in the feed includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The name of a person and their job title (such as "Manager, Transportation" or "Shipping Coordinator").&lt;/li&gt;
&lt;li&gt;The name of the organization they are associated with.&lt;/li&gt;
&lt;li&gt;A brief description of the activity, for example, "was sent email Checking In / Following Up" or "was sent email Reintroducing ALG Worldwide Logistics."&lt;/li&gt;
&lt;li&gt;A timestamp of when the activity occurred.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also ways for the user to interact with the notification, such as flagging the activity for further action. The design is clean, which is essential for an activity feed as it must clearly present a lot of new, moving information.&lt;/p&gt;

&lt;p&gt;This activity feed from Atlassian is a little different. This is the Confluence activity feed on Android.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5tAnJY0v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://knock.app/assets/blog/the-benefits-of-adding-an-activity-feed-to-your-product/atlassian-feed.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5tAnJY0v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://knock.app/assets/blog/the-benefits-of-adding-an-activity-feed-to-your-product/atlassian-feed.gif" alt="Atlassian activity feed" width="620" height="1318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Source: &lt;a href="https://community.atlassian.com/t5/Confluence-Cloud-articles/Introducing-Activity-Feed-for-Android/ba-p/2179437"&gt;Atlassian&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;This is an example of an internal activity feed for a team-based product. Here, the activity feed is used for team collaboration and document sharing, with functionalities for commenting and reacting to shared content. You can also follow your co-workers in the Confluence feed, blurring the lines between a social media feed and a SaaS app.&lt;/p&gt;

&lt;p&gt;Here’s a more regular activity feed from Drift, used to monitor engagement and follow up with leads or customers:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EBdurOUB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/the-benefits-of-adding-an-activity-feed-to-your-product/drift-feed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EBdurOUB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/the-benefits-of-adding-an-activity-feed-to-your-product/drift-feed.png" alt="Drift activity feed" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Source: &lt;a href="https://gethelp.drift.com/s/article/Activity-Feed"&gt;Drift&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This activity feed shows website visitors and any interactions with them through Drift. Each entry has information about the site visitor, and the conversation thus far, and can interact further with the visitor, either through the Drift app or through email. You can also find more information about the user through LinkedIn.&lt;/p&gt;

&lt;p&gt;Again, the interface is clean and modern and designed to monitor visitor activity and real-time interactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benefits of activity feeds
&lt;/h2&gt;

&lt;p&gt;The core benefit of activity feeds that cuts across all use cases is &lt;strong&gt;awareness&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You use an activity feed in your application to inform users of what is happening. Let’s take the HubSpot example above. The activity feed informs users of what is happening with email campaigns–what emails have been sent, to whom, and whether the recipient has interacted with the email. This gives HubSpot users an immediate understanding of how the email campaign performs.&lt;/p&gt;

&lt;p&gt;From this benefit to users comes benefits to the product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User engagement&lt;/strong&gt;. Activity feeds can enhance user engagement by providing constant, relevant content. They encourage users to interact more frequently with the application, as they can quickly see new updates and respond in real-time. This feature makes the app more dynamic and interactive, leading to longer session times and more frequent visits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User retention&lt;/strong&gt;. Activity feeds play a role in retaining users by keeping them informed and engaged. They create a habit loop for users, who return to the app to check for updates and notifications. This regular interaction fosters a deeper connection with the app. The ability to then personalize feeds, showing content based on user behavior and preferences, makes the app more relevant to the user, thereby increasing the likelihood of them staying engaged over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User insight&lt;/strong&gt;. Activity feeds provide valuable data on user behavior, preferences, and engagement patterns. By analyzing interactions with the feed, such as clicks, likes, shares, and the time spent on different types of content, developers and marketers can gain insights into what users find most appealing and engaging. This information can be used to tailor the app's content strategy, improve features, and even direct product development efforts to better meet user needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also secondary benefits to users from activity feeds beyond simple awareness. In the HubSpot example above, users will be excited to see their campaigns working in real time. This little dopamine hit will make them more engaged with the activities and further engaged with the app. The same goes for Atlassian–Annika will probably be thrilled to get a heart emoji from her boss on her leadership principles doc.&lt;/p&gt;

&lt;p&gt;User engagement and retention also come from another avenue–a lack of task-switching. If the user has to check other applications or windows for updates continually, they will lose focus and will likely get lost elsewhere. The Drift feed shows this. Users can check LinkedIn from within the app without heading off to a new tab, open LinkedIn, type in the person’s name, and then find the necessary information. Instead, the activity feed condenses that workflow into a single click.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key components of an activity feed
&lt;/h2&gt;

&lt;p&gt;What design and product features do each of the example feeds above exhibit?&lt;/p&gt;

&lt;h3&gt;
  
  
  Relevance
&lt;/h3&gt;

&lt;p&gt;Whatever is presented in the activity feed should be relevant to the user. The idea is to bring them further into the app, not remove them from their work. Relevance comes initially from the information you provide in the activity feed. In the case of the HubSpot example, email usage is relevant to a marketer following a campaign. Three other components can increase relevance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Personalization&lt;/strong&gt;. Personalization algorithms can tailor the feed to the user’s behavior, preferences, and past interactions. By analyzing user data and activity, the feed can dynamically present the most pertinent information to each user. For instance, a marketer using HubSpot would see the most recent and relevant campaign data based on the past activities and campaigns they are managing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User customization&lt;/strong&gt;. Allowing users to set preferences for what they wish to see in their activity feed empowers them to customize the content to their needs. Features like filtering by topic, project, or team and muting less relevant threads or notifications can help users maintain focus on what's most important to them. In a practical sense, users could customize their feed in HubSpot to monitor specific campaign metrics that they find most indicative of performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual information&lt;/strong&gt;. Including contextual information such as related projects, previous activities, or upcoming deadlines can make each item in the feed more informative. Embedding links to related reports, conversations, or tasks in the feed items can provide a more holistic view and understanding of the activity. For example, a feed item in HubSpot about a recent email campaign could include open rates, how those rates compare to past campaigns, and how they correlate with subsequent conversions or sales.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By combining these components, an activity feed can become a powerful tool for keeping users informed and engaged, ensuring they have a clear, concise, and relevant stream of information at their fingertips.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeliness
&lt;/h3&gt;

&lt;p&gt;Activity feeds need to display content that is relevant &lt;em&gt;and&lt;/em&gt; timely. Users expect to see the latest updates and actions in their feed. The challenge lies in balancing the need for real-time updates without overwhelming the user. The success of an activity feed in maintaining user engagement often depends on its ability to provide up-to-date information.&lt;/p&gt;

&lt;p&gt;Timeliness comes down to two factors. The first is technical. One of the primary technical considerations in building an activity feed is the choice between push and pull mechanisms for updating the feed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;push systems&lt;/strong&gt;, updates are sent in real-time from the server to the client, often using technologies like WebSockets. This approach is ideal for applications with crucial immediate content updates, ensuring the feed is constantly refreshed with the latest information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull systems&lt;/strong&gt; require the client to request updates from the server periodically. Typically implemented through traditional HTTP requests, this approach is more suited to applications where real-time updates are less critical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second is user-controlled &lt;a href="https://docs.knock.app/concepts/preferences"&gt;preferences&lt;/a&gt;. An activity feed should allow users to customize how and when they receive updates, giving them control over the timeliness of their feed. Users could receive some updates in real-time, while others could be summarized in a daily digest. For example, a project management tool might offer real-time updates for task completions but provide daily summaries for general project progress.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interactivity
&lt;/h3&gt;

&lt;p&gt;Features like commenting, liking, and sharing are integral, enabling users to interact with the content and notifications. Within an activity feed based around team activity, this interactivity fosters a sense of teamwork among users, making the feed more than a passive viewing experience.&lt;/p&gt;

&lt;p&gt;All three examples above use interactivity, whether just flagging the activity for further review in HubSpot or the full commenting, liking, and sharing of Atlassian, interactivity is a critical component that enhances user engagement and collaboration. This engagement improves the user experience by making it more social and connected and provides valuable data to the platform regarding which features are most used and appreciated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and pitfalls of activity feeds
&lt;/h2&gt;

&lt;p&gt;Developing and maintaining activity feeds come with unique challenges and pitfalls that can impact user experience and technical performance.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;scaling&lt;/strong&gt;. Imagine the Drift activity feed if you had 1,000 visitors to your site. Or the Atlassian activity feed with 100 team members. Both would be constantly updating, and it would be impossible to find relevant information. The feed would become overwhelming for the user.&lt;/p&gt;

&lt;p&gt;The simplest solution here would be strong user preferences. Allowing users to control what they see and when would remove much of this burden. This requires more backend complexity but a much better user experience.&lt;/p&gt;

&lt;p&gt;The second is &lt;strong&gt;design&lt;/strong&gt;. Activity feeds must be designed to allow users to understand information at a glance. This includes a clean layout, intuitive icons, and a clear information hierarchy. Poor design can lead to confusion and a steep learning curve, which may drive users away.&lt;/p&gt;

&lt;p&gt;A well-designed feed also needs to account for the cognitive load on the user. Too much visual complexity or dense text can be counterproductive. Simplifying the presentation of information with visual cues such as color coding, avatars, and icons can enhance comprehension.&lt;/p&gt;

&lt;p&gt;The third is managing the &lt;strong&gt;signal-to-noise ratio&lt;/strong&gt;. An activity feed must strike the right balance between being informative and not overwhelming. It should filter out the 'noise' or less essential updates and highlight the 'signal' or the most relevant and critical information.&lt;/p&gt;

&lt;p&gt;Without careful management, users can quickly become frustrated with unimportant notifications, leading to notification fatigue and potential disengagement from the feed or the product itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building an activity feed
&lt;/h2&gt;

&lt;p&gt;Activity feeds help both the user and the product. For the user, they allow them to work better and more efficiently, giving them the real-time information they need to perform their job. For products, they increase engagement and retention, keeping users coming back for more.&lt;/p&gt;

&lt;p&gt;If you are adding an activity feed to your product, try Knock for free by &lt;a href="https://dashboard.knock.app/signup"&gt;signing up for a free account&lt;/a&gt; and get started by checking out our &lt;a href="https://docs.knock.app/in-app-ui/overview"&gt;in-app feed docs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>b2b</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Build a Slack app with SlackKit</title>
      <dc:creator>Jeff Everhart</dc:creator>
      <pubDate>Tue, 23 Apr 2024 16:06:04 +0000</pubDate>
      <link>https://dev.to/knocklabs/build-a-slack-app-with-slackkit-1707</link>
      <guid>https://dev.to/knocklabs/build-a-slack-app-with-slackkit-1707</guid>
      <description>&lt;p&gt;If you’re looking for ways to provide value to your app’s users, a Slack integration is worth building . But adding Slack support to an existing app has historically been a large engineering task: you need to map &lt;a href="https://marketing-site-7vf61de5g-knocklabs.vercel.app/blog/how-to-model-slack-webhook-connections-in-knock"&gt;your app’s data model onto Slack’s users and channels&lt;/a&gt;, provide a way &lt;a href="https://marketing-site-7vf61de5g-knocklabs.vercel.app/blog/how-to-authenticate-users-in-slack-using-oauth"&gt;to authenticate and store credentials&lt;/a&gt;, manage notification templates in domain-specific formats like &lt;a href="https://marketing-site-7vf61de5g-knocklabs.vercel.app/blog/the-guide-to-slack-markdown"&gt;mrkdwn&lt;/a&gt; and &lt;a href="https://marketing-site-7vf61de5g-knocklabs.vercel.app/blog/taking-a-deep-dive-into-slack-block-kit"&gt;Block Kit&lt;/a&gt;, and then create UI to handle the self-serve aspects of this process.&lt;/p&gt;

&lt;p&gt;SlackKit is a library of drop-in components powered by the &lt;code&gt;KnockSlackProvider&lt;/code&gt;, and it uses components like the &lt;code&gt;SlackAuthButton&lt;/code&gt; and &lt;code&gt;SlackChannelsCombobox&lt;/code&gt;. Engineering teams can add support for Slack channel notifications in Knock in hours, not days or weeks.&lt;/p&gt;

&lt;p&gt;In this post, we'll take a high-level look at SlackKit in action and walk through an example Next.js application that implements it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;For this project, there are quite a few configuration variables you need before you can get started. Some of these are typical environment variables you would supply to your application, like API keys, tokens, or client ids.&lt;/p&gt;

&lt;p&gt;The other set of values would typically be determined by your product's business logic, but you can hardcode these values for the time being.&lt;/p&gt;

&lt;p&gt;You can access the &lt;a href="https://github.com/knocklabs/slack-kit-example"&gt;repository for this project&lt;/a&gt; directly or clone it to your local machine using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/knocklabs/slack-kit-example.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Video walkthrough
&lt;/h2&gt;

&lt;p&gt;If you prefer to learn with video, watch it here.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/xsw24iuUTLg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up a Slack app
&lt;/h3&gt;

&lt;p&gt;You can either follow the setup steps outlined here, or use the official instruction in the &lt;a href="https://docs.knock.app/integrations/chat/slack-kit/setup"&gt;SlackKit docs&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Create a new app
&lt;/h4&gt;

&lt;p&gt;First, visit &lt;a href="https://api.slack.com/apps"&gt;https://api.slack.com/apps&lt;/a&gt; and sign into your account. Then, click &lt;code&gt;Create an App&lt;/code&gt; and select the &lt;code&gt;From scratch&lt;/code&gt; option. Next, select which workspace to develop it in. You'll still be able to use it in other workspaces, so this selection isn't critical.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OkPfc2UM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/slack-create-new-app.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OkPfc2UM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/slack-create-new-app.png" alt="create new app" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Add bot features
&lt;/h4&gt;

&lt;p&gt;Under &lt;code&gt;Add features and functionality&lt;/code&gt; select &lt;code&gt;Bots&lt;/code&gt; features. Then, under &lt;code&gt;OAuth and Permissions&lt;/code&gt;, give it the &lt;code&gt;channels:read&lt;/code&gt; scope. It doesn’t need any scopes here since we’ll be sending the scopes we need from the component, but we need to do this so we can expose the redirect url form.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OkPfc2UM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/slack-create-new-app.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OkPfc2UM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/slack-create-new-app.png" alt="create Slack scopes" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Add redirect URL
&lt;/h4&gt;

&lt;p&gt;Under &lt;code&gt;OAuth and Permissions&lt;/code&gt;, find the redirect URL section and add this Knock URL to that field: &lt;code&gt;https://api.knock.app/providers/slack/authenticate&lt;/code&gt;. Knock's API endpoint will handle the OAuth callback for you and store a Slack access token. Finally, Under &lt;code&gt;Manage distribution&lt;/code&gt;, allow it to be publicly distributed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--R5zcJl4_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/redirect-urls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--R5zcJl4_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/redirect-urls.png" alt="save redirect urls" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up Knock
&lt;/h3&gt;

&lt;p&gt;For this tutorial, you'll need a Knock account. If you don't already have one, you can &lt;a href="https://dashboard.knock.app/signup"&gt;sign up on this page&lt;/a&gt;. You can also find these steps outlined in the official &lt;a href="https://docs.knock.app/integrations/chat/slack-kit/setup#add-slack-app-to-knock-slack-channel"&gt;SlackKit docs&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Create a Slack channel
&lt;/h4&gt;

&lt;p&gt;Add &lt;a href="https://docs.knock.app/concepts/channels#channel-settings"&gt;a Slack channel in Knock&lt;/a&gt; with the &lt;code&gt;Client Id&lt;/code&gt; and &lt;code&gt;Client Secret&lt;/code&gt; from the &lt;code&gt;Basic Info&lt;/code&gt; section of your Slack. Take note of this channel id for use in the next step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--R3WKfWL6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/create-slack-channel.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--R3WKfWL6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/create-slack-channel.png" alt="create slack channel" width="800" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Create a new workflow
&lt;/h4&gt;

&lt;p&gt;Create a new &lt;a href="https://docs.knock.app/concepts/workflows"&gt;workflow&lt;/a&gt; with a &lt;a href="https://docs.knock.app/designing-workflows/channel-step"&gt;Slack channel step&lt;/a&gt; pointing to this Slack channel. Take note of this workflow key for use in the following steps, and the example project uses a default workflow key of &lt;code&gt;new-issue&lt;/code&gt;. In the &lt;a href="https://docs.knock.app/designing-workflows/template-editor/overview"&gt;message template&lt;/a&gt; use the following liquid tag to send messages: &lt;code&gt;A new issue was submitted: {{message}}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_oQFvOFw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/create-new-workflow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_oQFvOFw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/create-new-workflow.png" alt="create new workflow" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment variables
&lt;/h3&gt;

&lt;p&gt;All of these values are sourced from environment variables at runtime. The example app will check for these values as the first step. Make a copy of &lt;code&gt;.env.sample&lt;/code&gt; using the following command: &lt;code&gt;cp .env.sample .env.local&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;NEXT_PUBLIC_KNOCK_SLACK_CHANNEL_ID&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This value comes from Knock after you create a Slack channel in the dashboard.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;NEXT_PUBLIC_SLACK_CLIENT_ID&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This value comes from Slack when you create a new Slack app. You can find it in your app's "Basic Info" section.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;KNOCK_SIGNING_KEY&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This value comes from Knock and is used to sign a JWT on behalf of a user to store channel data for Slack tokens and channel ids. You can generate a signing key under "Developers" &amp;gt; "API keys." &lt;strong&gt;This is a secret value and should not be exposed publicly.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;KNOCK_API_KEY&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This value comes from Knock and is used to authenticate server-side API requests. You can find it listed as the secret key under "Developers" &amp;gt; "API keys." &lt;strong&gt;This is a secret value and should not be exposed publicly.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;NEXT_PUBLIC_KNOCK_CLIENT_ID&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This value comes from Knock and is used to authenticate public API requests from the browser. You can find it listed as the public key under "Developers" &amp;gt; "API keys."&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;NEXT_PUBLIC_KNOCK_API_URL&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This value comes from Knock and is used to construct the URL for API endpoints. You can keep the default value for this.&lt;/p&gt;

&lt;h5&gt;
  
  
  &lt;code&gt;NEXT_PUBLIC_REDIRECT_URL&lt;/code&gt;
&lt;/h5&gt;

&lt;p&gt;This value comes from your application. It is where Knock will redirect your user after the OAuth flow with Slack. The default of &lt;code&gt;http://localhost:3000&lt;/code&gt; is valid when running this project locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Knock resource variables
&lt;/h3&gt;

&lt;p&gt;To make the connection between Slack channels and Knock objects, you'll also need to provide details for several types of resources in Knock. To do this, you can replace the values in the &lt;code&gt;getAppDetails&lt;/code&gt; function inside of the &lt;code&gt;/app/lib/app-details.ts&lt;/code&gt; file. These values would typically be determined by your application's business logic, but we can hardcode them for this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAppDetails&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;knock-projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repositories&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repo-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;workflowKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new-issue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should already have a value for &lt;code&gt;workflowKey&lt;/code&gt; from a previous step, and you can choose a &lt;a href="https://docs.knock.app/concepts/users#user-identifiers"&gt;user identifier&lt;/a&gt; to use from the dashboard.&lt;/p&gt;

&lt;h4&gt;
  
  
  Create a tenant
&lt;/h4&gt;

&lt;p&gt;In SlackKit, &lt;a href="https://docs.knock.app/concepts/tenants"&gt;tenants&lt;/a&gt; are used to store the access token for an organization's Slack workspace. You can create a new tenant from the dashboard and include its ID as the value for the &lt;code&gt;tenant&lt;/code&gt; property in the &lt;code&gt;getAppDetails&lt;/code&gt; function. You can also use this cURL command to create a tenant by replacing the values for &lt;code&gt;tenant-id&lt;/code&gt;, &lt;code&gt;KNOCK_API_KEY&lt;/code&gt;, and &lt;code&gt;tenant-name&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nt"&gt;--request&lt;/span&gt; PUT &lt;span class="s1"&gt;'https://api.knock.app/v1/tenants/&amp;lt;tenant-id&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer &amp;lt;KNOCK_API_KEY&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
  "name": "&amp;lt;tenant-name&amp;gt;",
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do &lt;strong&gt;NOT&lt;/strong&gt; have to create your tenant before using SlackKit. If you pass a tenant ID into the &lt;code&gt;KnockSlackProvider&lt;/code&gt; that doesn't exist, SlackKit will create it for you on completion of the OAuth process. However, tenants are an important concept in Knock so we'd recommend reading about these &lt;a href="https://docs.knock.app/integrations/chat/slack-kit/overview#key-concepts"&gt;key concepts&lt;/a&gt; in the SlackKit overview.&lt;/p&gt;

&lt;h4&gt;
  
  
  Create an object
&lt;/h4&gt;

&lt;p&gt;In SlackKit, &lt;a href="https://docs.knock.app/concepts/objects"&gt;objects&lt;/a&gt; are used to store channels and act as the recipient of your workflows. You can't create a new object from the dashboard, so you can use this cURL command to create an object by replacing the values for &lt;code&gt;object-collection&lt;/code&gt;, &lt;code&gt;object-id&lt;/code&gt;, &lt;code&gt;KNOCK_API_KEY&lt;/code&gt;, and &lt;code&gt;object-name&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nt"&gt;--request&lt;/span&gt; PUT &lt;span class="s1"&gt;'https://api.knock.app/v1/objects/&amp;lt;object-collection&amp;gt;/&amp;lt;object-id&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer &amp;lt;KNOCK_API_KEY&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "&amp;lt;object-name&amp;gt;"
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you've done that, update the values in the &lt;code&gt;getAppDetails&lt;/code&gt; function to point to your new object.&lt;/p&gt;

&lt;p&gt;You do &lt;strong&gt;NOT&lt;/strong&gt; have to create your object before using SlackKit. If you pass an object ID into the &lt;code&gt;SlackChannelsCombobox&lt;/code&gt; that doesn't exist, SlackKit will create it for you on the completion when the user registers a channel. However, objects are an important concept in Knock so we'd recommend reading about these &lt;a href="https://docs.knock.app/integrations/chat/slack-kit/overview#key-concepts"&gt;key concepts&lt;/a&gt; in the SlackKit overview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the app locally
&lt;/h2&gt;

&lt;p&gt;Now that you have all of the configuration out of the way, you can install your dependencies using one of the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;#or&lt;/span&gt;
yarn &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After your dependencies have installed, you can run the app in dev mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;span class="c"&gt;#or&lt;/span&gt;
yarn dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you open the app in a browser at &lt;code&gt;http://localhost:3000&lt;/code&gt; you should see a screen that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EFZP-AZB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/confirm-env-vars.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EFZP-AZB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/confirm-env-vars.png" alt="confirm env vars" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This step gives you an opportunity to confirm that you have all of the necessary environment variables needed to use the application.&lt;/p&gt;

&lt;p&gt;If you click &lt;code&gt;Next&lt;/code&gt; you'll navigate to a screen where you can confirm the Knock resources you'll use to connect your Slack app:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4eqyFvim--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/confirm-knock-resources.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4eqyFvim--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/confirm-knock-resources.png" alt="confirm knock resources" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If all of the values in this step look good, you can click &lt;code&gt;Next&lt;/code&gt; to authenticate with your Slack app and store the &lt;code&gt;access_token&lt;/code&gt; in Knock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing Knock and Slack state
&lt;/h2&gt;

&lt;p&gt;Before we dig into the actual SlackKit UI components and their functionality, let's look at how we share certain values across those components in our application.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;@knocklabs/react&lt;/code&gt; package, Knock already exposes a &lt;code&gt;KnockProvider&lt;/code&gt; component that is meant to provide top-level authentication and authorization to components farther down in the component tree.&lt;/p&gt;

&lt;p&gt;With the SlackKit release, the &lt;code&gt;@knocklabs/react&lt;/code&gt; package now also contains a Slack-specific provider, called &lt;code&gt;KnockSlackProvider&lt;/code&gt;. Since these both rely on React context, they are implemented as client-side components in &lt;code&gt;/app/components/providers.tsx&lt;/code&gt; in the following manner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;KnockProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;KnockSlackProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@knocklabs/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Providers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;userToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;knockUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;userToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;knockUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;KnockProvider&lt;/span&gt;
        &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_KNOCK_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;knockUserId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_KNOCK_API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;userToken&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;userToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/*
        The KnockProvider handles authentication with Knock, while the KnockSlackProvider
        provides shared context to all Slack-related components.
        Both are required for Slack-related apps.
        */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;KnockSlackProvider&lt;/span&gt;
          &lt;span class="nx"&gt;knockSlackChannelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_KNOCK_SLACK_CHANNEL_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/KnockSlackProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/KnockProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To create some of the necessary authentication data, like &lt;code&gt;userToken&lt;/code&gt; and &lt;code&gt;userId&lt;/code&gt;, we implement the &lt;code&gt;Providers&lt;/code&gt; component inside of the root &lt;code&gt;layout.tsx&lt;/code&gt; file. In this code, which runs on the server, we also add some additional grants to the &lt;code&gt;userToken&lt;/code&gt; JWT to allow the user to interact with the Slack resources stored in Knock. You can read more about these &lt;a href="https://docs.knock.app/integrations/chat/slack-kit/resource-access-grants"&gt;resource access grants in the docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note 🚨:&lt;/strong&gt; this is necessary because the user in this context is an end user in&lt;br&gt;
  your application who does not have access to Knock as a &lt;a href="https://docs.knock.app/manage-your-account/managing-members"&gt;member of the&lt;br&gt;
  account&lt;/a&gt;.&lt;br&gt;
  Therefore, these grants provide them elevated privileges to operate on&lt;br&gt;
  specific resources using the API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Knock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@knocklabs/node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Grants&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@knocklabs/node/dist/src/common/userTokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@knocklabs/react/dist/index.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Providers&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/providers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./global.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getAppDetails&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./lib/app-details&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;objectId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAppDetails&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signingKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KNOCK_SIGNING_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactElement&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;//Generate a production build in CI/CD even if we're missing the env vars&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signingKey&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Knock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signUserToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;grants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nx"&gt;Knock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buildUserTokenGrant&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tenant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tenant&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nx"&gt;Grants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SlackChannelsRead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;]),&lt;/span&gt;
          &lt;span class="nx"&gt;Knock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buildUserTokenGrant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Grants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ChannelDataRead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Grants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ChannelDataWrite&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;secretOrPrivateKey&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px-12 py-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-2xl font-bold mb-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;SlackKit&lt;/span&gt; &lt;span class="nx"&gt;Demo&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Providers&lt;/span&gt; &lt;span class="nx"&gt;userToken&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;userToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;knockUserId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Providers&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/body&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/html&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Authenticating with Slack
&lt;/h2&gt;

&lt;p&gt;On this screen, you can initiate the OAuth flow with Slack. This page uses two SlackKit components to help facilitate this interaction: &lt;code&gt;SlackAuthButton&lt;/code&gt; and &lt;code&gt;SlackAuthContainer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XdcsWeg---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/initiate-slack-auth.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XdcsWeg---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/initiate-slack-auth.png" alt="initiate oauth flow" width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both of these components are client-side components. The &lt;code&gt;SlackAuthButton&lt;/code&gt; component takes your &lt;code&gt;redirectUrl&lt;/code&gt; and &lt;code&gt;slackClientId&lt;/code&gt; as props:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SlackAuthContainer&lt;/span&gt;
  &lt;span class="nx"&gt;actionButton&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SlackAuthButton&lt;/span&gt;
      &lt;span class="nx"&gt;slackClientId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SLACK_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;redirectUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_REDIRECT_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;onAuthenticationComplete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onAuthComplete&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this button is clicked, it will initiate an OAuth flow in a pop-up window where the user can authenticate with Slack and choose a workspace to install the app. Once the flow is complete, the pop-up window should close and the state of the component should update to show it is &lt;code&gt;Connected&lt;/code&gt; to Slack. Behind the scenes, Knock handles the OAuth callback from Slack and stores an &lt;code&gt;access_token&lt;/code&gt; on the &lt;code&gt;tenant&lt;/code&gt; you provided to the &lt;code&gt;KnockSlackProvider&lt;/code&gt; as &lt;a href="https://docs.knock.app/managing-recipients/setting-channel-data"&gt;channel data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note 🚨: if the pop-up window does not close, double check that the&lt;br&gt;
  &lt;code&gt;redirectUrl&lt;/code&gt; matches your current environment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From here, you can click &lt;code&gt;Next&lt;/code&gt; to choose one or more Slack channels to associate with your object recipient.&lt;/p&gt;
&lt;h2&gt;
  
  
  Choosing Slack channels
&lt;/h2&gt;

&lt;p&gt;On this route, you can associate your Knock object recipient with one or more Slack channels using the &lt;code&gt;SlackChannelCombobox&lt;/code&gt; component. It accepts a &lt;code&gt;slackChannelsRecipientObject&lt;/code&gt; as a prop that specifies a &lt;code&gt;collection&lt;/code&gt; and &lt;code&gt;objectId&lt;/code&gt; for the recipient object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SlackChannelCombobox&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@knocklabs/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SlackChannelWrapper&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slackChannelsRecipientObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SlackChannelCombobox&lt;/span&gt;
        &lt;span class="nx"&gt;slackChannelsRecipientObject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;slackChannelsRecipientObject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you select channels using the combobox input, the component will update the channel data for that object to store the Slack channel ids.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--h74_IoQV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/choose-slack-channel.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--h74_IoQV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/choose-slack-channel.png" alt="choosing slack channels" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note 🚨: if you want to use a private Slack channel, you need to make sure that&lt;br&gt;
  the Slack Bot has been invited to that channel.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At this point, Knock has all of the information it needs to send Slack messages to your selected channels. Click &lt;code&gt;Next&lt;/code&gt; to examine the channel data for your object and tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Examine channel data
&lt;/h2&gt;

&lt;p&gt;In this step, you're not required to take any action, but this page uses the Node SDK to pull channel data for your object recipient and tenant. This will give you an idea of how Knock is storing the data and how these two abstractions interact.&lt;/p&gt;

&lt;p&gt;This data is pulled dynamically, so if you make different channel selections or disconnect and reconnect your Slack auth, the value for &lt;code&gt;access_token&lt;/code&gt; and &lt;code&gt;connections.channelIds&lt;/code&gt; will change as well.&lt;/p&gt;

&lt;p&gt;If you click &lt;code&gt;Next&lt;/code&gt; you can test your workflow end-to-end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trigger your workflow
&lt;/h2&gt;

&lt;p&gt;In this step, you can see a code sample using the Node SDK of how you'd trigger your workflow. This is actually the code that runs in a server action when you submit the form on this page. Go ahead and submit a form to send a Slack message into your designated channels:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jWqIEJdE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/trigger-workflow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jWqIEJdE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/getting-started-with-slack-kit/trigger-workflow.png" alt="a form to trigger our workflow" width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If you need help implementing any of the code in this example app, consider joining our &lt;a href="https://knock.app/join-slack"&gt;Slack community&lt;/a&gt;. We've got over 500 other developers learning how to improve their notification experiences with Knock.&lt;/p&gt;

&lt;p&gt;Thanks for reading and Knock on 🤘&lt;/p&gt;

</description>
      <category>slack</category>
      <category>javascript</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>How push notifications work on Apple and Android</title>
      <dc:creator>Jeff Everhart</dc:creator>
      <pubDate>Mon, 22 Apr 2024 18:14:50 +0000</pubDate>
      <link>https://dev.to/knocklabs/how-push-notifications-work-on-apple-and-android-fn6</link>
      <guid>https://dev.to/knocklabs/how-push-notifications-work-on-apple-and-android-fn6</guid>
      <description>&lt;p&gt;Push notifications are an essential component of many mobile apps, helping developers engage users, improve retention, personalize experiences, and drive action. However, it's crucial for developers to use push notifications responsibly and respectfully to avoid overwhelming or alienating users.&lt;/p&gt;

&lt;p&gt;But how do those notifications work? How does an application get direct access to your mobile OS and put itself at the forefront of your attention? In this post, we’ll walk you through the key concepts behind push notifications to better understand how this process works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic framework of push notifications
&lt;/h2&gt;

&lt;p&gt;Push notifications consist of interactions between the mobile device, your app, server, and the operating system push notification service (OSPNS).&lt;/p&gt;

&lt;p&gt;When the app is launched, it requests the user's permission to receive push notifications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3EJ5Kfwl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-push-notifications-work-on-apple-and-android/apple-consent-popup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3EJ5Kfwl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-push-notifications-work-on-apple-and-android/apple-consent-popup.png" alt="An iOS consent popup" width="448" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Source: &lt;a href="https://developer.apple.com/documentation/usernotifications/asking_permission_to_use_notifications"&gt;Apple Developer&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With consent, the app communicates with the push notification service provided by the operating system, which is Apple Push Notification Service (APNS) for iOS devices or Firebase Cloud Messaging (FCM) for Android devices. The consent interaction generates a device token specific to the app on that device. The app sends this device token to the application server, registering the device for future notifications.&lt;/p&gt;

&lt;p&gt;The application server stores and manages the device tokens, associating them with user profiles or app installs. When an event in the app needs to trigger a notification, the server creates the message, and then sends this message and the device tokens to the push notification service.&lt;/p&gt;

&lt;p&gt;The push notification service processes the requests from the application server, verifying the messages. Using the device tokens, the push notification service identifies the associated devices and routes the notifications to them. The push notification service manages network conditions and device availability, as devices may be offline or in a do-not-disturb mode when the notification is sent. The service might queue the notifications or attempt redelivery later in such cases.&lt;/p&gt;

&lt;p&gt;Once the notification reaches the user's device, the OS displays the notification according to the user's settings and the app's parameters.&lt;/p&gt;

&lt;p&gt;This all seems straightforward. But the interaction that needs to happen between your app and backend, a user’s mobile device, and Apple/Firebase requires a lot of moving parts that you don’t have to consider if you are building email or in-app notifications. Let’s investigate some of these critical components of push notifications to understand how they work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Device tokens: the core of push notifications
&lt;/h2&gt;

&lt;p&gt;The journey begins when the user first launches the app. The app requests permission to send push notifications. This step is crucial, as the app cannot send notifications without the user's consent.&lt;/p&gt;

&lt;p&gt;iOS mandates the explicit permission prompt above. Up until Android 13, apps didn’t have to require permission from a user to send push notifications explicitly. Instead, they could be managed via the Android settings page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--c2GPyTw4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-push-notifications-work-on-apple-and-android/android-settings.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--c2GPyTw4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-push-notifications-work-on-apple-and-android/android-settings.png" alt="An Android settings screen" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Source: &lt;a href="https://developer.android.com/develop/ui/views/notifications/channels"&gt;Android Developers&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From &lt;a href="https://source.android.com/docs/core/display/notification-perm"&gt;Android 13 onwards&lt;/a&gt;, the OS requires developers to use an opt-in model, like Apple. However, developers have more flexibility to design their permission modals in Android.&lt;/p&gt;

&lt;p&gt;Once permission is granted, the device establishes communication with either APNS or FCM. This generates a unique identifier known as the device token.&lt;/p&gt;

&lt;p&gt;The device token is the core part of push notifications. It is a globally unique identifier for the user’s device and that specific app installation on that device. If the user has your application installed on another device, they’ll be granted a separate token. If they uninstall and reinstall your app on that device, they’ll get another device token.&lt;/p&gt;

&lt;p&gt;On Apple devices, you generate a device token using the &lt;a href="https://developer.apple.com/documentation/uikit/uiapplication/1623078-registerforremotenotifications"&gt;registerForRemoteNotifications()&lt;/a&gt; method. On Android, developers declare their intent to send notifications in their &lt;a href="https://developer.android.com/about/versions/13/changes/notification-permission"&gt;manifest&lt;/a&gt;, and then the SDK asks FCM to generate the token.&lt;/p&gt;

&lt;p&gt;The app then transmits this device token to its server, registering the device for future notifications. This registration signals to the server that the device can receive notifications.&lt;/p&gt;

&lt;p&gt;When your server sends a push notification, it must always include this device token in the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  APNS and FCM: the routing system for push notifications
&lt;/h2&gt;

&lt;p&gt;The application (or your servers) doesn’t send push notifications itself. Instead, each operating system has a push notification service, which routes your request to your user's device.&lt;/p&gt;

&lt;p&gt;For Apple, this is the Apple Push Notification Service (APNS). For Android, it is Firebase Cloud Messaging (FCM). The two are mostly similar, with a key difference being that APNS only works with Apple devices, whereas FCM works with Google and Apple devices.&lt;/p&gt;

&lt;p&gt;Here's an overview of how this routing typically works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Triggering the notification&lt;/strong&gt;. An event on the application server triggers a notification. This event could be anything from a new message in a chat app to a price drop alert in a shopping app. The server prepares the notification, which includes setting its content (like the message body and title) and any additional data or commands.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sending the notification to the push notification service&lt;/strong&gt;. The application server sends the notification to APNS or FCM. The server includes the unique device tokens of the target devices in the request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Processing by the push notification service&lt;/strong&gt;. Upon receiving the notification request, the PNS verifies it. This includes checking the sender's credentials and the validity of the device tokens. The PNS may queue the notification for delivery. Depending on the service and the message's priority, it might be delivered immediately or delayed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Delivering to the target device&lt;/strong&gt;. Using the device token, the PNS locates the correct device. If the device is offline or unavailable, the PNS may retry delivery later based on the service’s retry policy. Once the device is reachable, the PNS delivers the notification. The operating system receives the notification on the device and passes it to the appropriate app. The notification is then displayed to the user.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;APNS allows for varying &lt;a href="https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns"&gt;notification priorities&lt;/a&gt;, providing immediate delivery or delivery options at the next opportunity. FCM, on the other hand, offers features like &lt;a href="https://firebase.google.com/docs/cloud-messaging/android/topic-messaging"&gt;topic subscription&lt;/a&gt;, where devices can subscribe to specific topics for notifications, and message collapses to prevent similar notification overload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your app and server: push notification creation and management
&lt;/h2&gt;

&lt;p&gt;With routing and delivering the push notification handled by the push notification services, your application and servers are responsible for creating the notification and managing device tokens. First, the server must craft the notification payload. The payload needs to be structured in a JSON format that complies with the specifications of the target push notification service.&lt;/p&gt;

&lt;p&gt;For APNS, a payload looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aps&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;New Message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You have a new message from John.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;badge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sound&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customData&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;conversationId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12345abcde&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messageType&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;aps&lt;/code&gt; is the Apple-defined key for the payload. The &lt;code&gt;alert&lt;/code&gt; object contains the notification's title and body. The &lt;code&gt;badge&lt;/code&gt; tells the OS the number to display on the app icon and &lt;code&gt;sound&lt;/code&gt; which sound to play when the notification is received. &lt;code&gt;customData&lt;/code&gt; is a custom key-value pair (optional) for additional data the app might use.&lt;/p&gt;

&lt;p&gt;An FCM payload has a similar, with the naming different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;to&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;device_token_here&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;New Message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You have a new message from John.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;conversationId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12345abcde&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messageType&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;priority&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;to&lt;/code&gt; is the target device token (this is in the HTTP headers for APNS). &lt;code&gt;notification&lt;/code&gt; contains the notification's title and body, &lt;code&gt;data&lt;/code&gt; is the custom key-value pairs for additional data, and &lt;code&gt;priority&lt;/code&gt; sets the priority of the message.&lt;/p&gt;

&lt;p&gt;Each push notification service has a limit on the size of the notification payload (4KB for APNS and FCM). Your server should ensure that the payload size does not exceed these limits.&lt;/p&gt;

&lt;p&gt;Your server also has to manage a stateful connection to the push notification service to send each notification request. This request includes the payload and a list of device tokens to which the notification should be delivered. The request is made using server credentials like APNS certificates or FCM API keys for authentication. After sending the notification, your app server should handle the response from the push notification service, such as processing success or error messages and implementing retry logic.&lt;/p&gt;

&lt;p&gt;Your server also has to manage and store device tokens. Usually, these are stored in your user database alongside other user information. These tokens must be stored securely to prevent unauthorized access, as they represent a direct line of access to the user's device.&lt;/p&gt;

&lt;p&gt;Device tokens can change over time, such as when users reinstall the app or update their device's operating system. The mobile app must be designed to detect these changes and resend the updated token to the server. The server, in turn, must update its database with the new token.&lt;/p&gt;

&lt;p&gt;Regularly validating and cleaning up the tokens in the database is essential. This can be done by processing feedback from the push notification service, which may inform the server about invalid or expired tokens that need to be removed or updated.&lt;/p&gt;

&lt;p&gt;Knock provides &lt;a href="https://docs.knock.app/sdks/overview#mobile-sdks"&gt;mobiles&lt;br&gt;
  SDKs&lt;/a&gt; for iOS, Andriod, Flutter, and React Native that can help you manage the user token lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lean On Services for Push Notifications
&lt;/h2&gt;

&lt;p&gt;Apple and Google have built extensive push notification services to help developers use their devices and integrate push notifications into their apps. These services do a lot of the heavy lifting associated with push notifications. As long as you use their SDKs and have a way to store tokens, using push notifications with these services securely is seamless.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://docs.knock.app/integrations/push/overview"&gt;Knock&lt;/a&gt;, we remove this burden even further. You don’t have to manage the stateful connection with each push notification service. You also don’t need to work with two different JSON payload structures–you can send the same templated message to multiple providers.&lt;/p&gt;

&lt;p&gt;Check out how to integrate &lt;a href="https://docs.knock.app/integrations/push/apns"&gt;Apple devices with Knock&lt;/a&gt;, as well as how to integrate &lt;a href="https://docs.knock.app/integrations/push/firebase"&gt;FCM with Knock&lt;/a&gt;. If you need help, &lt;a href="https://knock.app/join-slack"&gt;reach out to us in Slack&lt;/a&gt; or set up a time to &lt;a href="https://knock.app/contact-sales"&gt;talk with our team&lt;/a&gt; today.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>android</category>
      <category>reactnative</category>
      <category>flutter</category>
    </item>
    <item>
      <title>Taking a deep dive into Slack's Block Kit</title>
      <dc:creator>Jeff Everhart</dc:creator>
      <pubDate>Thu, 01 Feb 2024 15:47:45 +0000</pubDate>
      <link>https://dev.to/knocklabs/taking-a-deep-dive-into-slacks-block-kit-1cjm</link>
      <guid>https://dev.to/knocklabs/taking-a-deep-dive-into-slacks-block-kit-1cjm</guid>
      <description>&lt;p&gt;If you’re building Slack notifications, you can keep it simple: a bit of text, maybe an emoji and some &lt;strong&gt;bolding&lt;/strong&gt; to draw attention to what’s essential.&lt;/p&gt;

&lt;p&gt;But you can also go much further with Slack notifications. Slack allows you to use various text options, images, design, and interactivity in your messaging. To put these together, you can use &lt;a href="https://api.slack.com/block-kit" rel="noopener noreferrer"&gt;Slack’s Block Kit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Block Kit is Slack's UI framework for composing the app's messages, modals, and pages. Using it, you can create rich notifications for your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video walkthrough
&lt;/h2&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/aTtgcLrVK-E"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Block Kit is just JSON
&lt;/h2&gt;

&lt;p&gt;This is the most straightforward idea to grasp when working with Slack’s Block Kit. You are just putting JSON objects together, no matter what type of interface you are building. &lt;a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/JSON#json_structure" rel="noopener noreferrer"&gt;JSON&lt;/a&gt; is a string format that closely resembles JavaScript object format, and is made up of key-value pairs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Jeff Everhart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But these key-value pairs don’t have to be just strings. They can be strings, numbers, arrays, booleans, or even other objects. This means you can create complex hierarchies, like we’ll see in the following Slack interfaces.&lt;/p&gt;

&lt;p&gt;Let’s start with a simple example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plain_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is a plain text section block.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, you have the root object, which is contained by the outer curly braces &lt;code&gt;{ … }&lt;/code&gt;. Inside this object, there's a key named &lt;code&gt;blocks&lt;/code&gt;. The value associated with this key is an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array" rel="noopener noreferrer"&gt;array&lt;/a&gt;, indicated by the square brackets &lt;code&gt;[ ... ]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our array here contains just one object, but, as we’ll see, we can put many objects inside this array to build our message UI. Inside this object, there are two key-value pairs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;type&lt;/strong&gt;: This key has a value of "section." This indicates the type or role of this object within the broader structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;text&lt;/strong&gt;: The value associated with this key is another object. This nested object contains two key-value pairs:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;type&lt;/strong&gt;: This has a value of "plain_text". Again, this is the role of the object&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;text&lt;/strong&gt;: This has the value of "This is a plain text.”, the actual content that will be displayed to the user.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;To add another section, we can add more JSON objects to the blocks array. That’s what we’ll do next to build out our message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating and sending a notification with Block Kit
&lt;/h2&gt;

&lt;p&gt;Now, let’s put together something more useful using Block Kit. We’ll build a notification for an application performance monitoring tool, telling the user in Slack what is happening in their application. Typically, these monitoring tools will send notifications when certain conditions are met, like error rates above a certain threshold, or when actions are taken, like a new deployment.&lt;/p&gt;

&lt;p&gt;Here’s what this message will look like in Slack:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Fcomplex-slack-message.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Fcomplex-slack-message.png" alt="A complex Slack message"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Slack has an excellent tool to help build Block Kit, unsurprisingly called &lt;a href="https://app.slack.com/block-kit-builder" rel="noopener noreferrer"&gt;Block Kit Builder&lt;/a&gt;. This interface allows you to visually build out the JSON of your Slack message and get previews in real-time as you make changes.&lt;/p&gt;

&lt;p&gt;Using the ‘add block’ actions on the left-hand side of the UI, you can add blocks to your payload with just a few clicks. The code pane will also highlight syntax errors as you go. Since JSON has a very specific structure, this can be pretty helpful if you’re just learning this syntax or need to debug something.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Fslack-block-kit-builder.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Fslack-block-kit-builder.png" alt="Slack's block kit builder interface"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s what the payload for this notification looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plain_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;APM ALERT: Elevated Error Rates Detected!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2023-12-25&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Service: *User Authentication API*&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Environment: *Production*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fields&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*Metrics*&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;:red_circle: Error Rate: *15%* (Threshold: 5%)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;:red_circle: Response Time: *900ms* (Average: 200ms)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;:red_circle: Throughput: *1200 rpm* (Requests Per Minute)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image_url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://i.imgur.com/Bj577mJ.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alt_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error rates.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*Dashboard Link:* &amp;lt;https://example.com|APM Dashboard&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;divider&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Assign incident to:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accessory&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users_select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;placeholder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plain_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Select a team member&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emoji&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;action_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users_select-action&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plain_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Recommended Actions:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emoji&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;actions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;elements&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkboxes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;options&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*Verify backend database health and connection.*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                            &lt;span class="p"&gt;},&lt;/span&gt;
                            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value-0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                        &lt;span class="p"&gt;},&lt;/span&gt;
                        &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*Check Redis server and related metrics.*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                            &lt;span class="p"&gt;},&lt;/span&gt;
                            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                        &lt;span class="p"&gt;},&lt;/span&gt;
                        &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*Assess recent deployments for potential issues.*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                            &lt;span class="p"&gt;},&lt;/span&gt;
                            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;action_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;actionId-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s break this down into its constituent parts to see what you can add to your messages using Block Kit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding text
&lt;/h3&gt;

&lt;p&gt;There are two ways to add text with Block Kit. First, we can use &lt;code&gt;plain_text&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plain_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;APM ALERT: Elevated Error Rates Detected!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emoji&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is easy for building out simple notifications and one-line messages. By setting &lt;code&gt;emoji&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, you can optionally add emojis to this text. This is also &lt;a href="https://api.slack.com/reference/block-kit/blocks#header" rel="noopener noreferrer"&gt;a Header block&lt;/a&gt;, which makes the font larger and bolds the text.&lt;/p&gt;

&lt;p&gt;But a better way to include text in a message is to use &lt;a href="https://knock.app/blog/the-guide-to-slack-markdown" rel="noopener noreferrer"&gt;Slack’s mrkdwn&lt;/a&gt; to format your messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2023-12-25&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Service: *User Authentication API*&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Environment: *Production*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fields&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*Metrics*&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;:red_circle: Error Rate: *15%* (Threshold: 5%)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;:red_circle: Response Time: *900ms* (Average: 200ms)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;:red_circle: Throughput: *1200 rpm* (Requests Per Minute)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we can use formatting such as bolding to highlight specific text. We can also use emojis to draw the user's attention to particular items in the notification.&lt;/p&gt;

&lt;p&gt;The snippet above also shows how you can add some structure to your payloads using &lt;a href="https://api.slack.com/reference/block-kit/blocks#section" rel="noopener noreferrer"&gt;the Section block&lt;/a&gt;. By adding &lt;code&gt;fields&lt;/code&gt;, we can group text or images into compact side-by-side columns if we include more than one text object:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Fslack-sections.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Fslack-sections.png" alt="A Slack message with sections"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Images &amp;amp; Videos
&lt;/h3&gt;

&lt;p&gt;Next, we have an image of the incident from the dashboard. You can add images to your Slack message by defining the block as an &lt;code&gt;image&lt;/code&gt; type and then passing a publicly available image URL using the &lt;code&gt;image_url&lt;/code&gt; key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image_url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://i.imgur.com/Bj577mJ.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alt_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error rates.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://api.slack.com/reference/block-kit/blocks#image" rel="noopener noreferrer"&gt;Image block&lt;/a&gt; also requires a value be passed for &lt;code&gt;alt_text&lt;/code&gt;, and you can optionally provide a &lt;code&gt;title&lt;/code&gt;. Depending on the use case for your Slack integration, the Image block can also use internal Slack files using the &lt;code&gt;slack_file&lt;/code&gt; option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://api.slack.com/reference/block-kit/blocks#video" rel="noopener noreferrer"&gt;The Video block&lt;/a&gt; is similar to images, with a different block type and a few additional pieces of information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plain_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;How to use Slack.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emoji&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title_url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.youtube.com/watch?v=RRxQQxiM7AA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plain_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Slack is a nifty way to communicate with your team.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emoji&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video_url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&amp;amp;autoplay=1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alt_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;How to use Slack?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;thumbnail_url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://i.ytimg.com/vi/RRxQQxiM7AA/hqdefault.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Arcado Buendia&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;provider_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YouTube&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;provider_icon_url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://a.slack-edge.com/80588/img/unfurl_icons/youtube.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to the Slack docs, the Video block requires the following fields: &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;alt_text&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;thumbnail_url&lt;/code&gt;, and &lt;code&gt;video_url&lt;/code&gt;. Since the Video block is meant to embed and unfurl video links in any Slack context, the &lt;code&gt;&lt;a href="https://api.slack.com/scopes/links.embed:write" rel="noopener noreferrer"&gt;links.embed:write&lt;/a&gt;&lt;/code&gt; scope is required for your integration.&lt;/p&gt;

&lt;p&gt;The example above would render like this inside of Slack.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Fslack-video.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Fslack-video.png" alt="A Slack message with a video"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding interactivity to notifications
&lt;/h2&gt;

&lt;p&gt;Static notifications are good, but one of the biggest benefits of Slack over email as a notification channel is the level of interactivity you can add. Adding interactivity to your Slack notifications allows integrations to enhance user workflows and perform actions in other systems directly from their existing workspace.&lt;/p&gt;

&lt;h3&gt;
  
  
  Links can be interactive
&lt;/h3&gt;

&lt;p&gt;In this example, we'll introduce a small element of interactivity first using a basic link. We can link the user to the APM dashboard so they can find out more information about the incident.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"section"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mrkdwn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*Dashboard Link:* &amp;lt;https://example.com|APM Dashboard&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An important thing to note is that &lt;a href="https://api.slack.com/reference/surfaces/formatting#linking-urls" rel="noopener noreferrer"&gt;Slack mrkdwn links&lt;/a&gt; are subtly different from regular markdown links, using angle brackets, &lt;code&gt;&amp;lt; … &amp;gt;&lt;/code&gt; instead of square brackets &lt;code&gt;[ … ]&lt;/code&gt;. Even though links are the lowest level of interactivity we can provide for users in this context, a direct link to a resource in another system can help streamline workflows for the user.&lt;/p&gt;

&lt;p&gt;Taking the application monitoring example one step further, this link could take the user directly to the offending server or even include a query string to filter to a more particular view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*Dashboard Link:* &amp;lt;https://example.com/dashboard?server=ec2-small&amp;amp;errorType=warn,error|APM Dashboard&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using action blocks
&lt;/h3&gt;

&lt;p&gt;However, sometimes we’ll want to let users perform actions directly from a notification, so let’s move on to a more complex form of interactivity. In the following example, we have two ways for a user to interact with the notifications here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A multi-select to assign the incident to a user&lt;/li&gt;
&lt;li&gt;A list of checkboxes for an incident response checklist&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here are the blocks for these.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Assign incident to:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accessory&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users_select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;placeholder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plain_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Select a team member&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emoji&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;action_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users_select-action&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plain_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Recommended Actions:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emoji&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;actions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;elements&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkboxes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;options&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*Verify backend database health and connection.*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value-0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*Check Redis server and related metrics.*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*Assess recent deployments for potential issues.*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;action_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;actionId-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The crucial parts of these blocks aren’t how you put them together but what you can do with them. In Slack Block Kit, these are ‘Action’ blocks, meaning you can do something with the output when a user selects them. Here, we’re just using multi-select and checkboxes, but you can have radio buttons, inputs, date and time pickers, and buttons.&lt;/p&gt;

&lt;p&gt;You need a backend for your Slack app to use the output of these interactions. Then, you need to add a Request URL to your Slack dashboard so it knows where to send the “action.”&lt;/p&gt;

&lt;p&gt;If you have a fully developed app, this will be a POST endpoint where you’ll manage the interactivity. For our case, we’ll spin up a &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok URL&lt;/a&gt; for a local development application and add that to Slack.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Finteractivity-url.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Finteractivity-url.png" alt="Setting a URL to handle interactivity from Slack"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example, we have a single Next.js API route handler that will accept the interaction payload generated by Slack. We will send our actions to the “/slack/interactive-endpoint” created by a &lt;code&gt;/app/slack/interactive-endpoint/route.js&lt;/code&gt; file. Here’s all the code the create the endpoint and handle the interactive requests in the block JSON above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users_select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedUserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;selected_user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You have been assigned an incident.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkboxes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;block_id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;block_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSelectedOption&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selected_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectedOption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;selectedOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
              &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
              &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isSelectedOption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;
              &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`~&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;~`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://slack.com/api/chat.postMessage`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slackResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;@&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json; charset-utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;slackResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://slack.com/api/chat.update`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slackResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json; charset-utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;slackResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There’s quite a bit going on in this file, so let’s take a second to walk through what’s happening. In the first few lines, we’ll extract the &lt;code&gt;payload&lt;/code&gt; from the POST request Slack sends, along with the &lt;code&gt;channel&lt;/code&gt; and &lt;code&gt;actions&lt;/code&gt; values. Here we’ll create some branches in our code to handle the different types of actions we’ve specified on the &lt;code&gt;action_id&lt;/code&gt; key of our Slack blocks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users_select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//handle user select&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkboxes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//handle checkboxes&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the Slack user selects a value from the users select dropdown, we’ll execute the code in the &lt;code&gt;users_select&lt;/code&gt; branch. In a more robust system, you might make a database call to assign someone to a particular incident, but in this example we’ll just send a message back to Slack tagging that invididual.&lt;/p&gt;

&lt;p&gt;To do that, we’ll create another function called &lt;code&gt;postMessage&lt;/code&gt; that will take a &lt;code&gt;channelId&lt;/code&gt;, some message &lt;code&gt;text&lt;/code&gt; and the &lt;code&gt;selectedUserId&lt;/code&gt; as parameters. In this function, the &lt;code&gt;SLACK_TOKEN&lt;/code&gt; value is a Bot token that we’ve assigned to this app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://slack.com/api/chat.postMessage`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slackResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;@&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json; charset-utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;slackResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much of the data we’ll need to handle interactive selections is passed as a part of the &lt;code&gt;payload&lt;/code&gt; from Slack, either as a part of the base payload or inside of the payload’s &lt;code&gt;actions&lt;/code&gt; key. Once we’ve extracted the data into variables, we can call the &lt;code&gt;postMessage&lt;/code&gt; function to make a POST request to Slack’s API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users_select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedUserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;selected_user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You have been assigned an incident.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this message, is sent it renders in Slack like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Finteractive-message.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Finteractive-message.png" alt="An interactive Slack message"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, this team member knows they have to deal with the incident. As they complete the recommended action, they can check off the items on the checklist.&lt;/p&gt;

&lt;p&gt;Though the checks will show what has been completed, as each of those text descriptions is mrkdwn, we can also use formatting to highlight what has happened further by updating the message.&lt;/p&gt;

&lt;p&gt;In our backend, we want to look through our payload to see what has been checked and strikethrough the text if something has been completed. If the interaction is generated by the checkboxes, we’ll loop through the blocks in the message and compare the checkbox values with the values that have been selected. If they have been selected, we can apply some additional formatting to the option &lt;code&gt;text&lt;/code&gt; property.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkboxes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;//loop through the blocks in the message payload&lt;/span&gt;
  &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//if the current block triggered the interaction&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;block_id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;block_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;//loop through the options on that block&lt;/span&gt;
      &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;//compare the current option value to the list of&lt;/span&gt;
        &lt;span class="c1"&gt;//selected option values and generate a boolean&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSelectedOption&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selected_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectedOption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;selectedOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isSelectedOption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;//if the current option is a selected option&lt;/span&gt;
          &lt;span class="c1"&gt;//add the strikethrough formatting if not already present&lt;/span&gt;
          &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`~&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;~`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;//if not selected, remove any strikethrough formatting&lt;/span&gt;
          &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we need to update the message using the Slack API. We need the channel ID and the initial message timestamp (&lt;code&gt;message.ts&lt;/code&gt;) to identify the message we are updating. We then pass that to our &lt;code&gt;updateMessage&lt;/code&gt; function, which uses the &lt;code&gt;chat.update&lt;/code&gt; API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://slack.com/api/chat.update`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slackResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json; charset-utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;slackResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we select a checkbox, the text gets a strikethrough for all in the channel to see.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Fstrikethrough.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fknock.app%2Fassets%2Fblog%2Ftaking-a-deep-dive-into-slack-block-kit%2Fstrikethrough.gif" alt="Applying strikethrough formatting as messages are checked"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A good use for this style of updating is cleaning up after your notifications. If someone selects a choice from two buttons, you can remove the buttons and show the selection. You can see more design best practices for using Slack Block Kit in the &lt;a href="https://api.slack.com/block-kit/designing" rel="noopener noreferrer"&gt;Slack docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Depending on your product, a Slack integration can be a valuable addition to your feature set. At Knock, we're focused on helping developers leverage Slack as a notification channel, so here are a few additional posts we've written on using Slack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/how-to-think-about-slack-as-a-notification-channel"&gt;How to think about Slack as a notification channel for your app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/the-guide-to-slack-markdown"&gt;The developer's guide to Slack's Markdown formatting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/how-to-authenticate-users-in-slack-using-oauth"&gt;How to autheticate users in Slack using OAuth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/the-guide-to-designing-slack-notifications"&gt;The product manager's guide to designing Slack notifications&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using Knock to Manage Your Slack Notification Design
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://knock.app/" rel="noopener noreferrer"&gt;Knock&lt;/a&gt; allows engineering and product teams to build their notification system, without dealing with the infrastructure required. You can manage notifications across multiple channels, such as email, in-app, Push, and Slack.&lt;/p&gt;

&lt;p&gt;If you build out your Slack messages using Block Kit, you can quickly transfer your JSON to the &lt;a href="https://docs.knock.app/integrations/chat/slack/designing-slack-templates" rel="noopener noreferrer"&gt;Knock JSON block editor&lt;/a&gt;. A bonus is that you can use &lt;a href="https://shopify.github.io/liquid/" rel="noopener noreferrer"&gt;liquid&lt;/a&gt; within your blocks to inject variables and add control flows. You can follow the instructions to set up a &lt;a href="https://docs.knock.app/integrations/chat/slack/overview" rel="noopener noreferrer"&gt;Knock and Slack&lt;/a&gt; integration. With Knock, you can build out your OAuth flow, set up messaging templates, and use all the interactivity of Slack that we showed above.&lt;/p&gt;

&lt;p&gt;If you want to try it out, &lt;a href="https://dashboard.knock.app/signup" rel="noopener noreferrer"&gt;sign up for a free account&lt;/a&gt; or &lt;a href="https://knock.app/contact-sales" rel="noopener noreferrer"&gt;chat with our team&lt;/a&gt;. 👋&lt;/p&gt;

</description>
      <category>slack</category>
      <category>json</category>
      <category>javascript</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>How to authenticate users with Slack using OAuth</title>
      <dc:creator>Jeff Everhart</dc:creator>
      <pubDate>Tue, 30 Jan 2024 14:22:37 +0000</pubDate>
      <link>https://dev.to/knocklabs/how-to-authenticate-users-with-slack-using-oauth-2ged</link>
      <guid>https://dev.to/knocklabs/how-to-authenticate-users-with-slack-using-oauth-2ged</guid>
      <description>&lt;p&gt;Slack won't let your application anywhere near its users and platform without authentication through the &lt;a href="https://api.slack.com/authentication/oauth-v2"&gt;Slack OAuth flow&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This gives Slack a secure way to control access to its API, ensuring that only authorized applications can interact with user data and functionality. It also allows users to grant specific permissions to your application based on the required scopes, thereby protecting their privacy and data security.&lt;/p&gt;

&lt;p&gt;Luckily, implementing the OAuth flow in your application is straightforward, so developers can integrate their applications with Slack's features and functionalities, enabling them to enhance their app's capabilities, such as posting messages, scheduling reminders, or reading from channels where the app is present.&lt;/p&gt;

&lt;p&gt;In this post, we're going to build a simple app that does just that - uses the OAuth flow to grant our application access to user data, all in less than 100 lines of code, including error handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video walkthrough
&lt;/h2&gt;

&lt;p&gt;If you prefer to learn with video, watch it here.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/o6k51xE1qBg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Why third-party apps use OAuth
&lt;/h2&gt;

&lt;p&gt;Using OAuth with Slack offers several significant benefits for your application:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Secure authentication&lt;/strong&gt;. OAuth is a secure standard for authorization that allows applications to authenticate users without handling their passwords. This minimizes the security risks associated with storing and transmitting sensitive credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped access&lt;/strong&gt;. OAuth allows applications to request specific levels of access, commonly known as scopes, to a user's Slack resources, such as channels, messages, or user data. This means the application doesn’t gain more access than necessary, following the principle of least privilege.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User experience&lt;/strong&gt;. The OAuth flow provides a more streamlined user experience by allowing users to authenticate with an existing Slack account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token-based access&lt;/strong&gt;. After the OAuth process is complete, the application receives an access token to interact with the Slack API on behalf of the user. This token can be stored and used for future authenticated requests without further user interaction as long as the token remains valid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revoke access&lt;/strong&gt;. Users can revoke the application's access through their Slack settings, giving them control over which third-party apps can access their Slack account.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Building the Slack OAuth flow into a Node.js application
&lt;/h2&gt;

&lt;p&gt;Let's build a small Node.js application allowing you to authenticate using the Slack OAuth flow. This app only needs two components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A route to call the Slack OAuth flow URL&lt;/li&gt;
&lt;li&gt;A route to handle the redirect with the Slack access token&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We'll also need a few dependencies to help us build this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;dotenv express axios express-session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need these for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dotenv&lt;/code&gt;: loads environment variables from a &lt;code&gt;.env&lt;/code&gt; file into &lt;code&gt;process.env&lt;/code&gt; to make it easy to manage configuration variables like API keys.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;express&lt;/code&gt;: a minimalist web server for Node.js.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;axios&lt;/code&gt;: a common library for making HTTP requests.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;express-session&lt;/code&gt;: session middleware for Express used to store session data on the server and allow persistence across requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the complete code, which we'll add to a file called &lt;code&gt;app.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express-session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Configure session middleware&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SESSION_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resave&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;saveUninitialized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Redirect user to Slack's OAuth authorization page&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/slack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;channels:read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://slack.com/oauth/v2/authorize?client_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_CLIENT_ID&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;user_scope=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;redirect_uri=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Callback URI endpoint&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/slack/callback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://slack.com/api/oauth.v2.access&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Save the tokens in session or a secure place&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authed_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slack_access_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slack_user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authed_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Fetch user's channels&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channelsResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://slack.com/api/conversations.list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channelsResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;channelsResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channels&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`Authorization successful! Here are your channels: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error fetching channels: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;channelsResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error authorizing with Slack: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Server error when exchanging code for token or fetching channels.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Start the server&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go through this to see how we're implementing OAuth in this app.&lt;/p&gt;

&lt;p&gt;At the top, we have this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line loads environment variables from a &lt;code&gt;.env&lt;/code&gt; file into &lt;code&gt;process.env&lt;/code&gt;. We haven't got our environment variables yet. We'll do that in a moment.&lt;/p&gt;

&lt;p&gt;Then we need to set up Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express-session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we require express and create a constant variable called &lt;code&gt;app&lt;/code&gt; that represents the Express application. We also set the port variable to 3000, which is the port our server will listen on.&lt;/p&gt;

&lt;p&gt;Then we're going to set up our session using &lt;code&gt;express-session&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SESSION_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resave&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;saveUninitialized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sessions are used to store information about the user across multiple requests. The secret is used to sign the session ID cookie, &lt;code&gt;resave&lt;/code&gt; and &lt;code&gt;saveUninitialized&lt;/code&gt; are flags for session saving behavior, and cookie: &lt;code&gt;{ secure: "auto" }&lt;/code&gt; ensures that cookies are secure in production (used over HTTPS).&lt;/p&gt;

&lt;p&gt;With our server set up, we can create two endpoints to communicate with Slack. First, we'll create an endpoint for redirecting to the Slack OAuth flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/slack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;channels:read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://slack.com/oauth/v2/authorize?client_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_CLIENT_ID&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;user_scope=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;redirect_uri=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this route, when a GET request is made to the &lt;code&gt;/auth/slack&lt;/code&gt; path, the server responds by redirecting the client to Slack's OAuth authorization page.&lt;/p&gt;

&lt;p&gt;The Slack URL is constructed with the query parameters from your application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[https://slack.com/oauth/v2/authorize]&lt;/code&gt;(&lt;a href="https://slack.com/oauth/v2/authorize"&gt;https://slack.com/oauth/v2/authorize&lt;/a&gt;) is the base URL for Slack's OAuth 2.0 authorization endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;client_id=${process.env.SLACK_CLIENT_ID}&lt;/code&gt; is the query parameter that
includes the client ID for your application, which you obtain from Slack when you
register your application. It identifies which application is making the request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;amp;user_scope=${encodeURIComponent(scopes)}&lt;/code&gt; is the query parameter that
includes the OAuth scopes. The scopes variable contains the permissions the application
requests from the user's Slack account. In this case, it asks permission to read
the user's channels. URL encoding ensures that the scope string is safely formatted
for inclusion in a URL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;amp;redirect_uri=${encodeURIComponent(process.env.SLACK_REDIRECT_URI)}&lt;/code&gt; is the query parameter for the URL-encoded redirect URI, telling Slack
where to redirect the user after they have either approved or denied the authorization
request. The redirect URI must match one of the URIs configured in your Slack app
settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the user navigates to your server's &lt;code&gt;/auth/slack&lt;/code&gt; endpoint, they are immediately redirected to the Slack authorization page with the proper query parameters set, starting the OAuth flow.&lt;/p&gt;

&lt;p&gt;Our next route is our Slack OAuth callback route. This is the endpoint Slack will call when they have authenticated the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Callback URI endpoint&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/slack/callback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://slack.com/api/oauth.v2.access&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Save the tokens in session or a secure place&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authed_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slack_access_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slack_user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authed_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Fetch user's channels&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channelsResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://slack.com/api/conversations.list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channelsResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;channelsResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channels&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`Authorization successful! Here are your channels: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error fetching channels: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;channelsResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error authorizing with Slack: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Server error when exchanging code for token or fetching channels.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break this down. The whole thing is a GET route handler for the &lt;code&gt;/auth/slack/callback&lt;/code&gt; path. This is the URL where Slack redirects users after they have authorized the application.&lt;/p&gt;

&lt;p&gt;When Slack redirects back to this URL, it will include a temporary authorization code as a query parameter that can be exchanged for an access token, which we access using &lt;code&gt;const { code } = req.query&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We then POST that code using Axios with our &lt;code&gt;CLIENT_ID&lt;/code&gt;, &lt;code&gt;CLIENT_SECRET&lt;/code&gt;, and &lt;code&gt;REDIRECT_URI&lt;/code&gt; to the Slack API's &lt;code&gt;oauth.v2.access&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;If the request is successful, meaning &lt;code&gt;tokenResponse.data.ok&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt;, then we can use an access token to access Slack data as long as it is within the &lt;code&gt;scopes&lt;/code&gt;. We save the access token in the session and then use it with the &lt;code&gt;conversations.list&lt;/code&gt; API method. This sends back a list of channel names to the browser. The channel names are extracted from the API response and sent to the browser.&lt;/p&gt;

&lt;p&gt;This seems like a lot of back-and-forth, and it is, but it can be summarized like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5JJED9Fa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/oauth-flow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5JJED9Fa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/oauth-flow.png" alt="diagram of the oauth flow" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Steps 1 &amp;amp; 2 happen in the &lt;code&gt;/auth/slack&lt;/code&gt; route, and steps 3 through 6 occur in the &lt;code&gt;auth/slack/callback&lt;/code&gt; route.&lt;/p&gt;

&lt;p&gt;Finally, we can start the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on &amp;lt;http://localhost&amp;gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we were to run this now with &lt;code&gt;node app.js&lt;/code&gt; it would error because we're missing all the environment variables we need. We need to create the &lt;code&gt;.env&lt;/code&gt; file that &lt;code&gt;dotenv&lt;/code&gt; will read the environment variables from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env
SLACK_CLIENT_ID=your-slack-client-id
SLACK_CLIENT_SECRET=your-slack-client-secret
SLACK_REDIRECT_URI=your-slack-redirect-uri
SESSION_SECRET=some-random-string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But, of course, we don't have those variables. Let's move to Slack to create our application and get a client ID and client secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up OAuth in Slack
&lt;/h2&gt;

&lt;p&gt;It's quick and easy to get this information from Slack. Head to the &lt;a href="https://api.slack.com/apps"&gt;Slack API Apps&lt;/a&gt; page and hit Create New App:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OKiprQUQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-create-new-app.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OKiprQUQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-create-new-app.png" alt="the slack create new app screen" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose “From Scratch,” then name your app and pick a workspace to develop it in:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Mx2MPlBO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-create-new-app-modal.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Mx2MPlBO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-create-new-app-modal.png" alt="naming your new slack app" width="800" height="781"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From there, the first thing we want to do is set our permissions:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6iaBLYq6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-permissions.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6iaBLYq6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-permissions.png" alt="set the permissions on your slack app" width="800" height="1055"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scroll down until you see the 'Scopes' heading.&lt;br&gt;
This is where we will set the scopes that we used in the code above. Here, we're just using &lt;code&gt;channels:read as a user token scope&lt;/code&gt;, but you need to select the correct permissions for your application:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QIlua8F2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-scopes.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QIlua8F2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-scopes.png" alt="set the scopes of your slack app" width="800" height="798"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, we need to set the redirect URL. Because we are developing this locally for now, the full redirect URL from our code above would be &lt;code&gt;http://localhost:3000/auth/slack/callback&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, if we tried to add that to Slack, we'd get an error message because the HTTP protocol isn't secure. We could deploy our application to an online server, but instead, we can use &lt;a href="https://ngrok.com/"&gt;ngrok&lt;/a&gt;. ngrok is an ingress controller that will give us an HTTPS URL for our localhost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Development &amp;amp; testing only&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ngrok opens a tunnel between the web and your local machine. It should only be used for the development and testing of this feature. In some cases, these local tunnels can be blocked by your network.&lt;/p&gt;

&lt;p&gt;You can download and install ngrok using the instructions from the link above and then run it using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ngrok http http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If ngrok is installed correctly, this command will print some data to your console that includes a 'Forwarding' URL. You can append the &lt;code&gt;/auth/slack/callback&lt;/code&gt; path to this URL and then add it to Slack:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Z8CnkpOO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-redirect-urls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Z8CnkpOO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-redirect-urls.png" alt="set the redirect url of your slack app" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, let's grab our client ID and secret from the 'Basic Information' screen in Slack:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Rlni0h3d--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-app-credentials.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Rlni0h3d--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/slack-app-credentials.png" alt="the slack app credentials screen" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can now add all those details to our .env file: the &lt;code&gt;SLACK_CLIENT_ID&lt;/code&gt;, &lt;code&gt;SLACK_CLIENT_SECRET&lt;/code&gt;, and &lt;code&gt;SLACK_REDIRECT_URI&lt;/code&gt;. Now, we can run the app with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can access the application with our ngrok URL appended with &lt;code&gt;/auth/slack&lt;/code&gt;. In some cases, firewalls or network settings may block ngrok's access to your local machine. If all is working correctly, you'll be redirected to Slack and see this page:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jzt8B2Dh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/oauth-grant-flow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jzt8B2Dh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/oauth-grant-flow.png" alt="the OAuth grant step" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click 'Allow,' Slack will then pass you back to the redirect URL. The code above will run to obtain the channel list, and you'll see this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--L2kRP9ET--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/successful-auth-screen.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L2kRP9ET--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://knock.app/assets/blog/how-to-oauth-users-with-slack/successful-auth-screen.png" alt="a webpage that lists your slack channels" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations, you've OAuthed into Slack. You can now build out that application to use the Slack functionality and data as needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth simplifies authorization
&lt;/h2&gt;

&lt;p&gt;More and more services are defaulting to OAuth as their authentication mechanism. It is secure and allows developers and users to control both access and data using clear scopes. It is straightforward for developers to implement and users to use, as they don't have to use new passwords.&lt;/p&gt;

&lt;p&gt;You can integrate your OAuth flow&lt;a href="https://docs.knock.app/integrations/chat/slack/building-oauth-flow"&gt; with Knock&lt;/a&gt; so that the access, &lt;a href="https://docs.knock.app/integrations/chat/slack/slack-apps-and-scopes"&gt;scopes&lt;/a&gt;, and data received can be used in your notifications. This means that all you need for extensive Slack notifications is the few lines of code above, and then Knock can take care of the rest.&lt;/p&gt;

&lt;p&gt;If you want to add Slack notifications to your application, you can &lt;a href="https://dashboard.knock.app/signup"&gt;sign up for Knock&lt;/a&gt; or reach out to us on &lt;a href="https://knockcustomers.slack.com/join/shared_invite/zt-1l77r71t1-ll2HRAJCUucNo64_H7odoA#/shared-invite/email"&gt;Slack to chat&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>node</category>
      <category>slack</category>
      <category>express</category>
      <category>oauth</category>
    </item>
    <item>
      <title>How to send in-app notifications with Next.js</title>
      <dc:creator>Jeff Everhart</dc:creator>
      <pubDate>Mon, 29 Jan 2024 21:34:47 +0000</pubDate>
      <link>https://dev.to/knocklabs/how-to-send-in-app-notifications-with-nextjs-4eab</link>
      <guid>https://dev.to/knocklabs/how-to-send-in-app-notifications-with-nextjs-4eab</guid>
      <description>&lt;p&gt;&lt;a href="https://nextjs.org/"&gt;Next.js&lt;/a&gt; continues to be one of the most widely-used frameworks to build full-stack web applications. In version 13, Next.js introduced the &lt;a href="https://nextjs.org/docs/app/building-your-application/routing"&gt;App Router&lt;/a&gt;, a new routing paradigm built on top of &lt;a href="https://nextjs.org/docs/app/building-your-application/routing"&gt;React Server Components&lt;/a&gt; that works alongside the existing Pages Router. These changes offer developers a lot of benefits around data fetching and reduced bundle size, but also present a fundamental change to how you work in Next.js.&lt;/p&gt;

&lt;p&gt;In this guide you'll learn how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a notification workflow with an in-app feed step in Knock&lt;/li&gt;
&lt;li&gt;identify users and trigger workflows with the Knock Node.js SDK and Server Actions&lt;/li&gt;
&lt;li&gt;integrate the &lt;code&gt;KnockFeedProvider&lt;/code&gt; with React Server Components&lt;/li&gt;
&lt;li&gt;secure your application for production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Typically, real-time notification feeds are time-intensive features to create. First, you need to manage the infrastructure for WebSockets and Publish/Subscribe messaging patterns. Second, you need to deal with user preferences, user presence detection, storage and logging. And finally, you need to build out the frontend UI and underlying APIs to display the notifications. Luckily, Knock comes with all of that out of the box.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://knock.app/features/in-app-notifications"&gt;in-app feed&lt;/a&gt; channel in Knock manages all of the backend infrastructure for you. The &lt;code&gt;@knocklabs/react-notification-feed&lt;/code&gt; library is &lt;a href="https://docs.knock.app/in-app-ui/react/feed#common-recipes"&gt;highly customizable&lt;/a&gt;, with advanced features like experimental cross-browser feed synchronization. If you need a higher level of control, the &lt;code&gt;@knocklabs/client&lt;/code&gt; library is a low-level JavaScript SDK that you can use to build your &lt;a href="https://docs.knock.app/in-app-ui/javascript/quick-start"&gt;own custom client-side experience&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video walkthrough
&lt;/h2&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/yN0A_wjaWmc"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.knock.app/in-app-ui/react/feed"&gt;React in-app feed UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/knocklabs/knock-node"&gt;Knock Node.js SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.knock.app/integrations/in-app/knock"&gt;Knock in-app feed channel details&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to try out Knock to power your notifications, you can &lt;a href="https://dashboard.knock.app/signup"&gt;sign up for free here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>javascript</category>
      <category>realtime</category>
    </item>
    <item>
      <title>The developer's guide to Slack's Markdown formatting</title>
      <dc:creator>Colin White</dc:creator>
      <pubDate>Tue, 31 Oct 2023 18:06:17 +0000</pubDate>
      <link>https://dev.to/knocklabs/the-developers-guide-to-slacks-markdown-formatting-22cl</link>
      <guid>https://dev.to/knocklabs/the-developers-guide-to-slacks-markdown-formatting-22cl</guid>
      <description>&lt;p&gt;&lt;a href="https://api.slack.com/reference/surfaces/formatting"&gt;Slack mrkdwn&lt;/a&gt; is Slack’s version of the Markdown language tailored for their platform. It allows developers to craft visually appealing and structured messages as well as emphasize critical information and add flair to their Slack messages.&lt;/p&gt;

&lt;p&gt;In this guide, we’ll explore the nuances of mrkdwn, from its basic formatting capabilities to advanced features, coupled with practical examples to get you started and build the right Slack notifications for your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  mrkdwn isn’t Markdown
&lt;/h2&gt;

&lt;p&gt;At first glance, Slack’s mrkdwn and the popular Markdown syntax appear remarkably similar.&lt;/p&gt;

&lt;p&gt;Both Slack’s mrkdwn and John Gruber’s Markdown syntax offer a way to richly format text using simple, plaintext notations. However, the design and capabilities of each of the languages is different due to their different purposes.&lt;/p&gt;

&lt;p&gt;Markdown was designed as a lightweight markup language to convert plaintext into structurally valid HTML. It allows writers to easily ‘markup’ their text to convert it cleanly into HTML. Most developers will be used to Markdown in Github. You can use Markdown in both the Readme of a repo and individual commit messages. Markdown is also a core component of the Notion platform.&lt;/p&gt;

&lt;p&gt;Slack’s mrkdwn is a custom formatting syntax tailored specifically for the Slack platform. Its sole goal is to make it easier to write rich, visually appealing text within Slack and take advantage of the core features of the platform such as @-mentions and channels.&lt;/p&gt;

&lt;p&gt;The different purpose leads to core differences in the syntax. Most obviously, Markdown supports multiple header levels using #. For instance, &lt;code&gt;### Header 3&lt;/code&gt; would represent a &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; tag in HTML. mrkdwn doesn’t support multiple header levels in the same way. When would you use a header in a Slack message? It’s more streamlined for shorter messages.&lt;/p&gt;

&lt;p&gt;The same goes for tables or syntax highlighting. These are core for a full document, but less important in Slack messages. Conversely, mrkdwn has special link formats, like &lt;code&gt;&amp;lt;@U12345678&amp;gt;&lt;/code&gt; for user mentions or &lt;code&gt;&amp;lt;!channel&amp;gt;&lt;/code&gt; to notify all members of a channel. Markdown doesn’t have a native equivalent for these functionalities given how platform-specific they are.&lt;/p&gt;

&lt;p&gt;There are also some design choices within mrkdwn that are different from Markdown, such as the use of underscores for italics rather than a single asterisk, or angle brackets for links instead of square brackets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic formatting: the foundations of mrkdwn
&lt;/h2&gt;

&lt;p&gt;The core elements of mrkdwn help you convey your messages with clarity, emphasize key points, and make your text easy to scan and understand.&lt;/p&gt;

&lt;p&gt;Three elements are commonly used with regular text:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrapping your text with asterisks (&lt;code&gt;*&lt;/code&gt;) characters bolds the text to emphasize a word or phrase.&lt;/li&gt;
&lt;li&gt;Wrapping your text with underscores (&lt;code&gt;_&lt;/code&gt;) characters italicizes the text to emphasize subtly or indicate titles of works.&lt;/li&gt;
&lt;li&gt;Wrapping your text with tildes (&lt;code&gt;~&lt;/code&gt;) characters strikes through the text to indicate corrections or highlight something that’s no longer relevant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then we have two ways to add code to Slack messages. This could be particularly useful if you are building alerts for an application monitoring platform and need to show code that has failed.&lt;/p&gt;

&lt;p&gt;Inline code is good for highlighting code, commands, or special terminology. For this, you wrap your text with backticks (&lt;code&gt;`&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Multiline code blocks are ideal for sharing longer code snippets or preformatted text. To do this, you wrap your text with three backticks (&lt;code&gt;&lt;/code&gt;`&lt;code&gt;&lt;/code&gt;). To add a new line, you have to use &lt;code&gt;\n&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Throughout, we are using the &lt;a href="https://api.slack.com/block-kit"&gt;Block Kit&lt;/a&gt; to show how you build mrkdwn to use with the Slack API. Block Kit enables developers to build interactive and visually appealing messages in Slack. By incorporating mrkdwn within these blocks, you can achieve a fine balance between structure and textual presentation, ensuring your messages are not only interactive but also clear and captivating.&lt;/p&gt;

&lt;p&gt;If you are writing these into your code for a notification app, here’s how they would look:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is *bold* &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; This is _italic_ &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; This is ~strikethrough~ &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; This is `code` &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; ```This is a code block&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;And it's multi-line```&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This message will read in Slack as:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eMk8oIrq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/txhdu5bkmkn6ikk7ujce.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eMk8oIrq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/txhdu5bkmkn6ikk7ujce.png" alt="Slack Formatting" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The primary goal of this formatting is to enhance comprehension and draw attention to essential information. Overuse might make messages harder to read. So, while it’s tempting to jazz up every message with formatting, always prioritize clarity.&lt;/p&gt;

&lt;p&gt;You can find all the special formatting for mrkdwn in the &lt;a href="https://api.slack.com/reference/surfaces/formatting"&gt;Slack mrkdwn formatting docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links and mentions: engaging directly with users and content
&lt;/h2&gt;

&lt;p&gt;A foundational element of Slack is that you can use notifications to directly engage individuals and teams through mentions and direct those users to specific channels, applications, or sites through links.&lt;/p&gt;

&lt;p&gt;We can split these into two types: internal Slack links and mentions and external links to sites and emails.&lt;/p&gt;

&lt;p&gt;Internally, mrkdwn uses the @ symbol to mention a specific user (such as &lt;code&gt;@colin&lt;/code&gt;) or user group, such as &lt;code&gt;@here&lt;/code&gt;, &lt;code&gt;@everyone&lt;/code&gt;, or &lt;code&gt;@channel&lt;/code&gt;. As with when you are working directly in Slack, etiquette is important here. Don’t &lt;code&gt;@everyone&lt;/code&gt; unless you really need everyone.&lt;/p&gt;

&lt;p&gt;Internal to Slack you can also highlight and direct users to other channels, using the hash symbol (#).&lt;/p&gt;

&lt;p&gt;To link away from Slack, you use hyperlinks. The format for these in mrkdwn is &lt;code&gt;&amp;lt;URL|Displayed Text&amp;gt;&lt;/code&gt;. So &lt;code&gt;&amp;lt;https://knock.app|Knock&amp;gt;&lt;/code&gt; will render as a clickable link titled “Knock” leading to &lt;a href="https://knock.app/"&gt;https://knock.app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can use this syntax to link to an email address as well, you just prepend the email address with the mailto protocol. So &lt;code&gt;&amp;lt;mailto:hello@knock.app|hello@knock.app&amp;gt;&lt;/code&gt; will render as a clickable “&lt;a href="mailto:hello@knock.app"&gt;hello@knock.app&lt;/a&gt;” link that opens a user’s default email client with &lt;a href="mailto:hello@knock.app"&gt;hello@knock.app&lt;/a&gt; as the recipient.&lt;/p&gt;

&lt;p&gt;Here are code examples of these:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is a link to the &amp;lt;https://knock.app|Knock website&amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; This is a mention of user @andrew &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; This is an @channel mention &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; This is a link to the #random channel &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; This is an email link &amp;lt;mailto:hello@knock.app|hello@knock.app&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This will show in Slack as:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dwnzPTP_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0qsija9jrmuhm6cgks5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dwnzPTP_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0qsija9jrmuhm6cgks5d.png" alt="Slack Taggin" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When leveraging special links and mentions, always remember to consider the context and the intended audience. While mentions are a powerful way to get someone’s attention, they can be disruptive if overused.&lt;/p&gt;

&lt;p&gt;Likewise, linking to channels or external sites should be done judiciously to ensure the message remains clear and focused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lists and blockquotes: structuring your message
&lt;/h2&gt;

&lt;p&gt;Structuring messages lets you convey multiple points or highlight a specific piece of information. In mrkdwn, you can use lists and blockquotes to make your message more digestible.&lt;/p&gt;

&lt;p&gt;There is no particular syntax for lists within mrkdwn, at least when sending messages via the API. Instead, you can use whichever symbol you want to specify an unordered list&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;- Item 1 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; - Item 2 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; - Item 3 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; - Item 4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This renders to:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VQuG_jUp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ozi7gjike0vw172l4fex.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VQuG_jUp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ozi7gjike0vw172l4fex.png" alt="Slack Unordered" width="800" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ordered lists are just numbers or letters as the symbol. For instance:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1. Item 1 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; 2. Item 2 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; 3. Item 3 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; 4. Item 4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This becomes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ayjh-oG9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/weht67lqw1io52e43xzl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ayjh-oG9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/weht67lqw1io52e43xzl.png" alt="Slack Ordered" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To add blockquotes to highlight or emphasize a particular section, like a quotation or important note, you start a line with &lt;code&gt;&amp;gt;&lt;/code&gt; followed by a space.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt; This is a blockquote. It can span multiple lines and is often used to highlight specific pieces of information or quotations.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This renders as:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Hk4xp9Fb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wrlnjx443k45f1y0j7nt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Hk4xp9Fb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wrlnjx443k45f1y0j7nt.png" alt="Slack Blockquote" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While lists and blockquotes are excellent tools for enhancing readability, they should be used thoughtfully. Too many lists can confuse readers, and excessive blockquoting might dilute the emphasis you’re trying to create.&lt;/p&gt;

&lt;h2&gt;
  
  
  Special formatting: elevating your Slack messages
&lt;/h2&gt;

&lt;p&gt;There are a few other options with mrkdwn that you might need to use.&lt;/p&gt;

&lt;p&gt;First, displaying dates in a consistent, readable manner with date formatting. Here, you use the timestamp in UNIX format with the syntax &lt;code&gt;&amp;lt;!date^timestamp^{date_format}|{fallback_text}&amp;gt;&lt;/code&gt;, like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Today is: &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;!date^1697250654^{date_short}|Oct 13, 2023&amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;!date^1697250654^{date_short_pretty}|Oct 13, 2023&amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;!date^1697250654^{date_long}|Oct 13, 2023&amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;!date^1697250654^{date_long_pretty}|Oct 13, 2023&amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;!date^1697250654^{date_num}|Oct 13, 2023&amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;!date^1697250654^{date}|Oct 13, 2023&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This renders as:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--N-bqNjch--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vpa7w9yafyobz1e0dbhi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--N-bqNjch--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vpa7w9yafyobz1e0dbhi.png" alt="Slack Time" width="800" height="735"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tip: In JavaScript or TypeScript, use &lt;code&gt;const unixTimestamp = Math.floor(Date.now() / 1000)&lt;/code&gt; to get the current date in UNIX format.&lt;/p&gt;

&lt;p&gt;You can also combine some of the mrkdwn above. For example, to bold a link, you can use:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Here is a *bolded* link to &amp;lt;https://knock.app|*Knock*&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Which will render a bolded link:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9g-EyX98--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4deyfprsk6ee8gfwwpk6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9g-EyX98--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4deyfprsk6ee8gfwwpk6.png" alt="Slack Bold Link" width="800" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And, last but definitely not least, you have emojis:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:earth_americas: :rocket: :star: :alien: :moon: :sun_with_face: :zap: :fire: :cloud: :umbrella: :rainbow: :cat: :dog: :mouse: :elephant: :tiger: :rooster: :snake:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This gives us:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oLkx1j2I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4je0bb53g5g2x9ywdc30.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oLkx1j2I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4je0bb53g5g2x9ywdc30.png" alt="Slack Emoji" width="800" height="171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Remember, you can never have enough emojis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together: A mrkdwn alert
&lt;/h2&gt;

&lt;p&gt;Let’s put all this together into an alert that an application monitoring platform might send through Slack. Some elements we might use are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Notifying a specific individual or team&lt;/strong&gt;. The application might get this information from a database of SREs or on-call individuals. Here we will use &lt;code&gt;@-mentions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A concise description of the alert&lt;/strong&gt;. Here we will use basic formatting to highlight the issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Links to dashboards and pertinent information&lt;/strong&gt;. So the team can go deeper into the issue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A list of steps to be taken&lt;/strong&gt;. These can be predefined and added to the Block Kit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date and time information&lt;/strong&gt;. Then anyone seeing the alert will know when the alert happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emojis&lt;/strong&gt;. This can add visual appeal to the alert and draw attention to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Altogether, the mrkdwn for this alert would be:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@colin &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; @sres &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt; *&amp;lt;!date^1697250654^{date_num}|Oct 13, 2023&amp;gt;* &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt; *[APM ALERT]: Elevated Error Rates Detected!* :red_circle: &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; - Service: `User Authentication API` &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; - Environment: `Production` &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt; :chart_with_upwards_trend: Metrics &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt; *Error Rate*: _15%_ (Threshold: _5%_) &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; *Response Time*: _900ms_ (Average: _200ms_) &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; *Throughput*: _1200 rpm_ (Requests Per Minute) &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; :world_map: Geographical Impact: _US East, Europe Central_ &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; :sos: Related Incidents: &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;https://example.com|Database Latency Spike&amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;https://example.com|Redis Connection Failures&amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; :link: *Dashboard Link*: &amp;lt;https://example.com|APM Dashboard&amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; :speech_balloon: Recommended Actions: &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; 1. Verify backend database health and connection. &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; 2. Check Redis server and related metrics. &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; 3. Assess recent deployments for potential issues.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This code looks complicated, but most of this information will be added dynamically from the APM. This creates a well-formatted, clear message for the team:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IjM-hnnO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xbab9pa1r9c6534ig54n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IjM-hnnO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xbab9pa1r9c6534ig54n.png" alt="Slack Full Notification" width="800" height="1029"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Use mrkdwn for clear communication
&lt;/h2&gt;

&lt;p&gt;Slack’s mrkdwn is a powerful tool. Using mrkdwn enhances the readability and emphasis of critical pieces of information. The above example alert shows this, highlighting the crucial data for a team and making sure they can act on that information quickly.&lt;/p&gt;

&lt;p&gt;Integrating mrkdwn into your Slack notifications not only beautifies the message but, more importantly, makes it exponentially more user-friendly. When time is of the essence, and clarity is paramount, leveraging the full potential of mrkdwn ensures that your alerts are not just noticed but acted upon effectively.&lt;/p&gt;

&lt;p&gt;At Knock, we’ve taken mrkdwn further by integrating it into our &lt;a href="https://docs.knock.app/integrations/chat/slack/designing-slack-templates"&gt;templating engine&lt;/a&gt; and enhancing it with liquid tags for templating and an easy-to-use JSON editor. This allows you to design your Slack notifications exactly how you want them.&lt;/p&gt;

&lt;p&gt;Check out some &lt;a href="https://docs.knock.app/integrations/chat/slack/slack-examples"&gt;Slack notification examples&lt;/a&gt; for inspiration and sign up &lt;a href="https://dashboard.knock.app/signup"&gt;here&lt;/a&gt; to get started for free.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The top real-time notification services for building in-app notifications</title>
      <dc:creator>Colin White</dc:creator>
      <pubDate>Tue, 24 Oct 2023 20:11:29 +0000</pubDate>
      <link>https://dev.to/knocklabs/the-top-real-time-notification-services-for-building-in-app-notifications-olc</link>
      <guid>https://dev.to/knocklabs/the-top-real-time-notification-services-for-building-in-app-notifications-olc</guid>
      <description>&lt;p&gt;Real-time, in-app notifications in a product allow you to instantly engage users with relevant content, enhancing user experience and user retention. These notifications foster immediate action and feedback, ensuring users stay informed and connected to the application's latest updates and activities.&lt;/p&gt;

&lt;p&gt;But building in-app notifications from scratch is challenging. First, you have to manage the infrastructure for WebSockets and Publish/Subscribe messaging patterns to power the real-time behavior. Second, your product needs logic to deal with user preferences, user presence detection, storage, and logging. Finally, you need to build out the frontend UI features and underlying APIs to display the notifications.&lt;/p&gt;

&lt;p&gt;Using a SaaS service to offload some or all of this complexity allows you to concentrate on your own product while still giving the benefit of in-app notifications to your customers.&lt;/p&gt;

&lt;p&gt;Here, we will evaluate 4 providers to power the real-time infrastructure for your in-app notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key features of real-time services for in-app notifications
&lt;/h2&gt;

&lt;p&gt;What should you be looking for in a real-time service for in-app notifications?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pricing&lt;/strong&gt;: The cost-effectiveness of in-app notifications can significantly influence an app's overall budget and profitability. Transparent, scalable pricing ensures you can anticipate costs, allocate resources, and adjust any strategy based on budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: In-app notifications should adapt as user bases grow or shrink. A scalable notification system can handle spikes in traffic and vast volumes of concurrent users, ensuring every individual receives timely and consistent alerts without overburdening infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt;: A reliable in-app notification system guarantees that users receive messages without delays. This reliability is vital for maintaining trust, especially in applications where timely alerts can impact user decisions or safety.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework&lt;/strong&gt;: The chosen framework should support your desired communication mechanism, be it pub/sub for broadcasting messages to various subscribers, WebSockets for real-time bidirectional communication, or polling where the client periodically requests updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features&lt;/strong&gt;: Essential features to consider include presence (indicating who is online), low latency (ensuring rapid message delivery), the ability to query the state (understanding the current status of a user or system), and encryption (securing the content of the messages for privacy and protection).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer experience&lt;/strong&gt;: An intuitive and well-documented platform streamlines the integration and maintenance process for developers. A positive developer experience translates to quicker implementation, fewer bugs, and more time spent refining and enhancing the core features of the application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's look at a few services and how they align with these key features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pusher
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SbxGF-pL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wllg4041l802qmptl9gi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SbxGF-pL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wllg4041l802qmptl9gi.png" alt="Pusher" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pusher.com/channels"&gt;Pusher Channels&lt;/a&gt; is a real-time notifications service that empowers developers to swiftly integrate real-time functionality, like live notifications and chat, into web and mobile apps using WebSockets. Pusher is known for reliability and scalability and is used by Buffer, GitHub, and Datadog.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Managed WebSocket connections with fallback&lt;/li&gt;
&lt;li&gt;Support for Publish/Subscribe framework&lt;/li&gt;
&lt;li&gt;Presence detection&lt;/li&gt;
&lt;li&gt;The ability to query the API to find out channel and state information&lt;/li&gt;
&lt;li&gt;Interactivity through webhooks&lt;/li&gt;
&lt;li&gt;End-to-end encryption&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pros
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;. Pusher easily handles surges in traffic, ensuring consistent real-time communication for applications ranging from startups to large enterprises.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versatility&lt;/strong&gt;. With SDKs for various languages and platforms, Pusher seamlessly integrates with numerous tech stacks, simplifying real-time feature implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable WebSocket fallback&lt;/strong&gt;. Pusher's automatic fallback mechanisms guarantee continuous connectivity, even when WebSockets are unsupported or unstable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Location.&lt;/strong&gt; You are locked into a specific data center location when you sign up for Pusher and “it's not possible to change it afterwards.” This means that notification latency will slow further from the data center location.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing complexity&lt;/strong&gt;. With eight tiers and pricing changing with messages, connections, and support, it is not clear how you'll have to pay. You also have to pay separately for Pusher Beams, their Push notification service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pricing
&lt;/h3&gt;

&lt;p&gt;Pusher has &lt;a href="https://pusher.com/channels/pricing/"&gt;eight pricing tiers&lt;/a&gt; ranging from free on the “Sandbox” tier, where you get 200k messages per day and 100 concurrent connections, standard support and no monitoring, to $1,199/month on the “Growth Plus” tier for 90 million messages per day and 30,000 concurrent connections with premium support and monitoring. After that, you can contact the team for enterprise-level pricing.&lt;/p&gt;

&lt;h2&gt;
  
  
  PubNub
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nUtmm7AE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ok4wcgeqzxomk7y5as4f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nUtmm7AE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ok4wcgeqzxomk7y5as4f.png" alt="PubNub" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pubnub.com/"&gt;PubNub&lt;/a&gt; is a real-time communication platform based on HTTP long-polling instead of WebSockets. It provides scalable infrastructure to send and receive messages at low latency, and supports presence, compression, and storage. Customers include Niantic, Kustomer, and Autodesk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Uses the Publish/Subscribe framework to send high volumes of messages&lt;/li&gt;
&lt;li&gt;Low latency messaging&lt;/li&gt;
&lt;li&gt;GZip message compression&lt;/li&gt;
&lt;li&gt;Message persistence for history through their storage API&lt;/li&gt;
&lt;li&gt;Presence detection&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pros
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Broad integration options&lt;/strong&gt;. PubNub offers SDKs for numerous platforms and languages, making integration effortless across various development environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robust feature set&lt;/strong&gt;. Beyond just messaging, PubNub provides presence detection, storage, and playback, catering to diverse real-time app requirements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High scalability&lt;/strong&gt;. Built for large-scale applications, PubNub can handle millions of concurrent connections without compromising performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pricing concerns&lt;/strong&gt;. As message volumes or connections increase, costs can escalate due to usage-based pricing, making budgeting unpredictable for high-traffic applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lack of WebSockets&lt;/strong&gt;: PubNub uses the Publish/Subscribe model across HTTP rather than WebSockets. WebSockets have better scalability, better error handling, and lower latencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pricing
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.pubnub.com/pricing/"&gt;PubNub&lt;/a&gt; has a free tier giving users up to 200 MAUs or 1M total transactions per month. They are clear that this is for testing purposes only and should not be used for production. For production-level applications, they have a $49/month tier that allows up to 1000 MAUs and 3000 transactions per MAU. Usage-based billing is used beyond those limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ably
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mmu0PK69--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dfw9wpfmak2wikm09tdm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mmu0PK69--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dfw9wpfmak2wikm09tdm.png" alt="Ably" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ably.com/"&gt;Ably&lt;/a&gt; is a robust real-time data delivery platform based on WebSockets with features like message ordering, presence, and connection recovery. Customers include Toyota, HubSpot, and Verizon.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Low latencies of ~65ms for notification delivery&lt;/li&gt;
&lt;li&gt;Queryable state API to find popular channels and user presence&lt;/li&gt;
&lt;li&gt;Reduced bandwidth with delta compression&lt;/li&gt;
&lt;li&gt;Also supports MQTT and server-side events as well as WebSockets&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pros
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection State Recovery&lt;/strong&gt;: Ensures interrupted users can resume their session without missing data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Consistency&lt;/strong&gt;: Guaranteed message ordering and delivery ensure that all users receive updates in the correct sequence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge network&lt;/strong&gt;. Ably uses 15 data centers around the globe and persists data across each to lower latencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pricing Model&lt;/strong&gt;: With a pay-as-you-go model, costs can escalate quickly with little control if you add more users and channels.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pricing
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://ably.com/pricing"&gt;Ably pricing&lt;/a&gt; has a free tier offering 6M monthly messages across 200 concurrent channels and 200 concurrent connections. They then move to a pay-as-you-go model which charges $2.50 per million messages, $15.00 per thousand concurrent channels, and $15.00 per thousand concurrent connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="http://socket.io/"&gt;Socket.io&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5LuKIjvK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c5rt5fzy1nvapbwy3cfn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5LuKIjvK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c5rt5fzy1nvapbwy3cfn.png" alt="Socket.io" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://socket.io/"&gt;Socket.io&lt;/a&gt; is an open-source JavaScript library that facilitates real-time web communication. It simplifies bidirectional data transfer between web clients and servers. By abstracting WebSockets with fallbacks, it ensures consistent performance across devices and browsers, making real-time chats, notifications, and dynamic content more accessible for developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Automatic reconnection&lt;/li&gt;
&lt;li&gt;Middleware to filter and act on incoming packets&lt;/li&gt;
&lt;li&gt;Built-in support for binary data.&lt;/li&gt;
&lt;li&gt;Multiplexing through multiple namespaces&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pros
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ease of use&lt;/strong&gt;: Socket.io offers a simple API, enabling developers to quickly integrate real-time functionalities without deep expertise in WebSockets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform&lt;/strong&gt;: It provides client-side libraries for multiple platforms, ensuring broad compatibility across devices and browsers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback mechanisms&lt;/strong&gt;: If WebSockets aren't available, Socket.io automatically falls back to other methods, ensuring consistent connectivity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalability concerns&lt;/strong&gt;: For very large-scale applications, Socket.io might face challenges, requiring additional configurations or optimizations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosting&lt;/strong&gt;: As Socket.io is only a library, you have to set up the infrastructure yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pricing
&lt;/h3&gt;

&lt;p&gt;N/A. Socket.io is an open-source library that allows you to build real-time communication more easily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the right solution
&lt;/h2&gt;

&lt;p&gt;The above options are great choices for providing the backend infrastructure for real-time notifications. But if you want to use these real-time services as the foundation for in-app notifications, then you’ll have to think about what needs to be built around them. These options don’t feature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UI components&lt;/strong&gt;. While these services provide the backend infrastructure for notifications, they don't typically offer ready-to-use frontend components, meaning you'd need to design and implement the visual aspects of your notifications separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Templated messaging&lt;/strong&gt;. You may need to build or integrate a system that allows for templated messages, letting you send consistent yet customizable notifications based on predefined templates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read receipts&lt;/strong&gt;. Knowing if and when a user has seen a message can be vital for engagement and user experience. These platforms might not offer out-of-the-box solutions for read receipts, necessitating additional development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactivity&lt;/strong&gt;. These services deliver messages, but enabling interactive elements (e.g., buttons or links within notifications) would require additional development and design efforts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User preferences&lt;/strong&gt;. Tailoring notifications to user preferences, such as frequency, type, or topic, is crucial for a positive user experience. Incorporating such customization would need a separate mechanism or system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these will have to be built out for a complete in-app notification system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Introducing notification infrastructure
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://knock.app/"&gt;Knock&lt;/a&gt; is a developer tool for building cross-channel notification systems. You can use it to add in-app notifications to your app with pre-built components to get you started quickly, but the power comes from integrating in-app and out-of-app channels such as email, push, SMS, and Slack. You can build workflows for these notifications so the right messages are sent via the right channels to the right users at the right time. We built Knock to be the best option for teams needing to implement any type of notification in their application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--X5R1y5M7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3p4aqy2do775zesfia3e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--X5R1y5M7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3p4aqy2do775zesfia3e.png" alt="Knock" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Knock also takes care of all of your retry and delivery logic, as well as &lt;a href="https://docs.knock.app/designing-workflows/batch-function"&gt;batching&lt;/a&gt; (collapsing multiple notifications about a single topic into one), managing &lt;a href="https://knock.app/features/preferences"&gt;per-user notification preferences&lt;/a&gt;, and providing &lt;a href="https://knock.app/features/observability"&gt;unparalleled visibility into the notifications your product is sending&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://knock.app/"&gt;Knock&lt;/a&gt; is an all-in-one notification platform that allows developers to build stateful, in-app notification experiences like floating feeds, inboxes, toasts and banners. Customers include &lt;a href="https://knock.app/customers/vercel"&gt;Vercel&lt;/a&gt;, &lt;a href="https://knock.app/customers/amplitude"&gt;Amplitude&lt;/a&gt;, and &lt;a href="https://knock.app/customers/darwin-homes"&gt;Darwin Homes&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pre-built components to power in-app notifications that can be customized to match your brand&lt;/li&gt;
&lt;li&gt;No infrastructure setup, no APIs to build&lt;/li&gt;
&lt;li&gt;Workflow builder to determine how to alert users&lt;/li&gt;
&lt;li&gt;Visual template builder to create and update notification templates&lt;/li&gt;
&lt;li&gt;Preferences API to let users take control of their notifications&lt;/li&gt;
&lt;li&gt;Strong support with responses in hours&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd like to try it out, you can &lt;a href="https://dashboard.knock.app/signup"&gt;sign up for a free account&lt;/a&gt; and get started by checking out our &lt;a href="https://docs.knock.app/in-app-ui/overview"&gt;in-app feed docs&lt;/a&gt;. 👋&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
