<?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: Suvin Nimnaka</title>
    <description>The latest articles on DEV Community by Suvin Nimnaka (@suvink).</description>
    <link>https://dev.to/suvink</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F288828%2F6b7f598c-a9ea-4ef7-aaa1-72dbfeb51909.jpeg</url>
      <title>DEV Community: Suvin Nimnaka</title>
      <link>https://dev.to/suvink</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/suvink"/>
    <language>en</language>
    <item>
      <title>Why a Developer Portal Is Not an Internal Developer Platform</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Fri, 30 Oct 2026 08:22:39 +0000</pubDate>
      <link>https://dev.to/suvink/why-a-developer-portal-is-not-an-internal-developer-platform-17nd</link>
      <guid>https://dev.to/suvink/why-a-developer-portal-is-not-an-internal-developer-platform-17nd</guid>
      <description>&lt;p&gt;Imagine a developer joining a new team. On the first day, they're told that the team runs its applications on Kubernetes, uses GitHub for source code, has CI/CD pipelines for deployments, and uses a few different tools for monitoring and security. None of this is unusual. In fact, this is probably a fairly normal setup for a modern engineering team.&lt;/p&gt;

&lt;p&gt;Then the developer gets their first task: create a new service.&lt;/p&gt;

&lt;p&gt;That simple request suddenly opens a much bigger list of questions. Where should the repository be created? How should the deployment pipeline be configured? Which Kubernetes resources are needed? How does the service get its environment variables and secrets? Who creates the database? Which monitoring dashboard should be used? What security rules need to be followed?&lt;/p&gt;

&lt;p&gt;The developer can spend a few days figuring all of this out, or they can ask the platform team for help. If every new developer and every new service follows the same path, the platform team can quickly turn into a help desk answering the same set of questions over and over again.&lt;/p&gt;

&lt;p&gt;This is one of the problems that platform engineering tries to solve. Instead of asking every development team to understand all the infrastructure and tools underneath their applications, organizations can build a set of reusable capabilities and give developers a simpler way to use them.&lt;/p&gt;

&lt;p&gt;This is where two terms often appear: &lt;strong&gt;Internal Developer Platform (IDP)&lt;/strong&gt; and &lt;strong&gt;Internal Developer Portal (IDP)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F52w6hrcr8et6tkn8nnia.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F52w6hrcr8et6tkn8nnia.gif" alt="IDP vs IDP" width="400" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They're closely related, but they are not the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Platform Behind the Developer Experience
&lt;/h2&gt;

&lt;p&gt;An Internal Developer Platform is the collection of technologies, automation, workflows, and services that make it easier for developers to build, deploy, and operate applications.&lt;/p&gt;

&lt;p&gt;It can bring together infrastructure provisioning, CI/CD, Kubernetes, cloud resources, secrets, security policies, observability, and other engineering capabilities. The point isn't just to put these tools in one place. The platform should hide unnecessary complexity and provide developers with reliable ways to perform common tasks.&lt;/p&gt;

&lt;p&gt;For example, a platform team might create a standard way to deploy a web service. Instead of every development team writing its own Kubernetes configuration and deployment pipeline, the platform provides a reusable workflow. The developer supplies the information that matters to the application, while the platform takes care of the underlying details.&lt;/p&gt;

&lt;p&gt;This is where the idea of a &lt;strong&gt;golden path&lt;/strong&gt; comes in.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftlt5pwc1kgauoux1ek4k.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftlt5pwc1kgauoux1ek4k.png" alt="Golden Paths" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A golden path is a recommended way of doing something. It doesn't mean that developers are forced to use only one technology or one approach. It means that if they want the path that is already supported, secure, and maintained by the platform team, they have an easy way to follow it.&lt;/p&gt;

&lt;p&gt;This distinction is important because an Internal Developer Platform is not simply a collection of DevOps tools. Kubernetes, GitHub Actions, Terraform, Argo CD, Prometheus, and other tools can all be part of a platform, but putting them next to each other doesn't automatically create one.&lt;/p&gt;

&lt;p&gt;The value comes from connecting them into useful workflows.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://tag-app-delivery.cncf.io/whitepapers/platforms/" rel="noopener noreferrer"&gt;CNCF Platforms White Paper&lt;/a&gt; describes platforms in a similar way: they bring together capabilities and provide an interface that reduces the amount of infrastructure complexity developers need to deal with. &lt;a href="https://humanitec.com/blog/what-is-an-internal-developer-platform" rel="noopener noreferrer"&gt;Humanitec&lt;/a&gt; frames it as a set of capabilities that creates a self-service experience for developers and supports the organization's preferred ways of building and delivering software.&lt;/p&gt;

&lt;p&gt;That's half the picture. The other half is still a question: &lt;strong&gt;How does a developer actually find and use all these capabilities?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The front door: Internal Developer Portals
&lt;/h2&gt;

&lt;p&gt;Go back to our developer who needs to create a new service. Suppose the organization has already built a good internal platform. There are templates for new services, automated deployment workflows, documentation, monitoring integrations, and all the required infrastructure automation.&lt;/p&gt;

&lt;p&gt;That sounds great.&lt;/p&gt;

&lt;p&gt;Now imagine the developer has to search through Git repositories, internal documentation, Slack messages, and several different tools to find all of it.&lt;/p&gt;

&lt;p&gt;The platform may be excellent, but the developer experience is not.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;Internal Developer Portal&lt;/strong&gt; helps solve this problem by giving developers a central place to discover and interact with the software and capabilities available to them.&lt;/p&gt;

&lt;p&gt;One of the best-known examples is &lt;a href="https://backstage.io/docs/overview/what-is-backstage/" rel="noopener noreferrer"&gt;Backstage&lt;/a&gt;, the open-source developer portal framework created at Spotify. Backstage provides a software catalog where teams can keep information about their services, documentation, ownership, and dependencies. It also provides templates that can help developers create new software using the organization's preferred patterns.&lt;/p&gt;

&lt;p&gt;This matters because, as an engineering organization grows, knowing &lt;em&gt;what exists&lt;/em&gt; can become almost as difficult as building it.&lt;/p&gt;

&lt;p&gt;A developer may know that a payment service exists, but who owns it? Where is its source code? What does it depend on? Where is its documentation? Which team should they contact if something breaks?&lt;/p&gt;

&lt;p&gt;A good developer portal answers these questions without requiring the developer to play Sherlock Holmes.&lt;/p&gt;

&lt;p&gt;Spotify's experience with Backstage is quite interesting because it started as an internal tool and grew into an open-source project used by many organizations. Spotify has written about this journey in &lt;a href="https://engineering.atspotify.com/2025/4/celebrating-five-years-of-backstage" rel="noopener noreferrer"&gt;Celebrating Five Years of Backstage&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Commercial products such as &lt;a href="https://www.port.io/blog/the-four-pillars-of-internal-developer-portals" rel="noopener noreferrer"&gt;Port&lt;/a&gt; and &lt;a href="https://www.cortex.io/" rel="noopener noreferrer"&gt;Cortex&lt;/a&gt; take a similar idea and add capabilities around service catalogs, engineering standards, scorecards, and self-service workflows.&lt;/p&gt;

&lt;p&gt;But calling a portal a "directory" would undersell what modern portals can do. A developer portal can also be the place where a developer starts an action. It might allow them to create a service, request an environment, trigger a deployment, or perform another operational task.&lt;/p&gt;

&lt;p&gt;This is where the line between the portal and the platform starts to become less obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  So where does the platform end and the portal begin?
&lt;/h2&gt;

&lt;p&gt;Let's return to that new service.&lt;/p&gt;

&lt;p&gt;The developer opens the internal portal and selects &lt;strong&gt;Create a new component&lt;/strong&gt;. They choose a service template, enter the name of the application, connect the source code repository, and submit the request.&lt;/p&gt;

&lt;p&gt;The portal itself doesn't need to know how Kubernetes works. It doesn't need to know which API should be called or how the deployment pipeline should be configured. Instead, it passes the request to the underlying platform capabilities.&lt;/p&gt;

&lt;p&gt;The platform then does the hard work with it's control plane acting as its brain.&lt;/p&gt;

&lt;p&gt;The control plane coordinates the underlying tools and capabilities through a platform contract, enforcing governance. It knows how the different pieces of the platform need to work together and based on the request, it manages the necessary steps behind the scenes. It can create the Kubernetes manifests, configure the required resources, set up the deployment workflow, apply security policies, and connect the application to monitoring and other services.&lt;/p&gt;

&lt;p&gt;From the developer's point of view, this might feel like one simple action. Behind that button, however, there could be several systems working together.&lt;/p&gt;

&lt;p&gt;That is the key difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The portal is the place where developers discover and interact with the platform. The platform is the set of capabilities that actually enables the work.&lt;/strong&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftd6h9osqxfgpjvcth8c4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftd6h9osqxfgpjvcth8c4.png" alt="The Portal is just the tip of the iceberg" width="675" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The distinction isn't always a clean technical boundary. A portal can contain automation. A platform can expose APIs and command-line tools. Some products combine both ideas. What matters more is understanding the roles they play in the developer experience.&lt;/p&gt;

&lt;p&gt;This is also where a lot of the confusion comes from. Backstage's Software Templates can run real actions: create a repository, open a pull request, etc. It's tempting to keep adding steps until the template is your deployment process. At that point you've built a platform, just a thin one that lives inside your portal's template files. That holds up until a second interface, like a CLI, a CI job, or an AI agent, needs the same capability and has no way to reach it.&lt;/p&gt;

&lt;p&gt;It's also why you can have an Internal Developer Platform without a traditional portal. Developers could interact with the platform through APIs, CLIs, MCP (Model Context Protocol), or other interfaces. In the same way, it's possible to have a developer portal that provides a catalog and documentation without having a strong platform underneath it.&lt;/p&gt;

&lt;p&gt;The two become much more useful when they work together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does this look like in a real platform?
&lt;/h2&gt;

&lt;p&gt;Open-source projects such as &lt;a href="https://openchoreo.dev/" rel="noopener noreferrer"&gt;OpenChoreo&lt;/a&gt; provide an interesting example of the platform side of this story.&lt;/p&gt;

&lt;p&gt;OpenChoreo is an open-source Internal Developer Platform designed to simplify application development and deployment on Kubernetes. Instead of exposing every Kubernetes detail to application developers, it provides higher-level concepts and workflows that allow platform teams to define how applications should be built and deployed.&lt;/p&gt;

&lt;p&gt;This is an important idea in platform engineering.&lt;/p&gt;

&lt;p&gt;Developers don't necessarily need less technology. They need less &lt;strong&gt;unnecessary complexity&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Kubernetes is powerful. So is a commercial aircraft. Neither means every passenger should have to understand how the engine works. Most application developers do not want to think about every Kubernetes resource required to deploy a service. They want to build the service and have a reliable way to run it. &lt;/p&gt;

&lt;p&gt;OpenChoreo approaches this by providing platform abstractions, reusable workflows, deployment capabilities, GitOps, and observability. Platform engineers can define the capabilities and rules of the platform, while application developers work with simpler application-level concepts.&lt;/p&gt;

&lt;p&gt;OpenChoreo also includes a developer portal experience built around Backstage. This creates a useful example of how the two ideas can come together: the portal provides the developer-facing experience, while the underlying platform provides the capabilities and automation.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://openchoreo.dev/docs/overview/architecture/" rel="noopener noreferrer"&gt;OpenChoreo architecture documentation&lt;/a&gt; describes how its different platform components work together. At the heart of this architecture is the control plane, which acts as the brain of the platform. It brings the underlying tools and capabilities together, providing a consistent way to enforce governance and manage how applications are built, deployed, and operated. &lt;br&gt;
In addition to the control plane, OpenChoreo separates the platform into multiple planes, each with a distinct role. The experience plane provides the interaction interface (for both humans and agents), the data plane runs application workloads, the workflow plane handles build and deployment workflows, and the observability plane collects logs, metrics, and traces. The control plane coordinates the other planes, ensuring their capabilities work together as a cohesive platform while each plane focuses on its specific role.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feu8hqt9hnzc5lwallfom.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feu8hqt9hnzc5lwallfom.png" alt="OpenChoreo multi-plane architecture" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking at examples like this makes one thing clear. An Internal Developer Platform doesn't have to be a single tool. It can be a system made up of several connected capabilities, with clear interfaces between them.&lt;/p&gt;

&lt;p&gt;And the portal doesn't have to replace those capabilities. It gives developers a simpler way to consume them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does the distinction matter?
&lt;/h2&gt;

&lt;p&gt;It may be tempting to say that this is all just terminology. After all, developers do not really care whether something is called a portal or a platform. They care whether they can get their work done without spending three hours searching for the right configuration file.&lt;/p&gt;

&lt;p&gt;True, but the distinction becomes useful when deciding what to build.&lt;/p&gt;

&lt;p&gt;Imagine a company where developers constantly ask, "Who owns this service?", "Where is the documentation?", or "Which repository contains this application?" The immediate problem is discoverability. A software catalog and developer portal could make a big difference.&lt;/p&gt;

&lt;p&gt;Now imagine another company where developers know exactly where everything is, but creating a new environment takes three days and every team has a different deployment process. In that case, adding another portal page probably will not solve the real problem. The organization needs better platform automation.&lt;/p&gt;

&lt;p&gt;In many organizations, the answer is both.&lt;/p&gt;

&lt;p&gt;The portal becomes the front door, while the platform provides what is behind the door.&lt;/p&gt;

&lt;p&gt;This also explains why simply installing a developer portal doesn't automatically create an Internal Developer Platform. You can have a beautiful catalog with hundreds of services listed in it and still require developers to open tickets every time they need an environment.&lt;/p&gt;

&lt;p&gt;That is a very nice catalog. It is not necessarily a platform.&lt;/p&gt;

&lt;p&gt;The reverse is also true. An organization can have excellent platform automation but make developers interact with it through a confusing collection of scripts, repositories, and internal documentation.&lt;/p&gt;

&lt;p&gt;The machinery works, but nobody knows where the buttons are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the right thing
&lt;/h2&gt;

&lt;p&gt;Platform engineering usually goes better when you don't start with the question, "Which platform should we build?"&lt;/p&gt;

&lt;p&gt;Start with a developer problem.&lt;/p&gt;

&lt;p&gt;Maybe creating a new service takes too long. Maybe developers spend too much time configuring deployment pipelines. Maybe service ownership is difficult to discover. Maybe every team has invented its own way of deploying applications.&lt;/p&gt;

&lt;p&gt;Pick one of these problems and solve it well.&lt;/p&gt;

&lt;p&gt;A good platform team can then turn the solution into a reusable capability. A good portal can make that capability easy to discover and use. Over time, more workflows can be added as new problems appear.&lt;/p&gt;

&lt;p&gt;This is also why platform engineering should be treated as an ongoing product rather than a one-time infrastructure project. Developers are the users of the platform, and their feedback should shape what the platform provides.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://tag-app-delivery.cncf.io/whitepapers/platform-eng-maturity-model/" rel="noopener noreferrer"&gt;CNCF Platform Engineering Maturity Model&lt;/a&gt; is useful here because it looks at platform engineering as something that evolves as organizations mature.&lt;/p&gt;

&lt;p&gt;The goal is to remove the right complexity, not to build the biggest platform or the most impressive portal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The difference, in short
&lt;/h2&gt;

&lt;p&gt;After all the architecture diagrams, tools, portals, Kubernetes clusters, templates, and very serious platform-engineering discussions, the difference can be reduced to something quite simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An Internal Developer Platform provides the capabilities and automation developers need to build and run software, while an Internal Developer Portal gives them a simple place to discover and use those capabilities.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The two are not competitors. A portal without useful platform capabilities can become a nice-looking directory. A platform without a good developer experience can become another complicated system developers have to learn.&lt;/p&gt;

&lt;p&gt;When they work together, something much better happens.&lt;/p&gt;

&lt;p&gt;When the two work together, a developer can open one place, pick the supported path, get what they need, and go back to writing code. Tht might be the real test of a developer platform: how little time developers spend thinking about the platform itself.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>idp</category>
      <category>developerportal</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>One More Mile - Bringing Joy to Runners with Twilio</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:16:14 +0000</pubDate>
      <link>https://dev.to/suvink/one-more-mile-bringing-joy-to-runners-with-twilio-4fee</link>
      <guid>https://dev.to/suvink/one-more-mile-bringing-joy-to-runners-with-twilio-4fee</guid>
      <description>&lt;p&gt;If you've ever run a long race, you probably know that there are parts where things get a little difficult. Your legs hurt. You are tired. You start questioning your life choices and wondering why you voluntarily paid money to do this.&lt;/p&gt;

&lt;p&gt;And then you hear someone cheering for you.&lt;/p&gt;

&lt;p&gt;It could be your mum, your partner, a friend, or anyone else you care about. Just hearing a familiar voice can give you that little push to keep going.&lt;/p&gt;

&lt;p&gt;The problem is that your favourite people are usually not running alongside you. I've felt this myself during runs in Colombo. There are parts of some city races where you are running between buildings, with hardly anyone around to cheer. You have thousands of runners around you, but somehow it can still feel pretty lonely.&lt;/p&gt;

&lt;p&gt;That got me thinking. Most runners already have their phones with them, and a lot of them run with headphones. So why not use the phone to bring some of that support to them?&lt;/p&gt;

&lt;p&gt;The idea was simple. Friends and family could record short voice messages for a runner. During the race, the runner could open a link and listen to those messages.&lt;/p&gt;

&lt;p&gt;Then I started thinking about how to build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first idea
&lt;/h2&gt;

&lt;p&gt;My first thought was a phone hotline.&lt;/p&gt;

&lt;p&gt;A supporter could call a number, enter the runner's bib number, record a message, and hang up. The runner could then call another number and listen to the messages.&lt;/p&gt;

&lt;p&gt;Twilio seemed perfect for this. It already handles phone calls, recordings, menus and pretty much everything else you need for a voice application.&lt;/p&gt;

&lt;p&gt;There was just one problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I hit a wall
&lt;/h2&gt;

&lt;p&gt;Twilio phone numbers aren't available in Sri Lanka. That made the phone hotline idea pretty useless.&lt;/p&gt;

&lt;p&gt;I could have used an international number, but then supporters would have to pay international call charges just to leave a 20 second message. The runner would also have to spend phone credit listening to those messages during the race. Like running is not already expensive :D &lt;/p&gt;

&lt;p&gt;That didn't make much sense.&lt;/p&gt;

&lt;p&gt;So I started looking at the problem again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mundane workaround
&lt;/h2&gt;

&lt;p&gt;I realized I was too focused on the phone number.&lt;/p&gt;

&lt;p&gt;The number wasn't really what I needed. I just needed a way to send audio from a supporter to a runner, with the runner's bib number connecting the two.&lt;/p&gt;

&lt;p&gt;And a browser can handle audio.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.twilio.com/docs/voice/sdks" rel="noopener noreferrer"&gt;Twilio's Voice SDK&lt;/a&gt; supports WebRTC, which means a web page can connect to Twilio and behave much like a phone call, without actually using a phone number.&lt;/p&gt;

&lt;p&gt;That was enough.&lt;/p&gt;

&lt;p&gt;Instead of asking someone to call a number, I could just give them a link.&lt;/p&gt;

&lt;p&gt;The supporter opens the link, enters the bib number, records a message and sends it. The runner gets their own private link and can listen to everything.&lt;/p&gt;

&lt;p&gt;No app. No phone number. No international call charges. &lt;/p&gt;

&lt;p&gt;I called it &lt;strong&gt;One More Mile&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leaving a message
&lt;/h2&gt;

&lt;p&gt;A supporter opens the page and enters the runner's bib number.&lt;/p&gt;

&lt;p&gt;The app checks it and shows the runner's name. This is a small thing, but it helps avoid a surprisingly easy mistake. You never know if it's 1234 or 2444 (One 2, three 4 in case you didn't get it :p).&lt;/p&gt;

&lt;p&gt;Once the runner is confirmed, the supporter enters their own name and starts recording.&lt;/p&gt;

&lt;p&gt;They can say whatever they want. Well, not just anything. Only the good things.&lt;/p&gt;

&lt;p&gt;It might be a serious motivational message. It might be "Come on, you've got this!" Or it might be a (good?) friend reminding them that therapy is much cheaper than running.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F298dwum2x9h9qwa5rdui.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F298dwum2x9h9qwa5rdui.png" alt="Good Friends I Guess" width="800" height="918"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When they're done, they hit stop and the message is saved.&lt;/p&gt;

&lt;p&gt;There is a small trick happening here.&lt;/p&gt;

&lt;p&gt;With a normal phone call, you can press the &lt;code&gt;#&lt;/code&gt; key to tell Twilio that the recording is finished. Obviously, a browser doesn't have a phone keypad. So the stop button sends the same signal through the WebRTC connection. Twilio receives it, stops the recording and stores it.&lt;/p&gt;

&lt;p&gt;The person recording doesn't need to know any of that.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Listening to the messages
&lt;/h2&gt;

&lt;p&gt;The runner gets a private link, usually through a QR code or a message from the event organizer.&lt;/p&gt;

&lt;p&gt;When they open it, they see one big play button.&lt;/p&gt;

&lt;p&gt;That's basically the whole interface.&lt;/p&gt;

&lt;p&gt;At kilometer 32, nobody wants to read instructions or figure out which menu to open. You just want to press something and hear someone you know.&lt;/p&gt;

&lt;p&gt;The messages play one after another, with the sender's name announced before each one. Sometimes, just hearing the sender's name is enough to brighten your day, if you know what I mean. :wink&lt;/p&gt;

&lt;p&gt;If there aren't any messages yet, the app tells the runner to keep going anyway. Hopefully someone remembers to send one soon.&lt;/p&gt;

&lt;p&gt;The listening link also contains a secret token, so the messages aren't publicly accessible. If someone opens the page without a valid token, they just get an invalid-link screen.&lt;/p&gt;

&lt;p&gt;Nobody else gets to listen to your race-day fan mail.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What Twilio is actually doing
&lt;/h2&gt;

&lt;p&gt;Once I had the basic idea working, here is how I designed all the features around Twilio.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb9q53mx4g83l3lhma81o.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb9q53mx4g83l3lhma81o.png" alt="One More Mile Architecture" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;a href="https://www.twilio.com/docs/voice/sdks" rel="noopener noreferrer"&gt;Voice SDK&lt;/a&gt;&lt;/strong&gt; connects the browser to Twilio using WebRTC.&lt;/p&gt;

&lt;p&gt;A small token function gives the browser a short-lived token so it can make the connection. Think of it as a wristband at a concert.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;a href="https://www.twilio.com/docs/voice/twiml" rel="noopener noreferrer"&gt;TwiML App&lt;/a&gt;&lt;/strong&gt; tells Twilio where to send the call.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;a href="https://www.twilio.com/en-us/serverless/functions" rel="noopener noreferrer"&gt;Serverless Functions&lt;/a&gt;&lt;/strong&gt; handle the calls and return TwiML instructions such as "start recording" or "play this recording." &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.twilio.com/docs/sync" rel="noopener noreferrer"&gt;Twilio Sync&lt;/a&gt;&lt;/strong&gt; stores the runner roster and keeps track of the messages associated with each bib number.&lt;/p&gt;

&lt;p&gt;The actual audio is stored as recordings, which Twilio can then play back.&lt;/p&gt;

&lt;p&gt;The whole app also lives in Twilio. The web pages are served as &lt;a href="https://www.twilio.com/docs/serverless/functions-assets/assets" rel="noopener noreferrer"&gt;Twilio Assets&lt;/a&gt; alongside the Functions, so the frontend and backend can be deployed together.&lt;/p&gt;

&lt;p&gt;That means there isn't another server I need to worry about on race morning.&lt;/p&gt;

&lt;p&gt;One less thing to break at 5 am!&lt;/p&gt;

&lt;h2&gt;
  
  
  The race organizer's side
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1k6rozru7kpy30vv98y1.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1k6rozru7kpy30vv98y1.png" alt="Race Organizer's UI" width="800" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Someone still needs to tell the application who is actually running.&lt;/p&gt;

&lt;p&gt;For that, I built a small admin page where the organizer can upload a CSV containing the bib numbers and runner names.&lt;/p&gt;

&lt;p&gt;The page validates the file before replacing the existing roster.&lt;/p&gt;

&lt;p&gt;It also generates a private listening link for each runner, which the organizer can distribute before the race.&lt;/p&gt;

&lt;p&gt;And yes, there is a CSV template. Because whenever you give someone a CSV upload, the first question is almost always, "What columns do I need?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Want to try it yourself?
&lt;/h2&gt;

&lt;p&gt;If you have a Twilio account, just run:&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;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/Suvink/one-more-mile/main/init.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The script walks you through the setup and deploys the app for you. You only have to input the &lt;a href="https://help.twilio.com/articles/223136027-Auth-Tokens-and-How-to-Change-Them" rel="noopener noreferrer"&gt;Twilio Auth Token and account SID&lt;/a&gt;. Yes, it's a bit of a black box. But hey, at least it's a box that works!&lt;/p&gt;

&lt;p&gt;Now if you don't wanna put your blind faith in a total stranger, head over to the GitHub repo for the source code and setup instructions.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Suvink" rel="noopener noreferrer"&gt;
        Suvink
      &lt;/a&gt; / &lt;a href="https://github.com/Suvink/one-more-mile" rel="noopener noreferrer"&gt;
        one-more-mile
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;One More Mile&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Voice messages for marathon runners. Supporters enter a runner's bib number and
record a message in the browser. On race day, each runner opens a private link
and hears every message through their headphones.&lt;/p&gt;
&lt;p&gt;It runs entirely on Twilio, with no phone number needed. The browser places the
call over WebRTC using the Twilio Voice SDK, so it works in countries where
Twilio numbers aren't available.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Functions&lt;/strong&gt; (&lt;code&gt;functions/&lt;/code&gt;): access tokens, bib lookup, roster upload, and the TwiML for recording and playback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync&lt;/strong&gt; stores the roster and each runner's message list. Recordings stay in Twilio's recording storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web app&lt;/strong&gt; (&lt;code&gt;web/&lt;/code&gt;): a Vite + React PWA, served as Twilio Assets on the same domain&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;You need git, Node 18+, and your Twilio &lt;strong&gt;Account SID&lt;/strong&gt; and &lt;strong&gt;Auth Token&lt;/strong&gt; from
the &lt;a href="https://console.twilio.com" rel="nofollow noopener noreferrer"&gt;Twilio Console&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl -fsSL https://raw.githubusercontent.com/Suvink/one-more-mile/main/init.sh &lt;span class="pl-k"&gt;|&lt;/span&gt; bash&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;This clones the repo, asks for your…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Suvink/one-more-mile" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Where I could take it next
&lt;/h2&gt;

&lt;p&gt;The first version is intentionally simple, but there are a few things I'd like to add as I keep working on One More Mile.&lt;/p&gt;

&lt;p&gt;The first is moderation. Give people a microphone and eventually, someone is going to use it to say something they probably shouldn't. &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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4d2ob3t7l8l8p2wvf02z.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4d2ob3t7l8l8p2wvf02z.png" alt="A cat with a mic" width="532" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.twilio.com/en-us/products/conversational-ai/conversational-intelligence" rel="noopener noreferrer"&gt;Twilio's Conversational Intelligence&lt;/a&gt; could help here. Voice messages could be transcribed and analyzed before they reach the runner, allowing the application to flag or reject messages containing profanity or other unwanted content. That would be especially useful if this is being used for a large public race where hundreds or thousands of people might be sending messages.&lt;/p&gt;

&lt;p&gt;There is also room to make the experience more fun for supporters. Maybe they could choose a few quick prompts like "Give them a pep talk" or "Make them laugh" when they start recording. That could make it easier for someone who wants to send a message but has no idea what to say.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finish line
&lt;/h2&gt;

&lt;p&gt;The original phone hotline idea would have worked if Twilio phone numbers were available in Sri Lanka. The web version ended up being just as good anyways.&lt;/p&gt;

&lt;p&gt;It is easier to share, works directly from the runner's phone, doesn't require an app, and works without relying on local phone numbers.&lt;/p&gt;

&lt;p&gt;But the technical part is only half the reason I built it.&lt;/p&gt;

&lt;p&gt;The real reason is what happens during a long race when you are tired and the crowd disappears.&lt;/p&gt;

&lt;p&gt;You might hear your mum saying, "I'm so proud of you." You might hear your partner telling you that they're waiting at the finish line. Or you might hear your best friend saying something so stupid that you actually laugh when you are supposed to be conserving whatever energy you have left.&lt;/p&gt;

&lt;p&gt;For a moment, they feel like they're right there with you. And maybe that's enough to keep running.&lt;/p&gt;

&lt;p&gt;One more mile.&lt;/p&gt;

</description>
      <category>twilio</category>
      <category>marathon</category>
      <category>webrtc</category>
    </item>
    <item>
      <title>Don't become a victim of PII Harvesting</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Sat, 06 Dec 2025 14:11:31 +0000</pubDate>
      <link>https://dev.to/suvink/dont-become-a-victim-of-pii-harvesting-1gk9</link>
      <guid>https://dev.to/suvink/dont-become-a-victim-of-pii-harvesting-1gk9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Disclaimer:&lt;/em&gt;&lt;br&gt;
&lt;em&gt;This post is for educational and security awareness purposes only. The techniques described were performed as a proof-of-concept to demonstrate vulnerabilities in how personal information is shared on social media platforms. No data was collected, stored, or misused in any way. The goal is to help job seekers protect their personal information online. Please do not use these techniques for malicious purposes or to harvest data without consent. Unauthorized data collection may violate platform terms of service and applicable privacy laws.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;I was scrolling through LinkedIn earlier today when I came across a job posting that looked completely legitimate at first glance.&lt;/p&gt;

&lt;p&gt;Backend Developer position. Remote work. Open to freshers.&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%2F2xx1nq3flo2id2iwfi9w.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%2F2xx1nq3flo2id2iwfi9w.png" alt="Suspicious LinkedIn Post" width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I saw this line:&lt;/p&gt;

&lt;p&gt;"Comment your #email_ID and we'll send the assignment link within 24 hours."&lt;/p&gt;

&lt;p&gt;The post already had 35+ comments. People were dropping their personal email addresses in plain view.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Red flags
&lt;/h2&gt;

&lt;p&gt;I clicked on the poster's profile to learn more about the company and the recruiter's background.&lt;/p&gt;

&lt;p&gt;Their About section was blank. No company listed. Their post history showed random photos of "onboarding kits" from various tech companies, but nothing connecting them to an actual organization.&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%2Fasf444d291hi2t5avdqu.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%2Fasf444d291hi2t5avdqu.png" alt="Empty About Page" width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Meanwhile, real people were publicly exposing their contact information.&lt;/p&gt;

&lt;p&gt;Maybe it was a legitimate post. Maybe it wasn't. I had no solid proof either way. But something felt off. Empty profile, no company affiliation, dozens of people publicly exposing their emails in the comments.&lt;/p&gt;

&lt;p&gt;That's when a different question hit me. Even if this poster had good intentions, how easy would it be for someone else to harvest all these publicly visible emails?&lt;/p&gt;

&lt;p&gt;I decided to find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's write some code
&lt;/h2&gt;

&lt;p&gt;I wanted to understand the technical barrier to exploiting this kind of post. Turns out, it's almost non-existent.&lt;/p&gt;

&lt;p&gt;I opened ChatGPT and typed: "Write a Chrome extension that extracts email addresses from LinkedIn post comments."&lt;/p&gt;

&lt;p&gt;One minute later, I had working code.&lt;/p&gt;

&lt;p&gt;I didn't need to understand the LinkedIn DOM structure. I didn't need to write regex patterns for email validation. I didn't need to figure out Chrome extension manifests. The AI handled all of it.&lt;/p&gt;

&lt;p&gt;Copy, paste, load unpacked extension in Chrome. Done.&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%2Fsc6bssd6aazqwdzbnb2f.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%2Fsc6bssd6aazqwdzbnb2f.png" alt="Email Extractor Extension" width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I clicked on the malicious job post, opened the extension, hit "Extract." Within seconds, I had a clean list of 9 email addresses that people had publicly commented.&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%2Ffyhj2zvshl5c0gjqvxvu.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%2Ffyhj2zvshl5c0gjqvxvu.png" alt="Extracted Emails" width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No LinkedIn API access needed. No authentication bypass. No rate limiting to circumvent. No technical expertise required.&lt;/p&gt;

&lt;p&gt;The entire process, from idea to harvested emails, took under 3 minutes. With a little more time playing with the prompt, I would've gotten every single email extracted from the post.&lt;/p&gt;

&lt;p&gt;Now imagine this: multiple posts, multiple sock-puppet accounts, automated monitoring with Puppeteer or Selenium running on a cheap VPS. You're looking at thousands of emails harvested daily with basically zero effort or infrastructure cost.&lt;/p&gt;

&lt;p&gt;The scary part? I'm not even a security researcher. I'm just someone who knows how to use ChatGPT. And if I can do this in 3 minutes, imagine what someone with actual malicious intent can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens after emails are harvested
&lt;/h2&gt;

&lt;p&gt;Let me be very clear here. I collected nothing beyond the proof of concept. But bad actors absolutely do, and the downstream attacks are more sophisticated than most people realize. I'll explain a couple of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context-aware spear phishing
&lt;/h3&gt;

&lt;p&gt;When an attacker knows you commented on a backend developer posting, they can craft targeted messages:&lt;/p&gt;

&lt;p&gt;"Thanks for your interest in the Senior Backend Engineer role. We've reviewed your background and would like to move forward. Please access the technical assessment here: [malicious link]"&lt;/p&gt;

&lt;p&gt;This isn't generic spam. It references your actual behavior and interests. The click-through rate on context-aware phishing is 5-10x higher than traditional campaigns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Malicious payload delivery
&lt;/h3&gt;

&lt;p&gt;That "assignment link" the recruiter will be sending could resolve to many things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Phishing pages that clone Google Docs or GitHub's login interface&lt;/li&gt;
&lt;li&gt;ZIP archives containing RATs (Remote Access Trojans)&lt;/li&gt;
&lt;li&gt;Fake repositories with malicious dependencies buried in &lt;code&gt;package.json&lt;/code&gt; or &lt;code&gt;requirements.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers are especially vulnerable because our workflow involves downloading and executing code regularly. We're trained to &lt;code&gt;git clone&lt;/code&gt; and &lt;code&gt;npm install&lt;/code&gt; without suspicion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Long-term dataset enrichment
&lt;/h3&gt;

&lt;p&gt;Harvested emails get added to larger databases for future use. Your email from this LinkedIn comment can be correlated with many other information coming from various diffierent sources such as,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your GitHub activity (public repos, commit patterns, starred projects)&lt;/li&gt;
&lt;li&gt;Breach databases from previous incidents (haveibeenpwned, leaked credentials)&lt;/li&gt;
&lt;li&gt;OSINT from Twitter, personal blogs, conference talks&lt;/li&gt;
&lt;li&gt;Account enumeration across AWS, Azure, Digital Ocean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a persistent digital footprint. Attackers can revisit this data months or years later, long after you've forgotten about that one LinkedIn comment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automated account enumeration
&lt;/h3&gt;

&lt;p&gt;Once they have your email, attackers run automated checks to see where else you have accounts. Tools like &lt;code&gt;holehe&lt;/code&gt; or custom scripts can enumerate which cloud providers you use (AWS, GCP, Azure), your presence on GitHub, GitLab, Bitbucket, and developer tool accounts like npm, PyPI, or Docker Hub. Each confirmed account becomes another potential attack surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this work so well for the attackers?
&lt;/h2&gt;

&lt;p&gt;This works so well because LinkedIn's "professional environment" creates a false sense of security where people assume bad actors wouldn't operate where identities are theoretically verifiable. Job seekers, especially early-career developers, are economically vulnerable and when opportunity appears, critical evaluation goes out the window. The technical barrier for pulling off something like this is now close to zero. You don't need programming expertise anymore when you can ask an LLM to generate the code in 30 seconds. And it scales exponentially. One post harvests dozens of emails, ten posts hundreds, automated monitoring across multiple sock-puppet accounts captures thousands daily.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, how to protect yourself from being a victim?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Never post personal information publicly&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use LinkedIn's DM feature. Real recruiters understand this. If someone insists on public email drops, disengage immediately.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Validate the poster thoroughly&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before engaging with a recruitment post,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify company affiliation (check if the domain exists and has a careers page)&lt;/li&gt;
&lt;li&gt;Look for consistent employment history&lt;/li&gt;
&lt;li&gt;Check if actual employees from that company interact with the post&lt;/li&gt;
&lt;li&gt;Search the company's official job board&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Empty profiles + urgency = big red flags.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Use platform-provided channels&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most of the time, legitimate companies have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Branded career portals with proper TLS certificates (served over HTTPS)&lt;/li&gt;
&lt;li&gt;Corporate email domains (not free providers like Gmail)&lt;/li&gt;
&lt;li&gt;Application tracking systems (Greenhouse, Lever, Workday or LinkedIn Quick Apply)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If they're running hiring through comment sections, they're either fake or incompetent. Neither is worth your time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Compartmentalize your identity&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use dedicated email addresses for job applications. Services like SimpleLogin or AnonAddy let you create unique aliases per application. If one gets compromised, you know exactly where the leak happened, and you can kill that alias without affecting your main inbox.&lt;/p&gt;

&lt;p&gt;Think of it as the principle of least privilege, applied to your digital identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Look, I get it. We all want jobs. But posting your email in a public LinkedIn comment is like pulling down your pants in a crowded mall and yelling "I'm here for the interview!" Sure, maybe a recruiter sees you. But so does everyone else, including the guy in the corner taking notes for all the wrong reasons.&lt;/p&gt;

&lt;p&gt;This isn't about paranoia. LinkedIn remains valuable for networking and career growth. But recognize that modern social engineering doesn't require technical sophistication. Attackers just don't exploit software vulnerabilities, They exploit human psychology in professional contexts.&lt;/p&gt;

&lt;p&gt;Your email is PII. It can be a primary key in multiple databases. It's the recovery mechanism for most of your accounts. Treat it with the same caution you'd treat your home address or SSN.&lt;/p&gt;

&lt;p&gt;Next time you see "Drop your email in the comments," pause and evaluate. Is this legitimate, or is someone harvesting data?&lt;/p&gt;

&lt;p&gt;Your inbox security depends on that split-second decision. Stay skeptical. Verify everything. And keep your PII out of public spaces as much as you can.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Built a Twitter Bot So I Don't Have to Check Twitter Every 5 Minutes!</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Sun, 24 Aug 2025 06:02:43 +0000</pubDate>
      <link>https://dev.to/suvink/i-built-a-twitter-bot-so-i-dont-have-to-check-twitter-every-5-minutes-1a0g</link>
      <guid>https://dev.to/suvink/i-built-a-twitter-bot-so-i-dont-have-to-check-twitter-every-5-minutes-1a0g</guid>
      <description>&lt;p&gt;Picture this. You've built an amazing product. You're proud of it. Your mom's proud of it. Even your cat seems impressed (though cats are notoriously hard to please). But here's the thing: people are talking about your product on Twitter (well, “X” now, but let’s be real, we’re all still calling it Twitter), and you have absolutely no idea what they're saying.&lt;/p&gt;

&lt;p&gt;Sure, you could spend your entire day doom-scrolling through Twitter, searching for mentions of your product. But let's be honest. That's about as appealing as watching paint dry in slow motion. Plus, you've got actual work to do, like building more amazing features or figuring out why the coffee machine in the office keeps making that weird noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The solution?&lt;/strong&gt; Build a bot that watches Twitter for you and sends updates directly to your team's Google Chat. It's like having a really dedicated intern who never sleeps, never complains, and never eats your lunch from the office fridge.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;We're creating a Node.js service that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Monitors Twitter for tweets containing your product's hashtag.&lt;/li&gt;
&lt;li&gt;Forwards the juicy details to your Google Chat workspace.&lt;/li&gt;
&lt;li&gt;Runs once, exits, and repeats the same every 15 minutes.&lt;/li&gt;
&lt;li&gt;Does all this without you having to touch Twitter with a 10-foot pole.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prerequisites (The boring, necessary stuff)
&lt;/h2&gt;

&lt;p&gt;Before we dive into the fun part, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js installed (version 14 or higher – we're not animals here)&lt;/li&gt;
&lt;li&gt;A Twitter Developer Account&lt;/li&gt;
&lt;li&gt;A Google Chat workspace (or the ability to create webhooks in one)&lt;/li&gt;
&lt;li&gt;A Choreo Account. If you don't have one, sign up for free at &lt;a href="https://console.choreo.dev/signup" rel="noopener noreferrer"&gt;https://console.choreo.dev/signup&lt;/a&gt;. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Setting Up Your Twitter API Access
&lt;/h2&gt;

&lt;p&gt;First things first. You need to get cozy with the Twitter API. Don't worry, it's not as scary as it sounds.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Head over to &lt;a href="https://developer.x.com/" rel="noopener noreferrer"&gt;developer.twitter.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Create an app (call it something cool like "HashtagWatcher3000").&lt;/li&gt;
&lt;li&gt;Generate a Bearer Token.&lt;/li&gt;
&lt;li&gt;Keep that token safe.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2: Google Chat Webhook Setup
&lt;/h2&gt;

&lt;p&gt;Now let's set up the delivery mechanism – Google Chat webhooks.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your Google Chat space.&lt;/li&gt;
&lt;li&gt;Click on the space name → "Manage webhooks".&lt;/li&gt;
&lt;li&gt;Create a new webhook.&lt;/li&gt;
&lt;li&gt;Copy that URL and note it down somewhere.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: Project Setup (The Base)
&lt;/h2&gt;

&lt;p&gt;Create a new directory and initialize your 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;twitter-hashtag-monitor
&lt;span class="nb"&gt;cd &lt;/span&gt;twitter-hashtag-monitor
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 dependencies we'll need:&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;axios dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why these packages?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;axios&lt;/code&gt;: For making HTTP requests&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotenv&lt;/code&gt;: For managing environment variables (keeping secrets secret)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Environment Configuration
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file to store your sensitive information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TWITTER_BEARER_TOKEN=your_actual_bearer_token_here
GOOGLE_CHAT_WEBHOOK_URL=your_webhook_url_here
HASHTAG=#yourproducthashtag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And create a &lt;code&gt;.gitignore&lt;/code&gt; file just to make sure you don't accidentally commit your API keys.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules
.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Never, ever, EVER commit your &lt;code&gt;.env&lt;/code&gt; file to version control. That's like leaving your house key under the doormat with a note saying "free stuff inside."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: The Main Event – Building the Bot
&lt;/h2&gt;

&lt;p&gt;Now for the fun part! Create an &lt;code&gt;index.js&lt;/code&gt; 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="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;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="c1"&gt;// Configuration object (keeping configs organized)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;bearerToken&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;TWITTER_BEARER_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;googleChatWebhookUrl&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;GOOGLE_CHAT_WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hashtag&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;HASHTAG&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#choreoisawesome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Default hashtag&lt;/span&gt;
    &lt;span class="na"&gt;checkInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// minutes to look back&lt;/span&gt;
    &lt;span class="na"&gt;maxResults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;twitterServiceUrl&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://api.twitter.com/2/tweets/search/recent&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;
  
  
  The Tweet Search Function
&lt;/h3&gt;

&lt;p&gt;This function is the heart of our operation:&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;searchTweets&lt;/span&gt;&lt;span class="p"&gt;()&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;now&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;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// Twitter needs a 10-second buffer, so we use 20 to be safe&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endTime&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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;timeAgo&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkInterval&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hashtag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeAgo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="na"&gt;end_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;endTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="na"&gt;max_results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxResults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tweet.fields&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;created_at,author_id,public_metrics,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;user.fields&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;username,name&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;response&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="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;twitterServiceUrl&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;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bearerToken&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="nx"&gt;params&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error searching tweets:&lt;/span&gt;&lt;span class="dl"&gt;"&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;response&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&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;message&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="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;strong&gt;What's happening here?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We create a time window (last 15 minutes, but ending 20 seconds ago)&lt;/li&gt;
&lt;li&gt;We ask Twitter for tweets containing our hashtag in that timeframe&lt;/li&gt;
&lt;li&gt;We request additional fields like metrics and user info (because context matters)&lt;/li&gt;
&lt;li&gt;If something goes wrong, we gracefully return an empty array instead of crashing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To learn more about the search API, head over to the &lt;a href="https://docs.x.com/x-api/posts/search-recent-posts" rel="noopener noreferrer"&gt;Search Recent Posts API on X API Documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Google Chat Messenger
&lt;/h3&gt;

&lt;p&gt;This function takes our Tweet data and formats it nicely for Google Chat.&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;sendTweetsToService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tweets&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;tweets&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;===&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;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;No tweets found in the last&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkInterval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;minutes&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="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;tweetToSend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tweets&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="c1"&gt;// We only send the first tweet for now&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;`Found &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tweets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tweet(s), sending the first one to Google Chat`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Format message for Google Chat&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&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;`🐦 New Tweet Found!\n\n`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                &lt;span class="s2"&gt;`*Hashtag:* &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hashtag&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                &lt;span class="s2"&gt;`*Author:* &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tweetToSend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;tweetToSend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                &lt;span class="s2"&gt;`*Created:* &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tweetToSend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                &lt;span class="s2"&gt;`*Tweet:*\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tweetToSend&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;\n\n`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                &lt;span class="s2"&gt;`*Metrics:* ❤️ &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tweetToSend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_metrics&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;like_count&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&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;tweetToSend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_metrics&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;retweet_count&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;googleChatWebhookUrl&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="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="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&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="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;Successfully sent tweet to Google Chat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Mission accomplished – time to exit&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;Tweet sent successfully. Stopping the service...&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;exit&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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error sending tweet to Google Chat:&lt;/span&gt;&lt;span class="dl"&gt;"&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;response&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&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;message&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;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Instead of just sending a text message to Google Chat, you can send nicely formatted cards. Check the &lt;a href="https://developers.google.com/workspace/chat/api/reference/rest/v1/cards" rel="noopener noreferrer"&gt;Google Chat Cards Documentation&lt;/a&gt; for more information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tying It All Together
&lt;/h3&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;checkAndSendTweets&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\n[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;] Checking for tweets with hashtag: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hashtag&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;tweets&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;searchTweets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendTweetsToService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tweets&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startMonitoring&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;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;Starting Twitter Hashtag Monitor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;checkAndSendTweets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Run immediately&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Handle graceful shutdown (Ctrl+C)&lt;/span&gt;
&lt;span class="nx"&gt;process&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;SIGINT&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="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="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Shutting down Twitter Hashtag Monitor...&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;exit&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Start the service&lt;/span&gt;
&lt;span class="nf"&gt;startMonitoring&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 are too lazy to read the entire thing, here is the full source code: &lt;a href="https://github.com/Suvink/fetch-tweets-from-hashtag" rel="noopener noreferrer"&gt;https://github.com/Suvink/fetch-tweets-from-hashtag&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Package.json Scripts
&lt;/h2&gt;

&lt;p&gt;Add some convenient scripts to your &lt;code&gt;package.json&lt;/code&gt;:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"twitter-hashtag-monitor"&lt;/span&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="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Service to monitor Twitter hashtags and forward tweets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&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;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node index.js"&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;"dependencies"&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;"axios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.5.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dotenv"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^16.3.1"&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;"keywords"&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;"twitter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"monitoring"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hashtag"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tony Stark"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MIT"&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;h2&gt;
  
  
  Step 7: Testing Your Creation
&lt;/h2&gt;

&lt;p&gt;Time to see if your digital creation comes to life.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If everything is configured correctly, you should see:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A log message indicating the bot is checking for tweets.&lt;/li&gt;
&lt;li&gt;Either a "no tweets found" message or a successful Google Chat notification.&lt;/li&gt;
&lt;li&gt;The process exits gracefully.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;:&lt;br&gt;
Twitter (X) API has very low rate limits (duh) for free accounts. You will only be able to search Tweets once every 15 minutes. &lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Scheduling The Script on Choreo
&lt;/h2&gt;

&lt;p&gt;Choreo is a fantastic platform for running scheduled tasks without the hassle of managing infrastructure. Here's how to deploy your task there.&lt;/p&gt;

&lt;p&gt;Before following the below steps, make sure you commit your code to GitHub.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8.1: Set Up Your Choreo Component
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Log into Choreo&lt;/strong&gt;: Head over to &lt;a href="https://console.choreo.dev" rel="noopener noreferrer"&gt;console.choreo.dev&lt;/a&gt; and sign in with your account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a Component&lt;/strong&gt;: Click on "Create Component" to start the magic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose Component Type&lt;/strong&gt;: Select &lt;strong&gt;"Scheduled Task"&lt;/strong&gt; as your component type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link Your Repository&lt;/strong&gt;: Connect your GitHub repository that contains the source code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure Build Settings&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;NodeJS&lt;/strong&gt; as your &lt;strong&gt;Build Preset&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select your preferred Node.js version.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click Create&lt;/strong&gt;: Hit that create button and let Choreo work its magic&lt;/li&gt;
&lt;/ol&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%2Fhupvmx3nm39e2rjbnx43.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%2Fhupvmx3nm39e2rjbnx43.png" alt="Choreo Component Creation" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8.2: Wait for the Build
&lt;/h3&gt;

&lt;p&gt;Once your component is created, Choreo will start building your project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;Build&lt;/strong&gt; page from the left menu.&lt;/li&gt;
&lt;li&gt;Watch the build process complete (grab another coffee while you wait).&lt;/li&gt;
&lt;li&gt;Make sure the build status shows as successful.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 8.3: Configure and Deploy
&lt;/h3&gt;

&lt;p&gt;Now comes the fun part. Deployment!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;From the left menu, click on &lt;strong&gt;"Deploy"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;"Configure and Deploy"&lt;/strong&gt; button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment Configurations&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;In the pop-up drawer, click &lt;strong&gt;"Add Configuration"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add your environment variables from your &lt;code&gt;.env&lt;/code&gt; file:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TWITTER_BEARER_TOKEN&lt;/code&gt;: Your Twitter API bearer token&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GOOGLE_CHAT_WEBHOOK_URL&lt;/code&gt;: Your Google Chat webhook URL
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HASHTAG&lt;/code&gt;: Your product hashtag (e.g., &lt;code&gt;#choreoisawesome&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click Next&lt;/strong&gt;: Move to the scheduling configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set Schedule&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;In the schedule drawer, set it to run &lt;strong&gt;every 15 minutes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;This ensures the task is compatible with Twitter API rate limits :( &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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%2Ffy3110e3wslz9dt2pk7t.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%2Ffy3110e3wslz9dt2pk7t.png" alt="Scheduling a task in Choreo" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hit (just press!) that deploy button and watch your task come to life!&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 8.4: Monitoring the Execution
&lt;/h3&gt;

&lt;p&gt;Want to see your bot in action?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;Execute&lt;/strong&gt; from the left menu&lt;/li&gt;
&lt;li&gt;Check the execution history to see when your bot runs&lt;/li&gt;
&lt;li&gt;View execution logs to see if tweets are being found and sent&lt;/li&gt;
&lt;li&gt;Monitor for any errors or issues&lt;/li&gt;
&lt;/ul&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%2Fecouh486fxjltv7l53rl.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%2Fecouh486fxjltv7l53rl.png" alt="Task execution page on Choreo" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Choreo is awesome for this:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No server management headaches&lt;/li&gt;
&lt;li&gt;Built-in scheduling (no cron jobs needed)&lt;/li&gt;
&lt;li&gt;Easy environment variable management&lt;/li&gt;
&lt;li&gt;Execution history and logging out of the box&lt;/li&gt;
&lt;li&gt;Scales automatically (though our bot is pretty lightweight)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion (The Part Where I Wrap This Up)
&lt;/h2&gt;

&lt;p&gt;And there you have it! You've just built a Twitter monitoring bot that saves you from the endless scroll of social media doom. Your bot will dutifully watch Twitter for mentions of your product and deliver the good news (or constructive feedback) straight to your team's Google Chat.&lt;/p&gt;

&lt;p&gt;No more FOMO about what people are saying about your product. No more manual searching through Twitter. No more wondering if that one customer actually liked your latest feature.&lt;/p&gt;

&lt;p&gt;Just pure, automated, social media monitoring bliss.&lt;/p&gt;

&lt;p&gt;Now go forth and monitor those hashtags! And maybe treat yourself to a cheesecake from Barista. You've earned it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. - If this helped you become the office hero, consider buying me a coffee or simply putting a reaction!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/suvink" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.buymeacoffee.com%2Fbuttons%2Fv2%2Fdefault-yellow.png" alt="Buy Me A Coffee" width="545" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Build a Discord to Google Chat Bridge Bot (And Why Your Team Will Love You)</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Fri, 15 Aug 2025 14:37:32 +0000</pubDate>
      <link>https://dev.to/suvink/how-to-build-a-discord-to-google-chat-bridge-bot-and-why-your-team-will-love-you-2ljj</link>
      <guid>https://dev.to/suvink/how-to-build-a-discord-to-google-chat-bridge-bot-and-why-your-team-will-love-you-2ljj</guid>
      <description>&lt;p&gt;Ever been in a meeting discussing important project updates in Google Chat while your devrel team drops fire memes in Discord? Yeah, me too. Today, we're fixing that communication chaos once and for all.&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%2F5lvijbnotm89wq8y654n.jpg" 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%2F5lvijbnotm89wq8y654n.jpg" alt="Discord and Google Chat integration" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
Image credits: Masters of AI &amp;amp; Automation



&lt;h2&gt;
  
  
  The Problem Many Modern Teams Face
&lt;/h2&gt;

&lt;p&gt;Let's be honest. Your team probably lives in multiple chat apps. Marketing loves Slack, developers worship Discord, and management insists on Google Chat because "it integrates with our workspace." Meanwhile, important messages are getting lost in the shuffle faster than my motivation on Monday mornings.&lt;/p&gt;

&lt;p&gt;Sound familiar? Well, grab your favorite caffeinated beverage because we're about to build something that'll make you the office hero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;We're creating a Discord bot that acts like a digital messenger pigeon between Discord and Google Chat. Here's what this little guy will do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Listen&lt;/strong&gt; to a specific Discord channel like it's eavesdropping at a coffee shop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capture&lt;/strong&gt; messages and format them nicely (because we're not animals)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forward&lt;/strong&gt; everything to Google Chat with rich formatting and direct links&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make your team think&lt;/strong&gt; you're some kind of integration wizard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end of this tutorial, you'll have a bot that bridges these platforms seamlessly. No more "did you see what Janaka posted in Discord?" moments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before We Start Coding
&lt;/h2&gt;

&lt;p&gt;Make sure you've got these obvious but important stuff.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; (version 20 or newer - if you're still on Node 14, it's time to upgrade, friend)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NPM&lt;/strong&gt; (comes with Node.js, like fries with a burger)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discord Developer Account&lt;/strong&gt; with bot creation privileges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Workspace Account&lt;/strong&gt; (the kind that gives you access to Google Chat)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Choreo Account&lt;/strong&gt;: If you don't have one, sign up free at &lt;a href="https://console.choreo.dev/signup" rel="noopener noreferrer"&gt;https://console.choreo.dev/signup&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Creating Your Discord Bot
&lt;/h2&gt;

&lt;p&gt;First, let's bring a new Discord bot into existence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Head over to the &lt;a href="https://discord.com/developers/applications" rel="noopener noreferrer"&gt;Discord Developer Portal&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"New Application"&lt;/strong&gt; and give it a name (I went with "PingKong" - King Kong, but for pinging across apps.)&lt;/li&gt;
&lt;li&gt;Navigate to the &lt;strong&gt;"Bot"&lt;/strong&gt; tab and click &lt;strong&gt;"Add Bot"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Important:&lt;/strong&gt; Enable "Message Content Intent" - this is like giving your bot permission to actually read what people are saying.&lt;/li&gt;
&lt;li&gt;Copy that bot token (keep it secret, keep it safe).&lt;/li&gt;
&lt;li&gt;In OAuth2 URL Generator, select &lt;strong&gt;"bot"&lt;/strong&gt; scope and these permissions:

&lt;ul&gt;
&lt;li&gt;Read Messages/View Channels&lt;/li&gt;
&lt;li&gt;Send Messages&lt;/li&gt;
&lt;li&gt;Read Message History&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use the generated URL to invite your bot to your server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Pro tip: Your bot will show up offline initially - don't panic, it's not broken, just offline until we turn it up&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Setting Up Google Chat Webhook
&lt;/h2&gt;

&lt;p&gt;Google Chat webhooks are surprisingly straightforward and probably the easiest thing to do in this tutorial:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Google Chat and find your Chat Space.&lt;/li&gt;
&lt;li&gt;Click the space name → &lt;strong&gt;"Apps &amp;amp; integrations"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Add webhooks"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name.&lt;/li&gt;
&lt;li&gt;Add an avatar if you're feeling fancy.&lt;/li&gt;
&lt;li&gt;Hit &lt;strong&gt;"Save"&lt;/strong&gt; and copy that webhook URL&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: Project Setup
&lt;/h2&gt;

&lt;p&gt;Time to create our project structure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new directory for the project home.
&lt;/li&gt;
&lt;/ol&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;discord-to-gchat-bot
&lt;span class="nb"&gt;cd &lt;/span&gt;discord-to-gchat-bot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Initialize a new Node project.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Install our dependencies.
&lt;/li&gt;
&lt;/ol&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;discord.js axios dotenv typescript @types/node
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; typescript ts-node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Set up TypeScript.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsc &lt;span class="nt"&gt;--init&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What we just installed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;discord.js&lt;/code&gt; - The library that connects your bot to Discord.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;axios&lt;/code&gt; - For making HTTP requests.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotenv&lt;/code&gt; - Keeps our secrets safe in environment variables.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;typescript&lt;/code&gt; - Because we're professionals who like type safety.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: TypeScript Configuration
&lt;/h2&gt;

&lt;p&gt;Update your &lt;code&gt;tsconfig.json&lt;/code&gt; with these settings:&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;"compilerOptions"&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;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2020"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CommonJS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"esModuleInterop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"skipLibCheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"forceConsistentCasingInFileNames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"resolveJsonModule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="s2"&gt;"src/**/*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&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;"node_modules"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&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;This configuration basically tells TypeScript: "Compile our code to ES2020, be strict about types, and don't include node_modules in compilation" (because that would be chaos).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Environment Variables
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DISCORD_TOKEN=your_discord_bot_token_here
GOOGLE_CHAT_WEBHOOK_URL=your_webhook_url_here
TARGET_CHANNEL_ID=discord_channel_id_to_watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Getting your Discord channel ID:&lt;/strong&gt; Enable Developer Mode in Discord settings (Settings → Advanced), then right-click any channel and select "Copy ID".&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Security note: Never commit your .env file to version control. It's like posting your passwords on a billboard.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Type Definitions
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;src/types.ts&lt;/code&gt;:&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="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;BOT_EVENTS&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;READY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;MESSAGE_CREATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messageCreate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;ERROR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&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="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;EnvironmentVariables&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;DISCORD_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;GOOGLE_CHAT_WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;TARGET_CHANNEL_ID&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GoogleChatPayload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;cardsV2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;cardId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nl"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nl"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nl"&gt;imageType&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CIRCLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SQUARE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nl"&gt;imageAltText&lt;/span&gt;&lt;span class="p"&gt;?:&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;span class="nl"&gt;sections&lt;/span&gt;&lt;span class="p"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nl"&gt;widgets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;decoratedText&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nx"&gt;topLabel&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                        &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                        &lt;span class="nl"&gt;startIcon&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="nx"&gt;knownIcon&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                            &lt;span class="nl"&gt;iconUrl&lt;/span&gt;&lt;span class="p"&gt;?:&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;span class="p"&gt;};&lt;/span&gt;
                    &lt;span class="nl"&gt;textParagraph&lt;/span&gt;&lt;span class="p"&gt;?:&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="p"&gt;};&lt;/span&gt;
                    &lt;span class="nl"&gt;buttonList&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;:&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                            &lt;span class="nl"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="nx"&gt;openLink&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&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;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="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;&lt;strong&gt;What's happening here:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We're defining the structure for Google Chat's card format (it's like HTML but in JSON form). For more info, refer to the &lt;a href="https://developers.google.com/workspace/chat/api/reference/rest/v1/cards" rel="noopener noreferrer"&gt;Google Chat Cards Documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;TypeScript will yell at us if we try to use the wrong structure (which is good!)&lt;/li&gt;
&lt;li&gt;Think of interfaces as contracts - they define what shape our data should have&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 7: The Main Event - Building Our Bot
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;src/server.ts&lt;/code&gt; - this is where the magic happens:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GatewayIntentBits&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;TextChannel&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;discord.js&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;axios&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;axios&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dotenv&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;dotenv&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;BOT_EVENTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EnvironmentVariables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GoogleChatPayload&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;./types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Discord's logo - because branding matters&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;discordLogoUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;REPLACE_WITH_A_URL_TO_DISCORD_LOGO&amp;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;dotenv&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="c1"&gt;// Load our secret variables&lt;/span&gt;

&lt;span class="c1"&gt;// This function makes sure we have all our required environment variables&lt;/span&gt;
&lt;span class="c1"&gt;// Think of it as a pre-flight checklist&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validateEnv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;EnvironmentVariables&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;DISCORD_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GOOGLE_CHAT_WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TARGET_CHANNEL_ID&lt;/span&gt; &lt;span class="p"&gt;}&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;DISCORD_TOKEN&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CHAT_WEBHOOK_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;TARGET_CHANNEL_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing required environment variables - check your .env file!&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DISCORD_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GOOGLE_CHAT_WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TARGET_CHANNEL_ID&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;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateEnv&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Create our Discord client with the right permissions&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&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;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nx"&gt;GatewayIntentBits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Guilds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// Access to server info&lt;/span&gt;
        &lt;span class="nx"&gt;GatewayIntentBits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GuildMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// Read messages&lt;/span&gt;
        &lt;span class="nx"&gt;GatewayIntentBits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MessageContent&lt;/span&gt;    &lt;span class="c1"&gt;// Actually see message content&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;Now for the heart of our bot - the message forwarding function:&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="cm"&gt;/**
 * This function takes a Discord message and formats it nicely for Google Chat
 */&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;sendToGoogleChat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;discordMessage&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;threadTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Handle different channel types (because Discord has many flavors)&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;discordMessage&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="nf"&gt;isThread&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;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;discordMessage&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parentChannel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&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;parentChannel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;channelName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parentChannel&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="nx"&gt;threadTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thread&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;channelName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thread&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="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;channelName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;discordMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;TextChannel&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="c1"&gt;// Extract the details from the message&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;discordMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&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;avatarUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;discordMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;displayAvatarURL&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;messageContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;discordMessage&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="c1"&gt;// Create a direct link back to the original message&lt;/span&gt;
        &lt;span class="c1"&gt;// It's like leaving breadcrumbs, but digital&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://discord.com/channels/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;discordMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guild&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="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;discordMessage&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="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;discordMessage&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Build our fancy Google Chat card&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GoogleChatPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;cardsV2&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="na"&gt;cardId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`discord-message-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;discordMessage&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="na"&gt;title&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 from Discord&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;threadTitle&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="na"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;discordLogoUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="na"&gt;imageType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CIRCLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="na"&gt;imageAltText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Discord logo&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;sections&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="na"&gt;widgets&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="na"&gt;decoratedText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                            &lt;span class="na"&gt;topLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Posted by&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                            &lt;span class="na"&gt;startIcon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                                &lt;span class="na"&gt;iconUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;avatarUrl&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="na"&gt;textParagraph&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;messageContent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(No text content - probably an emoji battle)&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="na"&gt;buttonList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                            &lt;span class="na"&gt;buttons&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="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;View in Discord&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                    &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                                        &lt;span class="na"&gt;openLink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                                            &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messageLink&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="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="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;// Send it off to Google Chat&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CHAT_WEBHOOK_URL&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="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="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&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="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;`Message forwarded from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;channelName&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;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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error sending to Google Chat:&lt;/span&gt;&lt;span class="dl"&gt;"&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="c1"&gt;// In production, you might want to add retry logic here&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;Finally, let's set up our event listeners:&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="c1"&gt;// Listen for new messages (this is where the magic happens)&lt;/span&gt;
&lt;span class="nx"&gt;client&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="nx"&gt;BOT_EVENTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MESSAGE_CREATE&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;message&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Skip DM messages - we're only interested in server messages&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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;guild&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="c1"&gt;// Ignore bot messages (including our own) - prevents infinite loops&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bot&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="c1"&gt;// Only process messages from our target channel or its threads&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channelId&lt;/span&gt; &lt;span class="o"&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;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;parentId&lt;/span&gt; &lt;span class="o"&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isThread&lt;/span&gt;&lt;span class="p"&gt;()&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentId&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channelId&lt;/span&gt; &lt;span class="o"&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;TARGET_CHANNEL_ID&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;parentId&lt;/span&gt; &lt;span class="o"&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;TARGET_CHANNEL_ID&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="c1"&gt;// Forward the message to Google Chat&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendToGoogleChat&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// When the bot successfully connects&lt;/span&gt;
&lt;span class="nx"&gt;client&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="nx"&gt;BOT_EVENTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;READY&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;`🤖 Bot logged in as &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;tag&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;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;`👀 Monitoring channel ID: &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;TARGET_CHANNEL_ID&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;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;`🚀 Ready to accept messages!`&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 errors gracefully&lt;/span&gt;
&lt;span class="nx"&gt;client&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="nx"&gt;BOT_EVENTS&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Discord client error:&lt;/span&gt;&lt;span class="dl"&gt;"&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;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// In a real app, you might want to restart the bot or alert administrators&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Start the bot&lt;/span&gt;
&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&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;DISCORD_TOKEN&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="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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to login:&lt;/span&gt;&lt;span class="dl"&gt;"&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;message&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="nf"&gt;exit&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Package Scripts
&lt;/h2&gt;

&lt;p&gt;Update your &lt;code&gt;package.json&lt;/code&gt; scripts section with the following:&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;"scripts"&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;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node dist/server.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc &amp;amp;&amp;amp; node dist/server.js"&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;&lt;em&gt;Note: These are pretty standard TypeScript stuff.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Launch Time! 🚀
&lt;/h2&gt;

&lt;p&gt;Build and run your bot:&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="c"&gt;# Compile TypeScript to JavaScript&lt;/span&gt;
npm run build

&lt;span class="c"&gt;# Start the bot&lt;/span&gt;
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything works, you should see your bot come online in Discord and start monitoring your channel. Send a test message and watch it appear in Google Chat like magic!&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking It a Step Further
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Filter Messages by Keywords
&lt;/h3&gt;

&lt;p&gt;Want to only forward important messages? Add some filtering.&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="c1"&gt;// Only forward messages containing "urgent" or "important"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urgent&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="s1"&gt;important&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="s1"&gt;breaking&lt;/span&gt;&lt;span class="dl"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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="nx"&gt;keyword&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Monitor Multiple Channels
&lt;/h3&gt;

&lt;p&gt;Replace single channel monitoring with multiple:&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;targetChannels&lt;/span&gt; &lt;span class="o"&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;TARGET_CHANNEL_ID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;targetChannels&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="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;targetChannels&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="nx"&gt;parentId&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploying the Bot
&lt;/h2&gt;

&lt;p&gt;WSO2's Choreo is a developer-friendly cloud platform that makes deploying applications as easy as clicking a button. Let's walk through deploying our Discord bot as a Webhook component on Choreo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Commit Your Code to GitHub
&lt;/h3&gt;

&lt;p&gt;You already know what to do here :)&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create a Webhook Component
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;"Create Component"&lt;/strong&gt; in your project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select Component Type&lt;/strong&gt;: Choose &lt;strong&gt;"Webhook"&lt;/strong&gt; from the component types. You might want to click "View All Component Types" to find this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect Your Repository&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;  Select &lt;strong&gt;"GitHub"&lt;/strong&gt; as your source&lt;/li&gt;
&lt;li&gt;  Authorize Choreo to access your repositories&lt;/li&gt;
&lt;li&gt;  Choose the repository containing your bot code&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Setup the Component:

&lt;ul&gt;
&lt;li&gt;Set the &lt;strong&gt;Build Preset&lt;/strong&gt; to &lt;code&gt;Node.js&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Set the &lt;strong&gt;Language Version&lt;/strong&gt; to 20.x.x&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create and Deploy&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For more information, refer to &lt;a href="https://wso2.com/choreo/docs/developer/develop-components/develop-a-webhook/" rel="noopener noreferrer"&gt;Develop a Webhook&lt;/a&gt; documentation on Choreo.&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%2Fexvi1telt0dqeit3fk8s.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%2Fexvi1telt0dqeit3fk8s.png" alt="Creating a Choreo Webhook Component" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure Environment Variables
&lt;/h3&gt;

&lt;p&gt;This is where we safely store our Discord token and webhook URL:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; In your component, go to the &lt;strong&gt;"DevOps"&lt;/strong&gt; tab from the left menu.&lt;/li&gt;
&lt;li&gt; Click on &lt;strong&gt;"Configs &amp;amp; Secrets"&lt;/strong&gt; in the left sidebar.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+ Create&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set the type of the Configuration as "Environment Variables" and mark it as a secret.&lt;/li&gt;
&lt;li&gt;Give the configuration a Display Name: Mine is &lt;strong&gt;ping-kong-configs&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add your environment variables one by one. After adding each, click &lt;strong&gt;Add&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt; to create the configutation group and save it.&lt;/li&gt;
&lt;/ol&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%2Fg8qqyvzpeh78s80cxvza.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%2Fg8qqyvzpeh78s80cxvza.png" alt="Creating a Choreo Configuration Group" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For more information, refer to &lt;a href="https://wso2.com/choreo/docs/developer/devops-and-ci-cd/manage-configurations-and-secrets/#add-an-environment-variable-to-your-container" rel="noopener noreferrer"&gt;Manage Configurations and Secrets&lt;/a&gt; documentation on Choreo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Basic Troubleshooting Steps If Things Go Wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bot shows offline?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Double-check your Discord token&lt;/li&gt;
&lt;li&gt;Verify the bot has proper server permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No messages forwarding?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirm your TARGET_CHANNEL_ID is correct&lt;/li&gt;
&lt;li&gt;Make sure "Message Content Intent" is enabled&lt;/li&gt;
&lt;li&gt;Check that the bot can read the target channel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Google Chat webhook failing?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify your webhook URL is still valid&lt;/li&gt;
&lt;li&gt;Check if the webhook was removed from the Chat space&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TypeScript compilation errors?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;npm run build&lt;/code&gt; to see detailed error messages&lt;/li&gt;
&lt;li&gt;Make sure all dependencies are installed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Congratulations! You've just built a communication bridge that would make you a hero at work. Your team can now stay connected across platforms without missing important messages.&lt;/p&gt;

&lt;p&gt;This project demonstrates the power of APIs and webhooks for platform integration. With this foundation, you could do more cool stuff like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add bidirectional messaging&lt;/li&gt;
&lt;li&gt;Support more message types (images, reactions, etc.)&lt;/li&gt;
&lt;li&gt;Create bridges to other platforms like Slack or Microsoft Teams&lt;/li&gt;
&lt;li&gt;Add message threading and advanced formatting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part? Your team will think you're some kind of integration wizard, when really you just know how to make different APIs play nicely together.&lt;/p&gt;

&lt;p&gt;Now go forth and bridge those communication gaps! And remember: with great power comes great responsibility to not spam your teammates with bot messages. 😉&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. - If this helped you become the office hero, consider buying me a coffee or simply add a reaction. If it broke everything... well, that's what version control is for!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/suvink" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.buymeacoffee.com%2Fbuttons%2Fv2%2Fdefault-yellow.png" alt="Buy Me A Coffee" width="545" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Redis Headless Service vs. Cluster IP Service: Which one should you use?</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Tue, 18 Mar 2025 17:07:18 +0000</pubDate>
      <link>https://dev.to/suvink/redis-headless-service-vs-cluster-ip-service-which-one-should-you-use-2hio</link>
      <guid>https://dev.to/suvink/redis-headless-service-vs-cluster-ip-service-which-one-should-you-use-2hio</guid>
      <description>&lt;p&gt;If you’ve ever tried to set up a Redis cluster in Kubernetes, you’ve probably stumbled upon two types of services: the &lt;strong&gt;ClusterIP service&lt;/strong&gt; and the &lt;strong&gt;Headless service&lt;/strong&gt;. At first glance, they might seem like two sides of the same coin, but trust me, they’re not.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s a Redis Cluster?
&lt;/h3&gt;

&lt;p&gt;Before we get into the services, let’s quickly recap what a Redis cluster is. Redis cluster is a distributed version of Redis that allows you to shard your data across multiple nodes automatically. It’s like having a team of Redis instances working together, each handling a portion of the data. This setup gives you high availability, fault tolerance, and the ability to scale horizontally. For more info on the Redis cluster, see &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/" rel="noopener noreferrer"&gt;Redis documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In my local K8s cluster, I’ve deployed a &lt;a href="https://bitnami.com/stacks/redis-cluster" rel="noopener noreferrer"&gt;Bitnami Redis cluster&lt;/a&gt;. When I do a &lt;code&gt;kubectl get pods -n redis&lt;/code&gt;, it shows me 6 pods because I have configured the cluster to have 3 master nodes and one slave node each.&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%2Fhto10lmcsb1kxeqla27l.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%2Fhto10lmcsb1kxeqla27l.png" alt="Local Cluster Pods" width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let's check what are the services created for the Redis cluster with &lt;code&gt;kubectl get svc -n redis&lt;/code&gt;:&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%2F2n5j5fxwuidhqh3bki61.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%2F2n5j5fxwuidhqh3bki61.png" alt="Local Cluster Services" width="800" height="75"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It lists three services: &lt;code&gt;redis-cluster&lt;/code&gt; (also known as the Cluster IP service), &lt;code&gt;redis-cluster-headless&lt;/code&gt;, and &lt;code&gt;redis-cluster-metrics&lt;/code&gt;. As the name suggests, the metrics service is used to expose Redis cluster metrics for monitoring.&lt;/p&gt;

&lt;p&gt;In this blog, we’ll focus on the headless service and the cluster IP service.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cluster IP Service
&lt;/h3&gt;

&lt;p&gt;The cluster IP service, or in my example, the &lt;code&gt;redis-cluster&lt;/code&gt; service is a standard Kubernetes service that provides a single, stable IP address for accessing a set of pods. It load-balances traffic across the pods, making it easy to use for basic operations. When a client connects to this service, Kubernetes load-balances requests across the Redis nodes making it a straightforward method for simple use cases.&lt;/p&gt;

&lt;p&gt;While the cluster IP service is easy to use, it has some limitations when it comes to a Redis cluster.&lt;/p&gt;

&lt;p&gt;Redis cluster uses &lt;code&gt;MOVED&lt;/code&gt; and &lt;code&gt;ASK&lt;/code&gt; redirections to make sure the commands are routed to the correct node. In a cluster setup, each node is responsible for a specific range of hash slots. The hash slot for a given key is determined using the CRC16 algorithm, with the total number of hash slots being fixed.&lt;/p&gt;

&lt;p&gt;For example, in my Bitnami Redis cluster, let's assume that the master nodes handle the following ranges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node 1: 0–5460&lt;/li&gt;
&lt;li&gt;Node 2: 5461–10922&lt;/li&gt;
&lt;li&gt;Node 3: 10923–16383&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a key falls into hash slot 1234, it belongs to Node 1. However, when a client sends a command to store this key, it cannot directly connect to Node 1. The cluster IP service load balances the request and forwards it to a random pod. If the selected pod doesn’t serve the required hash slot, it responds with &lt;code&gt;MOVED&lt;/code&gt; or &lt;code&gt;ASK&lt;/code&gt;, instructing the client to reach the correct node.&lt;/p&gt;

&lt;p&gt;When the client retries to connect to the correct node, it again connects via the cluster IP service, but since the service is load-balanced, the request is sent to another random pod. This cycle continues, preventing a direct connection to the intended node.&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%2Fres.cloudinary.com%2Fdgebfmk1g%2Fimage%2Fupload%2Fv1742318007%2Fredis-cluster_udgb4e.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%2Fres.cloudinary.com%2Fdgebfmk1g%2Fimage%2Fupload%2Fv1742318007%2Fredis-cluster_udgb4e.png" alt="Illustration of Cluster IP Service Retries" width="800" height="332"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Click &lt;a href="https://res.cloudinary.com/dgebfmk1g/image/upload/v1742318007/redis-cluster_udgb4e.png" rel="noopener noreferrer"&gt;here&lt;/a&gt; for the full quality image.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Headless Service
&lt;/h3&gt;

&lt;p&gt;Unlike the cluster IP service, the headless service is designed to expose the individual IPs of your Redis pods, allowing clients to connect directly to the Redis nodes as needed.&lt;/p&gt;

&lt;p&gt;If you look at the earlier image showing the list of services, you’ll notice that the headless service doesn’t have an assigned IP. Instead, when a client queries its DNS name (e.g.: &lt;code&gt;redis-headless.namespace.svc.cluster.local&lt;/code&gt;), it gets back the IP addresses of all Redis pods in the cluster. The client can then connect to one of these nodes and use the &lt;code&gt;CLUSTER NODES&lt;/code&gt; or &lt;code&gt;CLUSTER SLOTS&lt;/code&gt; command to retrieve the full cluster topology. This provides key details like node roles (master or slave) and hash slot assignments, allowing the client to map out the cluster.&lt;/p&gt;

&lt;p&gt;With this information, when a client needs to store a key, it can determine which node is responsible for that key and connect directly using the headless service—ensuring efficient and accurate routing of requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which service should I use?
&lt;/h3&gt;

&lt;p&gt;If you’ve tried setting up a service that connects to a Redis cluster using a Redis client, you might have run into connection issues when using the cluster IP service. That’s because most Redis clients (if not all) are designed to understand the cluster topology and send commands directly to the right node, avoiding redirections.&lt;/p&gt;

&lt;p&gt;So, if you’re connecting to a Redis cluster from a client, you need to use the headless service.&lt;/p&gt;

&lt;p&gt;However, if you’re just running simple operations and performance isn’t a concern—like testing the Redis cluster from the CLI—the cluster IP service will work just fine.&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%2F7md6bo0podckl6avxgmz.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%2F7md6bo0podckl6avxgmz.png" alt="That's all folks" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s it for this post! If you have any questions, drop them in the comments. Thanks for reading!&lt;/p&gt;

</description>
      <category>redis</category>
      <category>rediscluster</category>
      <category>database</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Coding Authentication? Stop now and let Choreo manage it.</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Wed, 27 Dec 2023 19:16:17 +0000</pubDate>
      <link>https://dev.to/suvink/coding-authentication-stop-now-and-let-choreo-manage-it-4ofa</link>
      <guid>https://dev.to/suvink/coding-authentication-stop-now-and-let-choreo-manage-it-4ofa</guid>
      <description>&lt;p&gt;As a developer, you might have come across situations where you want to add authentication to a web app but it takes so much time/effort to do so writing all the code and managing protocols that you kind of wished there was an easy way. Well, Choreo's Managed Authentication lets you add authentication to your web app with a few clicks and a few lines of code easily. &lt;/p&gt;

&lt;p&gt;Let's see how.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: Choreo's managed authentication is currently available only for React, Angular, and Vue.js web applications.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A React app &lt;/li&gt;
&lt;li&gt;A GitHub Account&lt;/li&gt;
&lt;li&gt;A Choreo Account (&lt;a href="https://console.choreo.dev/signup" rel="noopener noreferrer"&gt;Create an account here&lt;/a&gt;. It's FREE)&lt;/li&gt;
&lt;li&gt;Basic understanding of React JS&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: The Code
&lt;/h3&gt;

&lt;p&gt;Once you enable Managed Authentication (which we will be doing in step 2), you need to redirect your application users to the respective login and logout paths for them to authenticate. &lt;/p&gt;

&lt;h4&gt;
  
  
  Configure Login
&lt;/h4&gt;

&lt;p&gt;To implement the login functionality, all you have to do is, redirect the user to the &lt;code&gt;/auth/login&lt;/code&gt; path. Once redirected, Choreo will show a login form and users can use their credentials to log in.  &lt;/p&gt;

&lt;p&gt;Let's implement this in a sample 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;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Link&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;react-router-dom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;App&lt;/span&gt;&lt;span class="dl"&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;/* Vanilla JS */&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;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/login&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;Login&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* React Router v6 */&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;Link&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/login&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;Login&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Link&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;If you are using React Router to handle navigation, you can use the respective navigation components offered by the React Router DOM. For this tutorial, we will be using vanilla JS.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Read user information
&lt;/h4&gt;

&lt;p&gt;Once the user is logged in, you may need to access information like the user's name, email, etc. Choreo sends this information encoded in a cookie (named &lt;code&gt;userinfo&lt;/code&gt;) upon successful login. We can extract the information using a cookie parsing library. For this example, we will be using &lt;a href="https://www.npmjs.com/package/js-cookie" rel="noopener noreferrer"&gt;&lt;code&gt;js-cookie&lt;/code&gt;&lt;/a&gt; library.&lt;/p&gt;

&lt;p&gt;You can execute &lt;code&gt;npm i js-cookie&lt;/code&gt; command from your terminal to install the library.&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="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="s1"&gt;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;import&lt;/span&gt; &lt;span class="nx"&gt;Cookies&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js-cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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;encodedUserInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Cookies&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="s1"&gt;userinfo&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;userInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUserInfo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&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="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;encodedUserInfo&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="c1"&gt;// Read the cookie and set it to the state.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userInfo&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="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encodedUserInfo&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;userInfo&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;info&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;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userinfo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setUserInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;Cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userinfo&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;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nx"&gt;encodedUserInfo&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;App&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;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/login&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;Login&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&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 code, first, we use the &lt;code&gt;js-cookie&lt;/code&gt; library to read the &lt;code&gt;userinfo&lt;/code&gt; cookie and then we set the information to the local state as well as to the local storage for persistence. After that, we clear the cookie (or biscuit if you're British). &lt;/p&gt;

&lt;p&gt;Now, at this point, we can modify our code a bit to show the user's username if authenticated and if not, we'll show the login button.&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="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="s1"&gt;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;import&lt;/span&gt; &lt;span class="nx"&gt;Cookies&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js-cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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;encodedUserInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Cookies&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="s1"&gt;userinfo&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;userInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUserInfo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&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="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;encodedUserInfo&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="c1"&gt;// Read the cookie and set it to the state.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userInfo&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="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encodedUserInfo&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;userInfo&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;info&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;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userinfo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setUserInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;Cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userinfo&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;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nx"&gt;encodedUserInfo&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;App&lt;/span&gt;&lt;span class="dl"&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;userInfo&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="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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Welcome&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&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;/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="sr"&gt;/div&amp;gt; &lt;/span&gt;&lt;span class="err"&gt;:
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/login&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;Login&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;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="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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Configure Logout
&lt;/h4&gt;

&lt;p&gt;Similar to the login, we have to redirect the users to &lt;code&gt;auth/logout&lt;/code&gt; path to log a user out. But for logout, we have to redirect the user with the session hint parameter which we can find in the &lt;code&gt;'session_hint&lt;/code&gt; cookie. This cookie is also set when the user is successfully authenticated. Let's add that code to our 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;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="s1"&gt;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;import&lt;/span&gt; &lt;span class="nx"&gt;Cookies&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js-cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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;encodedUserInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Cookies&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="s1"&gt;userinfo&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;userInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUserInfo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&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="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;encodedUserInfo&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="c1"&gt;// Read the cookie and set it to the state.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userInfo&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="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encodedUserInfo&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;userInfo&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;info&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;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userinfo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setUserInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;Cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userinfo&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;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nx"&gt;encodedUserInfo&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;handleLogout&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="c1"&gt;// Read the cookie&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionHint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Cookies&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="s1"&gt;session_hint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Clear session data&lt;/span&gt;
    &lt;span class="nx"&gt;Cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userinfo&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;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Redirect the user&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/auth/logout?session_hint=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionHint&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;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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;App&lt;/span&gt;&lt;span class="dl"&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;userInfo&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="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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Welcome&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&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;/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;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleLogout&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;Log&lt;/span&gt; &lt;span class="nx"&gt;Out&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&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&amp;gt; &lt;/span&gt;&lt;span class="err"&gt;:
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/login&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;Login&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;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="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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To explain the &lt;code&gt;handleLogout&lt;/code&gt; function, first, we read the &lt;code&gt;session_hint&lt;/code&gt; cookie. Then we clear the user information we persisted in the browser for better security. Lastly, we direct the user to the &lt;code&gt;auth/logout&lt;/code&gt; path with the session hint as a query parameter. &lt;/p&gt;

&lt;p&gt;🎉 Congratulations. Now you have an app with authentication enabled. Still, there's one more step to be done. &lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Host the code on Choreo.
&lt;/h3&gt;

&lt;p&gt;To complete this step, you need to have you source code committed to a GitHub (public) repository and you need to log into your Choreo Console. &lt;/p&gt;

&lt;h4&gt;
  
  
  Create a WebApp Component
&lt;/h4&gt;

&lt;p&gt;Once you are logged in to the Choreo Console, select the Web Application component and create a new web app.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F52mqsof3h9e9dw4zypqq.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F52mqsof3h9e9dw4zypqq.png" alt="Choose the WebApplication Component from the Choreo Console"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enter a name for the component and connect your GitHub repository to the component. From the buildpack section, choose "React" buildpack as we are creating a React Web App.&lt;/p&gt;

&lt;p&gt;In the build configurations, input your build command (default: &lt;code&gt;npm run build&lt;/code&gt;), build path (default: &lt;code&gt;/build&lt;/code&gt;) and Node.js version. Finally, click on Create button.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9hzhuj4dexv1jkff9m1e.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9hzhuj4dexv1jkff9m1e.png" alt="Web app creation settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Build the web application
&lt;/h4&gt;

&lt;p&gt;After the component creation, navigate to the "Build" area of the Choreo Console.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2j1d8ders0bgutui44i.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2j1d8ders0bgutui44i.png" alt="Use the left navigation bar to access the Build area"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on Build, Choose the latest commit of your GitHub repository and build it. It will take some time for the application to be built so in the meantime, feel free to explore the Choreo Console and see what it can do for you!&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fjdw2q46spl6d9n6upj.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fjdw2q46spl6d9n6upj.png" alt="Follow these steps to build the app"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Deploy the web application
&lt;/h4&gt;

&lt;p&gt;Once the web app is built, navigate to the Deploy area from the left navigation menu.&lt;/p&gt;

&lt;p&gt;From the "Set up" area, click on "Configure &amp;amp; Deploy" button to deploy the web application. &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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Files76t7n9v5pabi8nei.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Files76t7n9v5pabi8nei.png" alt="Deploy the web app"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the right drawer, just click next because we are not mounting any configurations for this web app and then you will see the Authentication step. You can already see that "Managed Authentication with Choreo" is enabled. &lt;/p&gt;

&lt;p&gt;In this menu, we have to configure the Post Login Path, Post Logout Path and the Error Path.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Post Login Path&lt;/strong&gt; - The relative path that the application will be redirected to on successful sign-in. In our example, we used the same page to display the user's info as well as to handle login/logout. So we will use &lt;code&gt;/&lt;/code&gt; as the Post Login Path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Post Logout Path&lt;/strong&gt; - The relative path to which Choreo redirects you on successful sign-out. For the same reason as above, we will use &lt;code&gt;/&lt;/code&gt; for this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Error Path&lt;/strong&gt; - The relative path to which Choreo redirects you when an error occurs during a redirection. By default, Choreo has a built-in error page to display errors. So we can leave this blank and let Choreo take care of that. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Configure Users
&lt;/h4&gt;

&lt;p&gt;To test the authentication settings we implemented, we can quickly add a demo user in the same "Authentication Settings" step. Click on the Create button and you can create a demo user with a strong password. &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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2z9alad8s0ubhwmsrlx7.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2z9alad8s0ubhwmsrlx7.png" alt="Create the demo user"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After creating the demo user, click on the "Deploy" button to deploy the app. Again, it will take a moment for the app to get deployed. Once deployed, you can click on the Web App URL on the Development Environment card and explore the app we created. &lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Try out a flow
&lt;/h3&gt;

&lt;p&gt;Click on the login button on the application. It will redirect you to the &lt;code&gt;/auth/login&lt;/code&gt; path and you can see Choreo's default login page.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fism97ef908ay99es67di.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fism97ef908ay99es67di.png" alt="Choreo's default login page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add your demo user's credentials and click on "Sign In". After successful sign in, you will see "Welcome demouser" message and the logout button on the screen. &lt;/p&gt;

&lt;h3&gt;
  
  
  Add more users
&lt;/h3&gt;

&lt;p&gt;When your app goes to production, you need to add more users than the demo user we created. To do that, go to the Choreo Console, navigate to Organization Settings &amp;gt; Application Security &amp;gt; Identity Providers and Click the "Manage" button on the "Choreo built-in Identity Provider". From there, you can download the CSV template for the users, fill it and upload it to the console.&lt;/p&gt;

&lt;p&gt;That's it folks!&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;In this tutorial, we implemented authentication functionality to a React JS web application using Choreo Managed Authentication. If you want to quickly start and experiment this feature, feel free to fork &lt;a href="https://github.com/Suvink/choreo-managed-auth-example" rel="noopener noreferrer"&gt;this repository&lt;/a&gt; and deploy it on the Choreo Console. &lt;/p&gt;

&lt;p&gt;Choreo Managed Authentication can be used for more advanced scenarios and it has more features that we can user in an enterprise environment. We will discuss them in the next article. &lt;/p&gt;

&lt;p&gt;Until then, Happy Coding!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>authentication</category>
      <category>wso2</category>
      <category>react</category>
    </item>
    <item>
      <title>Understanding Istio: A Beginner's Guide to Service Mesh</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Thu, 16 Nov 2023 16:32:52 +0000</pubDate>
      <link>https://dev.to/suvink/understanding-istio-a-beginners-guide-to-service-mesh-48k5</link>
      <guid>https://dev.to/suvink/understanding-istio-a-beginners-guide-to-service-mesh-48k5</guid>
      <description>&lt;p&gt;In the ever-changing world of modern software engineering, creating and managing applications with many moving parts can feel a bit like conducting a symphony. Imagine you're Hans Zimmer and your application is a musical masterpiece of the Pirates of the Caribbean, with each instrument representing a different microservice playing in harmony. Now, keeping this symphony in tune and ensuring every note is played just right becomes a complex challenge.&lt;/p&gt;

&lt;p&gt;This is where Istio steps onto the stage. Istio is like the conductor for your microservices orchestra, making sure each component plays its part seamlessly. In simpler terms, Istio is a powerful tool designed to simplify the management of applications made up of many smaller, interconnected pieces.&lt;/p&gt;

&lt;p&gt;Let's break it down in technical terms. Imagine that you have a web application with various services like user authentication, database access, and front-end rendering. As your user base grows, so does the complexity of managing these services. Istio comes to the rescue by providing a set of tools that help you manage traffic, enhance security, and gain insights into what's happening behind the scenes.&lt;/p&gt;

&lt;p&gt;But what's a service mesh and what does Istio have to do with it?&lt;/p&gt;

&lt;p&gt;In simple terms, the service mesh is our Hans Zimmer. Just like Zimmer instructs his instrument players to play a symphony, the service mesh takes care of managing and optimizing the communication pathways between services. Istio offers tools and features to implement and manage this service mesh, ensuring that your software's performance is orchestrated smoothly and securely. (It's open source!)&lt;/p&gt;

&lt;h3&gt;
  
  
  How Istio works:
&lt;/h3&gt;

&lt;p&gt;The Istio architecture can be explained in two main sections:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Data Plane&lt;/li&gt;
&lt;li&gt;The Control Plane&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Istio Data Plane
&lt;/h3&gt;

&lt;p&gt;The Data Plane is the operational heart of Istio, and it consists of a network of sidecar proxies, typically Envoy proxies, that are injected alongside each of your services. These sidecar proxies act as messengers, carrying out the instructions from the conductor (Control Plane) to ensure seamless communication between services.&lt;/p&gt;

&lt;p&gt;If we go back to our Hans Zimmer example, each of the players in Zimmer's orchestra has a dedicated trainer. This trainer runs around whispering instructions and ensuring the player's performance aligns with the overall symphony. Similarly, the envoy proxies act as the trainers for the services, managing traffic, enforcing policies, and providing vital information about what's happening within each service.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07t598anfbf7gmbqgrk1.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07t598anfbf7gmbqgrk1.jpg" alt="Istio DP"&gt;&lt;/a&gt;&lt;/p&gt;
Image Credits: &lt;a href="https://istio.io/" rel="noopener noreferrer"&gt;Istio&lt;/a&gt;



&lt;h3&gt;
  
  
  Istio Control Plane
&lt;/h3&gt;

&lt;p&gt;The Control Plane in Istio serves as the command center, overseeing the entire operation of your service mesh. It consists of three principal components, each playing a distinct role in ensuring your software's performance is orchestrated with precision.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fskv47737vqxd8gwqzgbp.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fskv47737vqxd8gwqzgbp.jpg" alt="Istio DP"&gt;&lt;/a&gt;&lt;/p&gt;
Image Credits: &lt;a href="https://istio.io/" rel="noopener noreferrer"&gt;Istio&lt;/a&gt;



&lt;p&gt;&lt;strong&gt;1. The Galley&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The galley component is Istio's configuration management, distribution and processing component. In a Hanz Zimmer show, there is a master choreographer backstage who is responsible for all the amazing dances on the stage. They may not show themselves on the stage, but they make sure that every dancer is ready and follows the script. Similarly, the galley receives the configuration file from the developer with instructions on how to configure the service mesh and hands it over to the next component; the Pilot. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The Pilot&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Istio Pilot is a critical component within the Istio service mesh architecture, serving as the navigation system that guides the various services through the ocean of microservices. The pilot receives the configuration from the Galley and then converts it to Envoy configurations and will distribute it to each of the Envoy proxies in the network. This is just like Zimmer ensures each musician in his orchestra knows when to play and how loud to be. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The Citadel&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Citadel plays a crucial role in Istio's security features, providing a robust infrastructure for securing communication within the service mesh. It provides each of these services with strong identities and generates certificates so that services can trust each other and communicate confidentially via mutual TLS (mTLS). In a Zimmer concert, this is something like the backstage pass that all the players get. The pass ensures that only authorized performers can access the backstage areas.&lt;/p&gt;

&lt;p&gt;To put all these things together, it can be illustrated 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7dxrheb04gao7box4nd9.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7dxrheb04gao7box4nd9.png" alt="Istio DP"&gt;&lt;/a&gt;&lt;/p&gt;
Image Credits: &lt;a href="https://istio.io/" rel="noopener noreferrer"&gt;Istio&lt;/a&gt;



&lt;p&gt;or something like &lt;a href="https://www.youtube.com/watch?v=KvdiMj8ei7Q" rel="noopener noreferrer"&gt;this&lt;/a&gt; :p &lt;/p&gt;

&lt;h3&gt;
  
  
  Why Istio?
&lt;/h3&gt;

&lt;p&gt;Istio ensures that organizations deliver decentralized applications at scale by simplifying network operations such as inter-service traffic management, encryption, authorization, troubleshooting, auditing, etc. &lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://www.solo.io/" rel="noopener noreferrer"&gt;solo.io&lt;/a&gt;, Istio provides the following capabilities beyond what plain Kubernetes provides:&lt;/p&gt;

&lt;p&gt;Quoting from &lt;a href="https://www.solo.io/" rel="noopener noreferrer"&gt;solo.io&lt;/a&gt;,&lt;/p&gt;

&lt;blockquote&gt;

1. &lt;b&gt;Cloud native application security&lt;/b&gt; — you can focus on application-level security with strong identity-driven authorization, authentication, and encryption.
&lt;br&gt;
&lt;br&gt;
2. &lt;b&gt;Efficient traffic management&lt;/b&gt;— achieve granular control over traffic and network behavior using rich routing rules, failover, retries, and error injection. In post-production testing, the Chaos Monkey integration allows SREs to inject faults and delays to improve resilience.
&lt;br&gt;
&lt;br&gt;
3. &lt;b&gt;Service mesh monitoring&lt;/b&gt; — Itsio provides service-level visibility so you can track, monitor, and troubleshoot issues. Without fine-grained details, bottlenecks take a long time to resolve. A service mesh makes it easy to disable failed services and replicas and maintain API responsiveness.
&lt;br&gt;
&lt;br&gt;
4. &lt;b&gt;Easy deployment with Kubernetes&lt;/b&gt; — Istio offers network controls and visibility for modern and traditional workloads, including virtual machines and containers.
&lt;br&gt;
&lt;br&gt;
5. &lt;b&gt;Simplified load balancing&lt;/b&gt; — advanced features can automate load balancing, provide client-based routing, and support canary deployments.
&lt;br&gt;
&lt;br&gt;
6. &lt;b&gt;Policy enforcement&lt;/b&gt; —a configuration API and policy layer help enforce policies with access control, quotas, and rate limits.
&lt;/blockquote&gt;

&lt;p&gt;That's about it folks! Hope y'all had a brief idea about what service mesh is and how Istio works. If you're wondering why Hans Zimmer was the highlight of this blog, please spare some time to watch &lt;a href="https://www.youtube.com/watch?v=8nfhHBJQXOY" rel="noopener noreferrer"&gt;Bastille, Hans Zimmer - Pompeii MMXXIII&lt;/a&gt;! It is amazing. &lt;/p&gt;

&lt;p&gt;If you have any questions or clarifications or have some extra points to add to this article, please drop them in the comments. I'm happy to read them and get back to you.&lt;br&gt;
Happy coding!&lt;/p&gt;

&lt;h3&gt;
  
  
  Further Reading
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=6zDrLvpfCK4" rel="noopener noreferrer"&gt;IBM Technology - Istio Service Mesh Explained&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.solo.io/topics/istio/istio-kubernetes" rel="noopener noreferrer"&gt;Solo.io - Why You Need Istio in K8s and a Quick Tutorial&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.solo.io/topics/istio/istio-architecture/" rel="noopener noreferrer"&gt;Solo.io - Istio architecture&lt;/a&gt;&lt;br&gt;
&lt;a href="https://istio.io/latest/docs/ops/deployment/architecture/" rel="noopener noreferrer"&gt;Istio - Architecture &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cover Image Credits: &lt;a href="https://boxboat.com" rel="noopener noreferrer"&gt;BoxBoat&lt;/a&gt;&lt;/p&gt;

</description>
      <category>istio</category>
      <category>kubernetes</category>
      <category>cloudnative</category>
      <category>servicemesh</category>
    </item>
    <item>
      <title>OIDC Authentication for Express Apps with Asgardeo</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Sat, 30 Apr 2022 03:15:41 +0000</pubDate>
      <link>https://dev.to/suvink/oidc-authentication-for-express-apps-with-asgardeo-5gbe</link>
      <guid>https://dev.to/suvink/oidc-authentication-for-express-apps-with-asgardeo-5gbe</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/suvink/add-authentication-to-your-express-app-with-asgardeo-13bh"&gt;previous article&lt;/a&gt;, I discussed how we can use &lt;a href="https://www.npmjs.com/package/@asgardeo/auth-node" rel="noopener noreferrer"&gt;Asgardeo Node SDK&lt;/a&gt; to add authentication to your Express app. As I mentioned before, the process of using the Node SDK in an Express app is not straightforward because the Node SDK is a generic SDK which is compatible across any Node JS platform. In this article, I will show you how you can use the Asgardeo Express SDK to add authentication to your Express app easily. &lt;/p&gt;

&lt;p&gt;TLDR;&lt;br&gt;
Try the official sample app &lt;a href="https://github.com/asgardeo/asgardeo-auth-express-sdk/samples" rel="noopener noreferrer"&gt;here&lt;/a&gt;.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Asgardeo organization. (You can create an organization upon registration at &lt;a href="https://asgardeo.io/signup" rel="noopener noreferrer"&gt;https://asgardeo.io/signup&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;A Standard Based Application - Once you create an organization in the Asgardeo Console, you can create a Standard Based Application from the dashboard. (&lt;a href="https://wso2.com/asgardeo/docs/guides/applications/register-standard-based-app/" rel="noopener noreferrer"&gt;More info&lt;/a&gt;) &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Projet Initialization&lt;/strong&gt;&lt;br&gt;
Create a new directory (a folder) and open the directory using your code editor. Open a terminal and type &lt;code&gt;yarn init --y&lt;/code&gt; and it will initialize a new project and create a &lt;code&gt;package.json&lt;/code&gt; file. &lt;/p&gt;

&lt;p&gt;The next step is to install the dependancies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add @asgardeo/auth-express cookie-parser express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add Nodemon as a dev dependancy to hot reload your changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add -D nodemon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you install Nodemon, modify the &lt;code&gt;package.json&lt;/code&gt; and add &lt;code&gt;"serve": "nodemon server.js"&lt;/code&gt; in the &lt;strong&gt;scripts&lt;/strong&gt; section so we can use Nodemon to run the server. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create an Express app&lt;/strong&gt;&lt;br&gt;
Create a new file and name it as &lt;code&gt;server.js&lt;/code&gt;.&lt;br&gt;
Then create a simple express server inside your &lt;code&gt;server.js&lt;/code&gt; file. You can use the code below.&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="s1"&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;cookieParser&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="s1"&gt;cookie-parser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;//Define the Port&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;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;//Initialize Express App&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="nf"&gt;cookieParser&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;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;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;Hello World&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;//Start the app and listen on the PORT&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 Started at PORT &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;After creating the server, run it using,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn run serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if you navigate to &lt;code&gt;http://localhost:5000&lt;/code&gt;, you should see a "Hello World" response from the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add configuration for Asgardeo Authentication&lt;/strong&gt;&lt;br&gt;
Create a new file in the directory and name it as &lt;code&gt;config.js&lt;/code&gt;. Inside this file, you need to specify the configuration for the Asgardeo Auth.&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="na"&gt;clientID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_CLIENT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_CLIENT_SECRET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_BASE_URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;appURL&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://localhost:5000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;scope&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;openid&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;profile&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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&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;Replace the &lt;code&gt;clientID&lt;/code&gt;, &lt;code&gt;clientSecret&lt;/code&gt; and &lt;code&gt;baseUrl&lt;/code&gt; from the values you get from &lt;a href="https://console.asgardeo.io/" rel="noopener noreferrer"&gt;Asgardeo Console&lt;/a&gt;. Since we run our server in &lt;code&gt;http://localhost:5000&lt;/code&gt;, we have to add it as the appURL. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
Make sure you add &lt;code&gt;localhost:5000&lt;/code&gt; as an allowed origin and add &lt;code&gt;localhost:5000/login&lt;/code&gt; as an authorized redirect URL in the console. We will discuss why should we authorize &lt;code&gt;localhost:5000/login&lt;/code&gt; endpoint specifically later on.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the AsgardeoAuth middleware&lt;/strong&gt;&lt;br&gt;
Open the &lt;code&gt;server.js&lt;/code&gt; file and import the middleware from the SDK.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsgardeoExpressClient&lt;/span&gt; &lt;span class="p"&gt;}&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;@asgardeo/auth-express&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;Then instantiate the Asgardeo Express client with the config we created earlier.&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;AsgardeoExpressClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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;Now you have to define three callback functions which are needed to initiate the middleware.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;onSignIn&lt;/code&gt; - Handle a successful user sign in.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onSignOut&lt;/code&gt; - Handle a successful user sign out.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onError&lt;/code&gt; - Handle errors that might occur in the authentication process.
&lt;/li&gt;
&lt;/ol&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;onSignIn&lt;/span&gt; &lt;span class="o"&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="nx"&gt;response&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;response&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;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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="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;Something went wrong&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;onSignOut&lt;/span&gt; &lt;span class="o"&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="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;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;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;Sign out successful&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;onError&lt;/span&gt; &lt;span class="o"&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="nx"&gt;error&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;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;400&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="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="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="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;Something went wrong&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;After defining the callback functions, use the middleware with the Express app we created.&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="nx"&gt;AsgardeoExpressClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asgardeoExpressAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onSignIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSignOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onError&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;That is all you have to do with the Express SDK to implement authentication. At this point, you can just navigate to &lt;code&gt;http://localhost:5000/login&lt;/code&gt; and authenticate yourself. That brings me to explain why we added &lt;code&gt;localhost:5000/login&lt;/code&gt; as an authorized redirect URL. The SDK itself is pre-configured to set the login endpoint to &lt;code&gt;/login&lt;/code&gt; and to set the logout endpoint to &lt;code&gt;/logout&lt;/code&gt;. So even though you didn't have to implement the logic for those 2 endpoints, you still need to authorize the endpoints in the console in order to function properly. &lt;/p&gt;

&lt;p&gt;In case you don't like the pre-configured login and logout paths, you can always override the defaults in the config file. All you have to do is add another property to the config object as follows.&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;config&lt;/span&gt; &lt;span class="o"&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;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;loginPath&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/customLogin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;logoutPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/customLogout&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;Don't forget to change the authorized redirect URL in the console if you're using a custom login and logout paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementing Protected Routes&lt;/strong&gt;&lt;br&gt;
In some use cases, you may want to protect a specific route with a login. To do this, Asgardeo Express SDK provides you with a simple middleware. All you have to do is, attach the &lt;code&gt;isAuthenticated&lt;/code&gt; middleware to the route you want to protect.&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;/protected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isAuthenticated&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="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;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from protected route!&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;a href="https://media.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%2Fwfs13mapwvg3abym3yqg.jpeg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfs13mapwvg3abym3yqg.jpeg" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Asgardeo Express SDK has many features in addition to the basic login functions I described above. You can check them out at &lt;a href="https://github.com/asgardeo/asgardeo-auth-express-sdk" rel="noopener noreferrer"&gt;Express SDK GitHub&lt;/a&gt; repo.  As always, feel free to reach me if you have any questions and you are welcome to &lt;a href="https://github.com/asgardeo/asgardeo-auth-express-sdk/blob/main/pull_request_template.md" rel="noopener noreferrer"&gt;make contributions&lt;/a&gt; to the SDK as well. Thanks for reading!  &lt;/p&gt;

</description>
      <category>javascript</category>
      <category>express</category>
      <category>wso2</category>
      <category>asgardeo</category>
    </item>
    <item>
      <title>Add authentication to your Express app with Asgardeo</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Fri, 22 Apr 2022 04:09:13 +0000</pubDate>
      <link>https://dev.to/suvink/add-authentication-to-your-express-app-with-asgardeo-13bh</link>
      <guid>https://dev.to/suvink/add-authentication-to-your-express-app-with-asgardeo-13bh</guid>
      <description>&lt;p&gt;When you are writing code for a project, at some point you're gonna have to come across user logins. To perform user specific tasks, to save user data and to do various kinds of stuff, user accounts is a mandatory feature in any system. But building a fully fledged login system is not so easy: especially having the flexibility to incorporate many kinds of login options (Email and password, Social Logins, Magic Links, etc) securely takes in a lot of effort. That is where &lt;a href="https://asgardeo.io/" rel="noopener noreferrer"&gt;Asgardeo&lt;/a&gt; by &lt;a href="https://wso2.com/" rel="noopener noreferrer"&gt;WSO2&lt;/a&gt; comes into play.&lt;/p&gt;

&lt;h4&gt;
  
  
  TLDR;
&lt;/h4&gt;

&lt;p&gt;Try the official sample app &lt;a href="https://github.com/asgardeo/asgardeo-auth-node-sdk/tree/main/samples" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Asgardeo is an IDaaS (Identity as a Service) empowering developers to implement secure and frictionless access. In a broader sense, it is a cloud-based solution that offers Identity and Access Management (IAM) capabilities to your applications easily. Now let's have a look at how we can integrate Asgardeo OIDC (Open ID Connect) authentication to our Express application. For that, we will be using Asgardeo Node SDK.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Asgardeo organization. (You can create an organization upon registration at &lt;a href="https://asgardeo.io/signup" rel="noopener noreferrer"&gt;https://asgardeo.io/signup&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;A Standard Based Application - Once you create an organization in the Asgardeo Console, you can create a Standard Based Application from the dashboard. (&lt;a href="https://wso2.com/asgardeo/docs/guides/applications/register-standard-based-app/" rel="noopener noreferrer"&gt;More info&lt;/a&gt;)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Projet Initialization&lt;/strong&gt;&lt;br&gt;
Let's get started. First and foremost you're gonna have to create a new folder (or a directory) and initialize a new project. You can do that with &lt;code&gt;yarn init --y&lt;/code&gt; and it will create a new &lt;code&gt;package.json&lt;/code&gt; file for you. Now open your folder in your favourite code editor and we're ready to proceed.&lt;/p&gt;

&lt;p&gt;Now you have to install all the dependancies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add @asgardeo/auth-node cookie-parser express uuid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To live reload the changes as we do, we need &lt;code&gt;nodemon&lt;/code&gt; as a dev dependancy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add -D nodemon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing, add the following to your &lt;code&gt;package.json&lt;/code&gt; file. (If you already have a scripts block, you can replace it)&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="nl"&gt;"scripts"&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;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"echo &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Error: no test specified&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; exit 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"serve"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nodemon server.js"&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;&lt;strong&gt;Implement an Express app&lt;/strong&gt;&lt;br&gt;
Create a new file and name it as &lt;code&gt;server.js&lt;/code&gt;. (You can choose whatever the name you want :p but make sure you change the name in the &lt;code&gt;package.json&lt;/code&gt; as well!)&lt;br&gt;
Then create a simple express server inside your &lt;code&gt;server.js&lt;/code&gt; file. Something similar to 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="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="s1"&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;cookieParser&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="s1"&gt;cookie-parser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;//Define the Port&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;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;//Initialize Express App&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="nf"&gt;cookieParser&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;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;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;Hello World&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;//Start the app and listen on the PORT&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 Started at PORT &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;Run the server with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn run serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if you navigate to &lt;code&gt;localhost:5000&lt;/code&gt;, you should see a "Hello World".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrate Asgardeo Authentication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now that we have a working Express app, let's integrate Asgardeo authentication. First you need to create a new file and name it to &lt;code&gt;config.js&lt;/code&gt;.&lt;br&gt;
Inside that file, you need to specify the configuration for Asgardeo initialization.&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clientID&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;YOUR_CLIENT_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;clientSecret&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;YOUR_CLIENT_SECRET&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;baseUrl&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;YOUR_BASE_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;signInRedirectURL&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;http://localhost:5000/login&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;signOutRedirectURL&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;http://localhost:5000&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;scope&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;openid&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;profile&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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&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;Replace the &lt;code&gt;clientID&lt;/code&gt;, &lt;code&gt;clientSecret&lt;/code&gt; and &lt;code&gt;baseUrl&lt;/code&gt; from the values you get from &lt;a href="https://console.asgardeo.io/" rel="noopener noreferrer"&gt;Asgardeo Console&lt;/a&gt;. The &lt;code&gt;signInRedirectURL&lt;/code&gt; is the one a user should be redirected after the login. Keep in mind that the login endpoint and &lt;code&gt;signInRedirectURL&lt;/code&gt; should be the same (You'll see why later).&lt;br&gt;
Also make sure you add &lt;code&gt;localhost:5000&lt;/code&gt; as an allowed origin and add &lt;code&gt;signInRedirectURL&lt;/code&gt; as an authorized redirect URL in the console.&lt;/p&gt;

&lt;p&gt;Now open the &lt;code&gt;server.js&lt;/code&gt; file and import the Asgardeo Node SDK and our config file. (Note: As an additional package, we need the &lt;a href="https://www.npmjs.com/package/uuid" rel="noopener noreferrer"&gt;uuid&lt;/a&gt; package as well.)&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsgardeoNodeClient&lt;/span&gt; &lt;span class="p"&gt;}&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="s1"&gt;@asgardeo/auth-node&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="na"&gt;v4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uuidv4&lt;/span&gt; &lt;span class="p"&gt;}&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;uuid&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;config&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="s1"&gt;./config&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;Then initialize the AsgardeoNodeClient with the configuration.&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;authClient&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;AsgardeoNodeClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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;After that, we can move onto implementing the login endpoint.&lt;br&gt;
Earlier, we declared our &lt;code&gt;signInRedirectURL&lt;/code&gt; in the config file to a &lt;code&gt;/login&lt;/code&gt; endpoint. So we'll implement that endpoint here.&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;/login&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the route handler, first you need to check if the incoming request is already authenticated or not. To do this, we can check the &lt;code&gt;ASGARDEO_SESSION_ID&lt;/code&gt; cookie. If the request has a session ID, we can use that to re-authenticate the user and if not, we can create a new session ID and assign it to the request.&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;let&lt;/span&gt; &lt;span class="nx"&gt;userID&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;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASGARDEO_SESSION_ID&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="o"&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="nx"&gt;userID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;uuidv4&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;After that, you have to implement a callback function which will be used to redirect the user to the authorization URL. At the same time, you can send the user ID as a cookie back to the user. Make sure you use the httpOnly and sameSite attributes to prevent your cookie from cross-site request forgery (CSRF) attacks.&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;redirectCallback&lt;/span&gt; &lt;span class="o"&gt;=&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="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;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="c1"&gt;//Make sure you use the httpOnly and sameSite attributes to prevent from cross-site request forgery (CSRF) attacks.&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;cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ASGARDEO_SESSION_ID&lt;/span&gt;&lt;span class="dl"&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;httpOnly&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;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lax&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="nx"&gt;url&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="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 function will take in the authorization URL string as a parameter and will redirect the user. Now we will use the &lt;code&gt;signIn&lt;/code&gt; function from the SDK to authorize 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="nx"&gt;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirectCallback&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;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="nx"&gt;code&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;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session_state&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;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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;//Just to see whats going on&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="nx"&gt;response&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idToken&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;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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;Now let's figure out why we added the &lt;code&gt;signInRedirectURL&lt;/code&gt; in the config file as same as the login endpoint (&lt;code&gt;/login&lt;/code&gt;). The &lt;code&gt;signIn&lt;/code&gt; method accepts 5 parameters namely redirectCallback, userID, code, session_state and state. When a user logs in for the first time, the &lt;code&gt;/login&lt;/code&gt; endpoint will not get any values for &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;session_state&lt;/code&gt; and &lt;code&gt;state&lt;/code&gt; even though the &lt;code&gt;signIn&lt;/code&gt; function expects the URL to have them. If those parameters are not present, the SDK will generate an authorization URL and passes it down to the &lt;code&gt;redirectCallback&lt;/code&gt; function. From there, the user will be redirected to the Asgardeo Login Page and will securely authenticated. After doing so, as per our config file, the user will be redirected into the &lt;code&gt;signInRedirectURL&lt;/code&gt;. This redirection will have the &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;session_state&lt;/code&gt; and &lt;code&gt;state&lt;/code&gt; URL parameters. Now the SDK knows this user has been redirected from the Asgardeo Login and will validate the &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;session_state&lt;/code&gt; and &lt;code&gt;state&lt;/code&gt;, then create a session in the Express app against the &lt;code&gt;userID&lt;/code&gt;. There is a complicated process under the hood, but the user just have to call &lt;code&gt;/login&lt;/code&gt; endpoint once and the SDK will handle the flow smoothly.&lt;/p&gt;

&lt;p&gt;The final code block for the &lt;code&gt;/login&lt;/code&gt; endpoint will look 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="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;/login&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;let&lt;/span&gt; &lt;span class="nx"&gt;userID&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;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASGARDEO_SESSION_ID&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="o"&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="nx"&gt;userID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;uuidv4&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;redirectCallback&lt;/span&gt; &lt;span class="o"&gt;=&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="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;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="c1"&gt;//Make sure you use the httpOnly and sameSite attributes to prevent from cross-site request forgery (CSRF) attacks.&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;cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ASGARDEO_SESSION_ID&lt;/span&gt;&lt;span class="dl"&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;httpOnly&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;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lax&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="nx"&gt;url&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="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirectCallback&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;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="nx"&gt;code&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;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session_state&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;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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="nx"&gt;response&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idToken&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;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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;Now you have a working login endpoint for your users 🥳. Let's add a log out endpoint as well.&lt;/p&gt;

&lt;p&gt;Logging out is rather easy than implementing the log in. We'll start with defining a new route.&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;/logout&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the logout logic, first we need to check if the session ID cookie is present in the request. If so, we know this user has already been authenticated.&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASGARDEO_SESSION_ID&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;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;Unauthenticated&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;If the cookie is not present, we reject the logout request. But if it's present, we need to logout the user and delete the session.&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;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signOut&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;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASGARDEO_SESSION_ID&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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;//Invalidate the session cookie&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;cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ASGARDEO_SESSION_ID&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;maxAge&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;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="nx"&gt;response&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;err&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="nx"&gt;err&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="nx"&gt;err&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 can extract the session ID from the cookie and then pass it on to &lt;code&gt;signOut&lt;/code&gt; function. It will identify the relevant user's session and will destroy accordingly. After that, we need to invalidate the cookie. This can be done by setting the value to &lt;code&gt;null&lt;/code&gt; sending back. So the code block for &lt;code&gt;/logout&lt;/code&gt; endpoint will look 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="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;/logout&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="k"&gt;if &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;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASGARDEO_SESSION_ID&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;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;Unauthenticated&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signOut&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;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASGARDEO_SESSION_ID&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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;//Invalidate the session cookie&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;cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ASGARDEO_SESSION_ID&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;maxAge&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;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="nx"&gt;response&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;err&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="nx"&gt;err&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="nx"&gt;err&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;&lt;a href="https://media.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%2Foe8tduszii0krnklvqtw.jpeg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foe8tduszii0krnklvqtw.jpeg" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations now you have implemented &lt;a class="mentioned-user" href="https://dev.to/asgardeo"&gt;@asgardeo&lt;/a&gt; authentication to your Express app. The Asgardeo Node SDK comes up with more functions that will be useful to implement your next project so do check them out as well. Please check the official sample app &lt;a href="https://github.com/asgardeo/asgardeo-auth-node-sdk/tree/main/samples" rel="noopener noreferrer"&gt;here&lt;/a&gt; and you can try out the SDK including the functions we coded earlier in this article. That's it for now, thanks for reading. See you folks soon! &lt;/p&gt;

</description>
      <category>express</category>
      <category>javascript</category>
      <category>asgardeo</category>
      <category>oauth20</category>
    </item>
    <item>
      <title>Easily add certifications to LinkedIn from your website.</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Sat, 18 Dec 2021 14:05:23 +0000</pubDate>
      <link>https://dev.to/suvink/easily-add-certifications-to-linkedin-from-your-website-550j</link>
      <guid>https://dev.to/suvink/easily-add-certifications-to-linkedin-from-your-website-550j</guid>
      <description>&lt;p&gt;A few weeks back &lt;a class="mentioned-user" href="https://dev.to/lakindu"&gt;@lakindu&lt;/a&gt; and I were building a platform for &lt;a href="https://academy.richmondlive.lk" rel="noopener noreferrer"&gt;@Richmond Live&lt;/a&gt; 's new training program. The students were given a digital certificate for the courses they completed at the Richmond Live's academy and once they log in, they can share their certificate across social media platforms or simply download the PDF certificate. In addition to that, students can maintain their public profile which can be used as a portfolio for their achievements at the academy. &lt;/p&gt;

&lt;p&gt;While building this platform, we needed a feature to have a button that will add the certification directly to LinkedIn. Checked the official LinkedIn documentation but to keep things simple, we wanted a small NPM package that can do the job. Unfortunately we couldn't find anything that is flexible and stable enough to use in out application. We ended up using the hard way; building the button from the scratch.&lt;/p&gt;

&lt;p&gt;After a few days we decided to create a package ourselves for people to use easily. So &lt;a href="https://www.npmjs.com/package/linkedin-certification" rel="noopener noreferrer"&gt;&lt;code&gt;Linkedin-certification&lt;/code&gt;&lt;/a&gt; was born!&lt;/p&gt;

&lt;h3&gt;
  
  
  What does it do?
&lt;/h3&gt;

&lt;p&gt;It will let you to add your certifications directly into LinkedIn from your website. (&lt;em&gt;Not directly! You still need to confirm the details :wink&lt;/em&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  How to install?
&lt;/h3&gt;

&lt;p&gt;If you're 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 plaintext"&gt;&lt;code&gt;npm install linkedin-certification --save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or else if you're a &lt;code&gt;yarn&lt;/code&gt; user,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add linkedin-certification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to use it?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;First, import the package
&lt;/li&gt;
&lt;/ul&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;LinkedinCertification&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;linkedin-certification&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;ul&gt;
&lt;li&gt;Then use the imported component with your certification details.
&lt;/li&gt;
&lt;/ul&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;LinkedinCertification&lt;/span&gt;
  &lt;span class="nx"&gt;certificationName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Sample Certification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;organizationName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Sample Organization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;issuedMonth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;issuedYear&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2021&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;expirationMonth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;expirationYear&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2023&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;certificateId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;29c2e87e-5c1c-11ec-bf63-0242ac130002&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;certificateURL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.google.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;newTab&lt;/span&gt;&lt;span class="o"&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="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;Thats it! You can see a button like this on your application upon saving.&lt;br&gt;
&lt;a href="https://camo.githubusercontent.com/46b14f0a5c9408a8b9ad5e66166806390290bae8ef580a47cdbdcf54c565cb41/68747470733a2f2f692e696d6775722e636f6d2f6c336d3432614e2e706e67" class="article-body-image-wrapper"&gt;&lt;img src="https://camo.githubusercontent.com/46b14f0a5c9408a8b9ad5e66166806390290bae8ef580a47cdbdcf54c565cb41/68747470733a2f2f692e696d6775722e636f6d2f6c336d3432614e2e706e67"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The property list
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;certificationName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The name of the certification. &lt;code&gt;[Required]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;organizationName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The name of the organizaion. &lt;code&gt;[Required]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;issuedMonth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Certification issued month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;issuedYear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Certification issued year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expirationMonth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Certification expiry month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expirationYear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Certification expiry year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;certificateId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unique ID of the certification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;certificateURL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL for the certification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;newTab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;true&lt;/code&gt; to open the LinkedIn Certification window in a new tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customButton&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;React Node&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lets you pass your own custom button.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  YES you saw it right!
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;linkedin-certification&lt;/code&gt; lets you add your custom buttons instead of the default one that comes with the package! Got a really cool button with a bunch of animations? Or need a button that goes with your website theme? We got you! Use the &lt;code&gt;customButton&lt;/code&gt; property and pass down your own button component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contributions are welcome!
&lt;/h3&gt;

&lt;p&gt;If you come across any bugs or feature requests, please &lt;a href="https://github.com/Suvink/linkedin-certification/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; or send a pull request. I'm happy to improve this with your cool ideas!&lt;/p&gt;

&lt;h2&gt;
  
  
  Help me more!
&lt;/h2&gt;

&lt;p&gt;If you like this package, please consider buying me a coffee so that I can keep improving this even further! &lt;br&gt;
&lt;a href="https://www.buymeacoffee.com/suvink" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.buymeacoffee.com%2Fbuttons%2Fv2%2Fdefault-yellow.png" alt="Buy Me A Coffee"&gt;&lt;/a&gt;&lt;/p&gt;

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

</description>
      <category>react</category>
      <category>npm</category>
      <category>typescript</category>
      <category>linkedin</category>
    </item>
    <item>
      <title>E-Waste Admin Panel</title>
      <dc:creator>Suvin Nimnaka</dc:creator>
      <pubDate>Thu, 07 Jan 2021 16:02:11 +0000</pubDate>
      <link>https://dev.to/suvink/e-waste-admin-panel-2den</link>
      <guid>https://dev.to/suvink/e-waste-admin-panel-2den</guid>
      <description>&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Admin panel shows you an overview of how the system works. It provides realtime numbers and charts so you can have an idea of how the system performs at a glance. Also these charts are downloadable so it is easy to make reports with these. I'm using &lt;code&gt;chart.js&lt;/code&gt; to create these charts. It was easy to integrate and provides a good looking customizable charts. &lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fl1agsja85eyyepemffm2.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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fl1agsja85eyyepemffm2.png"&gt;&lt;/a&gt;&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffzwj63izg6jjqrprinoy.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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffzwj63izg6jjqrprinoy.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have any concerns or suggestions for improvements, please let us know!&lt;/p&gt;

</description>
      <category>dohackathon</category>
      <category>showdev</category>
      <category>vue</category>
    </item>
  </channel>
</rss>
