<?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: Vincent Olagbemide</title>
    <description>The latest articles on DEV Community by Vincent Olagbemide (@vincentayorinde).</description>
    <link>https://dev.to/vincentayorinde</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1786496%2F8d1ab131-7933-4a6a-baf5-0fa4ec56dbce.jpg</url>
      <title>DEV Community: Vincent Olagbemide</title>
      <link>https://dev.to/vincentayorinde</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vincentayorinde"/>
    <language>en</language>
    <item>
      <title>How 23,000 Repos Got Their Secrets Stolen Through Their Own CI/CD Pipeline</title>
      <dc:creator>Vincent Olagbemide</dc:creator>
      <pubDate>Sun, 31 May 2026 21:54:43 +0000</pubDate>
      <link>https://dev.to/vincentayorinde/how-23000-repos-got-their-secrets-stolen-through-their-own-cicd-pipeline-2nnh</link>
      <guid>https://dev.to/vincentayorinde/how-23000-repos-got-their-secrets-stolen-through-their-own-cicd-pipeline-2nnh</guid>
      <description>&lt;p&gt;Been thinking about writing this one for a while. Supply chain attacks against CI/CD pipelines have been picking up pace over the past two years and the March 2025 tj-actions incident was the one that finally made me sit down and document everything properly. This is how I think about hardening GitHub Actions pipelines and what I actually do in practice. Original is on my blog but happy to have the conversation here too.&lt;/p&gt;




&lt;p&gt;On a regular Tuesday morning, your engineering team pushes code, the pipeline runs like it always does, and somewhere in those automated logs, your AWS access keys, your GitHub tokens, your RSA private keys, are being quietly printed out and collected by someone you have never met. You are not notified. No alarm goes off. GitHub does not send an email. Your pipeline shows green.&lt;/p&gt;

&lt;p&gt;That is not a hypothetical. That is exactly what happened to over 23,000 teams in March 2025.&lt;/p&gt;

&lt;p&gt;I have spent years working at the intersection of software engineering and security, where pipelines were moving over a billion dollars in monetary transactions, and more recently building &lt;a href="https://nexloy.dev" rel="noopener noreferrer"&gt;Nexloy&lt;/a&gt;, a self-hosted deployment platform where I had to make every security decision from scratch. What I am about to share is the pattern I have watched attackers exploit again and again, the mistakes I have seen brilliant teams make, and the seven specific things that actually work when you need to lock down a GitHub Actions pipeline.&lt;/p&gt;

&lt;p&gt;By the end of this article you will understand exactly how these attacks happen, why your pipeline is more exposed than you probably think, and what you can do about it today. Some of these changes take five minutes. Some take an afternoon. All of them matter.&lt;/p&gt;

&lt;p&gt;Stay with me.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Attack That Woke the Industry Up
&lt;/h2&gt;

&lt;p&gt;In March 2025, engineering teams at over 23,000 companies opened their workflow logs and found something horrifying. Their most sensitive credentials were sitting there, printed in plain text, for anyone with access to those logs to read. We are talking about AWS (Amazon Web Services) access keys**, **GitHub PATs (Personal Access Tokens, which are like passwords that give programmatic access to your codebase), RSA private keys (cryptographic keys used to prove identity and encrypt data), and **npm tokens (credentials for publishing software packages).&lt;/p&gt;

&lt;p&gt;The cause was not a sophisticated zero-day exploit. It was not a nation-state attacker breaching GitHub's own servers. It was something almost embarrassingly simple. A popular open-source GitHub Action called &lt;code&gt;tj-actions/changed-files&lt;/code&gt; had been quietly compromised.&lt;/p&gt;

&lt;p&gt;Here is how it worked. The attackers gained access to the repository hosting that Action and then did something sneaky. They went back and updated the version tags, which are labels like &lt;code&gt;@v35&lt;/code&gt; or &lt;code&gt;@v44&lt;/code&gt; that teams use to reference a specific version, to point at their malicious code instead of the original. No announcements. No pull requests. No alerts. Just a silent swap. Every team whose pipeline said "use version 44 of this Action" was now running the attackers' code, and that code had one job: find every secret in your pipeline environment and write it to the logs.&lt;/p&gt;

&lt;p&gt;This vulnerability was assigned &lt;strong&gt;CVE-2025-30066&lt;/strong&gt;. CVE stands for Common Vulnerabilities and Exposures, which is the official tracking system for publicly disclosed security flaws. It became the largest GitHub Actions supply chain attack on record.&lt;/p&gt;

&lt;p&gt;But here is the detail I want you to hold onto, because it is the heartbeat of this entire article. &lt;strong&gt;The teams that had pinned their Actions to a specific commit SHA were not affected.&lt;/strong&gt; A commit SHA (Secure Hash Algorithm) is a unique cryptographic fingerprint of the exact code they had reviewed, and it cannot be faked or silently moved. One configuration decision separated the impacted from the protected.&lt;/p&gt;

&lt;p&gt;That single fact changed how I think about pipeline security. Let me explain why, and then walk you through the seven things you can do to protect your own systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Was Not a One-Off
&lt;/h2&gt;

&lt;p&gt;Before I get into the solutions, I want to make sure you understand the scale of what is happening, because CVE-2025-30066 was a headline but it was not an outlier.&lt;/p&gt;

&lt;p&gt;In December 2024, Ultralytics, the company behind the &lt;strong&gt;YOLO (You Only Look Once)&lt;/strong&gt; computer vision library, was hit by an almost identical attack. YOLO is one of the most widely used AI projects in the world with nearly 60 million downloads on &lt;strong&gt;PyPI (the Python Package Index, which is the main place people download Python libraries from)&lt;/strong&gt;. Attackers exploited a misconfigured workflow trigger called &lt;code&gt;pull_request_target&lt;/code&gt; to run their own code inside Ultralytics' pipeline. The result was a &lt;strong&gt;cryptominer&lt;/strong&gt;, which is software that secretly uses your computer's resources to generate cryptocurrency for someone else, bundled into four official releases and shipped to users around the world.&lt;/p&gt;

&lt;p&gt;In February 2026, Trivy, Aqua Security's own open-source security scanning tool used by thousands of organisations to find vulnerabilities in their pipelines, had its GitHub Actions workflow exploited to steal an organisation-wide PAT (Personal Access Token) that had access to 33 internal workflows. The tool had been quietly vulnerable since October 2025. A separate scanner had even flagged the issue in November 2025, three months before an attacker found it first.&lt;/p&gt;

&lt;p&gt;When a security company's own security tool gets compromised through its &lt;strong&gt;CI/CD (Continuous Integration and Continuous Deployment)&lt;/strong&gt; pipeline, that tells you something important. This is not about negligence or inexperience. It is about a systemic gap in how the industry thinks about pipeline security.&lt;/p&gt;

&lt;p&gt;Across all of these incidents and others like them, three root causes appear again and again:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Actions pinned to tags instead of commit SHAs&lt;/strong&gt;, because tags can be silently changed to point anywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Misuse of the &lt;code&gt;pull_request_target&lt;/code&gt; trigger&lt;/strong&gt;, a workflow setting that accidentally grants fork contributors access to internal secrets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overly permissive &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; scopes&lt;/strong&gt;, where the automatic credential GitHub issues to every workflow is far more powerful than it needs to be&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now let us talk about how to fix them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why GitHub Actions Is Such an Attractive Target
&lt;/h2&gt;

&lt;p&gt;GitHub Actions is genuinely brilliant. It lets you automate almost anything. You can run tests, build Docker containers, deploy to cloud servers, and send notifications, all triggered by events in your repository. Its marketplace has thousands of pre-built Actions, which are reusable automation components contributed by the community.&lt;/p&gt;

&lt;p&gt;That same ecosystem is also what makes it dangerous.&lt;/p&gt;

&lt;p&gt;When you add a line like &lt;code&gt;uses: some-org/some-action@v2&lt;/code&gt; to your workflow file, you are doing something that should give you pause. You are downloading and executing code from the internet, written by someone you have probably never met, inside an environment that has access to every secret your pipeline uses. Your cloud credentials. Your deployment keys. Your database passwords.&lt;/p&gt;

&lt;p&gt;Most teams apply far less scrutiny to this than they would to adding a new &lt;strong&gt;npm (Node Package Manager, the tool used to install JavaScript libraries)&lt;/strong&gt; dependency. And the npm ecosystem has had its own supply chain disasters. At least with npm you tend to look at the package before running it.&lt;/p&gt;

&lt;p&gt;There is another layer to this. GitHub Actions automatically tries to redact known secrets from your workflow logs, but that protection only works if GitHub knows what your secrets are. The moment an attacker controls the code running inside your pipeline, they can extract secrets and send them outward in ways that bypass that redaction entirely.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, which is the automatically generated credential that every workflow receives, is another underappreciated risk. In many repositories, this token defaults to write permissions across the entire repository. A compromised workflow with that token can push code to your main branch, modify releases, or quietly alter your deployment scripts. Most developers I have spoken to have never looked at what their &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; is actually allowed to do.&lt;/p&gt;

&lt;p&gt;And then there is &lt;code&gt;pull_request_target&lt;/code&gt;. This is a workflow trigger created to allow workflows to safely respond to pull requests from forks, which are copies of your repository made by outside contributors. The problem is it runs in the context of the base repository, meaning it has access to your secrets and write permissions, even when the code triggering it came from a complete stranger's fork. This is the root cause of the Ultralytics attack, the Trivy attack, and the initial phase of the &lt;code&gt;tj-actions&lt;/code&gt; compromise that started by targeting Coinbase.&lt;/p&gt;




&lt;h2&gt;
  
  
  7 Hardening Techniques That Actually Work
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Pin Actions to Commit SHAs, Not Tags
&lt;/h3&gt;

&lt;p&gt;I will say this plainly. If you take only one thing from this article, let it be this.&lt;/p&gt;

&lt;p&gt;A tag like &lt;code&gt;@v4&lt;/code&gt; is just a label. Anyone with write access to that repository can move it to point at completely different code without telling you. A commit SHA is a unique fingerprint generated from the exact contents of the code at a specific point in time. It cannot be faked or silently moved. Once you pin to a SHA, you are guaranteed to always run exactly the code you reviewed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before, vulnerable to silent tag manipulation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tj-actions/changed-files@v44&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After, locked to exact verified code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683&lt;/span&gt; &lt;span class="c1"&gt;# v4.2.2&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tj-actions/changed-files@d6e91a0f7b4c9b8e5a3d2c1f0e9d8c7b6a5f4e3d&lt;/span&gt; &lt;span class="c1"&gt;# v44.5.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The human-readable comment next to the SHA is important. It tells future you and your colleagues what version this is supposed to be. Use tools like &lt;a href="https://docs.github.com/en/code-security/dependabot" rel="noopener noreferrer"&gt;Dependabot&lt;/a&gt; or &lt;a href="https://docs.renovateapp.com" rel="noopener noreferrer"&gt;Renovate&lt;/a&gt; to automate the process of updating these SHAs when new legitimate versions are released.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This single change is what separated the protected teams from the 23,000 affected by CVE-2025-30066.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Use OIDC Instead of Long-Lived Stored Secrets
&lt;/h3&gt;

&lt;p&gt;For a long time, the standard way to give your pipeline access to cloud services like AWS or GCP (Google Cloud Platform) was to create a set of credentials, copy them into GitHub's secrets manager, and have your workflow read them out at runtime.&lt;/p&gt;

&lt;p&gt;The problem is that those credentials are static. They do not expire on their own. If they are ever leaked through a compromised Action, a misconfigured log, or a breach of GitHub itself, they remain valid until someone manually goes in and rotates them. In a security incident, that window can be hours or days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OIDC (OpenID Connect)&lt;/strong&gt;, which is an open standard for authentication that allows systems to verify identity without exchanging long-lived passwords, solves this. Instead of storing credentials, your workflow requests a short-lived token from GitHub that proves who it is. Your cloud provider is configured to trust that token and exchange it for temporary credentials that expire when the job ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before, static credentials stored in GitHub secrets:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS credentials&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;aws-access-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;aws-secret-access-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After, OIDC authentication with no stored credentials needed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;   &lt;span class="c1"&gt;# Allow the workflow to request an OIDC token&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS credentials via OIDC&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@e3dd6a429d7300a6542e0d4c84cc4db732e71e04&lt;/span&gt; &lt;span class="c1"&gt;# v4.0.2&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::123456789012:role/github-actions-role&lt;/span&gt;
      &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if an attacker somehow captured that OIDC token mid-workflow, it is worthless the moment the job finishes. There is nothing to steal from your secrets manager because there is nothing stored there to steal. This is the same principle I applied when building the GitHub integration layer in &lt;a href="https://nexloy.dev" rel="noopener noreferrer"&gt;Nexloy&lt;/a&gt;, where encrypted GitHub OAuth tokens are stored rather than long-lived deploy keys sitting in plain text.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Restrict GITHUB_TOKEN to the Minimum It Needs
&lt;/h3&gt;

&lt;p&gt;Think of the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; like a master key that GitHub automatically hands to every workflow when it starts. In many default configurations, that key has write access to your entire repository. A compromised workflow with that key can push to your main branch, publish releases, and modify your deployment configuration, all without your knowledge.&lt;/p&gt;

&lt;p&gt;The fix is to start from zero permissions and then explicitly add back only what each job actually needs.&lt;/p&gt;

&lt;p&gt;First, go to your repository settings under &lt;strong&gt;Settings → Actions → General → Workflow permissions&lt;/strong&gt; and set it to read-only as the default.&lt;/p&gt;

&lt;p&gt;Then in your workflow files, declare permissions explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Lock down the whole workflow by default, nothing is permitted&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;    &lt;span class="c1"&gt;# Only needs to read code&lt;/span&gt;
      &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;   &lt;span class="c1"&gt;# Needs to push a container to the registry&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;   &lt;span class="c1"&gt;# Needs to request an OIDC token&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# build steps here...&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;    &lt;span class="c1"&gt;# This job genuinely only needs to read code&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# deploy steps here...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you do this, even if one job in your workflow is compromised, the attacker is limited to what that specific job was permitted to do. A breach of your build job cannot automatically become a write to your main branch.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Treat Workflow Files Like Production Code, Because They Are
&lt;/h3&gt;

&lt;p&gt;Here is something I have noticed over the years. Engineering teams will require two or three reviewers to approve any change to their core application code. But a change to a &lt;code&gt;.github/workflows/&lt;/code&gt; file, which controls what runs in their entire CI/CD pipeline, sometimes gets merged with a single click.&lt;/p&gt;

&lt;p&gt;That inconsistency is a problem. &lt;strong&gt;Workflow files are your production infrastructure.&lt;/strong&gt; A change to a workflow file can introduce a new third-party Action, relax permissions, or add a new deployment step. It deserves the same scrutiny as your application code.&lt;/p&gt;

&lt;p&gt;The simplest way to enforce this is through &lt;strong&gt;CODEOWNERS&lt;/strong&gt;, a GitHub feature that lets you specify which team must review changes to specific files or directories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight codeowners"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/CODEOWNERS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c1"&gt;# Any changes to workflow files require review from the security team&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;/.github/workflows/&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;@your-org/security-team&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@your-org/platform-engineering&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;/.github/actions/&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nf"&gt;@your-org/security-team&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place, no workflow change, including the introduction of a new third-party Action, can be merged without a review from someone who knows what to look for.&lt;/p&gt;

&lt;p&gt;Additionally, use GitHub's &lt;strong&gt;Environments&lt;/strong&gt; feature to add a human approval gate before any workflow deploys to production. This means even a fully compromised workflow has to stop and wait for a real human to click approve, which is often enough time to spot the anomaly.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Use Automated Tools to Scan Your Workflows
&lt;/h3&gt;

&lt;p&gt;Manual review of workflow files is valuable but does not scale. Add automated scanning to your security toolchain so problems are caught before they ever reach your main branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/woodruffw/zizmor" rel="noopener noreferrer"&gt;Zizmor&lt;/a&gt;&lt;/strong&gt; is a static analysis tool built specifically for GitHub Actions. It reads your workflow files and flags issues like dangerous use of &lt;code&gt;pull_request_target&lt;/code&gt;, script injection vulnerabilities (places where untrusted input could be interpreted as code), and overly permissive token configurations:&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;# Run it locally to audit your workflows right now&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;zizmor
zizmor .github/workflows/

&lt;span class="c"&gt;# Or add it as a step inside your CI pipeline itself&lt;/span&gt;
- name: Scan workflows with Zizmor
  uses: woodruffw/zizmor-action@db2cb61655abc17c8f8f00bd9e3d13b91cba0ef9 &lt;span class="c"&gt;# v1.5.0&lt;/span&gt;
  with:
    args: &lt;span class="nt"&gt;--format&lt;/span&gt; sarif .github/workflows/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/step-security/harden-runner" rel="noopener noreferrer"&gt;StepSecurity Harden Runner&lt;/a&gt;&lt;/strong&gt; takes a different approach. Instead of analysing your workflow files, it monitors what your workflow actually does at runtime. It can detect and block unexpected outbound network connections from your pipeline steps, the kind of monitoring that would have caught the &lt;code&gt;tj-actions&lt;/code&gt; attack sending your secrets to an attacker's server while your pipeline was still mid-run.&lt;/p&gt;

&lt;p&gt;The principle here is the same as application security. It is always better to fail at CI than to fail in production.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Mirror Critical Actions and Use a Private Container Registry
&lt;/h3&gt;

&lt;p&gt;For environments where the stakes are high, such as fintech, healthcare, or critical infrastructure, depending on public GitHub Actions is an unnecessary risk. The solution is to fork the Actions you depend on into your own organisation's repositories, review the code, and pin to SHAs within your fork. That way you are not dependent on an external maintainer's account security.&lt;/p&gt;

&lt;p&gt;For container images, which are the self-contained software packages that most modern deployments are built around, use a private registry like &lt;strong&gt;&lt;a href="https://goharbor.io" rel="noopener noreferrer"&gt;Harbor&lt;/a&gt;&lt;/strong&gt; instead of pulling from Docker Hub or public &lt;strong&gt;GHCR (GitHub Container Registry)&lt;/strong&gt; at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Build and push your image to your own Harbor instance&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push container image&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@...&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;harbor.internal.yourdomain.com/myapp:${{ github.sha }}&lt;/span&gt;

&lt;span class="c1"&gt;# In your Dockerfile, reference your Harbor instance, not Docker Hub&lt;/span&gt;
&lt;span class="s"&gt;FROM harbor.internal.yourdomain.com/base/node:20-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When every image flows through your own Harbor instance, you can enforce signing, which is cryptographic proof that the image came from your build pipeline and has not been tampered with. Nothing gets deployed unless it was built and approved through your controlled process.&lt;/p&gt;




&lt;h3&gt;
  
  
  7. Enforce Branch Protection Rules and Deployment Gates
&lt;/h3&gt;

&lt;p&gt;Every security measure I have described so far is most effective as one layer in a defence-in-depth approach, meaning multiple independent barriers that an attacker would need to defeat simultaneously.&lt;/p&gt;

&lt;p&gt;Branch protection rules and environment gates are your last line of defence. Even if a workflow is compromised, these controls can stop it from doing lasting damage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In your deployment workflow&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy-production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;   &lt;span class="c1"&gt;# This triggers a mandatory approval gate&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to production&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./deploy.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure your production environment in GitHub settings to require &lt;strong&gt;manual approval&lt;/strong&gt; from a named person or team before the deployment proceeds, a &lt;strong&gt;deployment branch restriction&lt;/strong&gt; so only your main branch can deploy to production, and a &lt;strong&gt;wait timer&lt;/strong&gt; for especially sensitive operations that gives you time to cancel if something looks wrong.&lt;/p&gt;

&lt;p&gt;A compromised workflow that needs human approval to reach production is a compromised workflow that gets caught.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Applied All of This in Nexloy
&lt;/h2&gt;

&lt;p&gt;Everything I have described in this article is not just theory I picked up from documentation. It is the set of decisions I had to make in practice while building &lt;a href="https://nexloy.dev" rel="noopener noreferrer"&gt;Nexloy&lt;/a&gt;, a self-hosted deployment platform I have been working on quietly for a while now.&lt;/p&gt;

&lt;p&gt;I am not ready to share the full details yet because the open source launch is coming soon, and I want to do it properly. But here is enough context to show you why these principles mattered so much to me as a builder.&lt;/p&gt;

&lt;p&gt;The core problem Nexloy was built to solve is that small engineering teams are stuck between two painful extremes. On one side you have manual SSH scripts, scattered environment variables, and deployment processes that live only in someone's head. On the other side you have Kubernetes, which is powerful but brings enormous complexity that most small teams do not actually need. Nexloy sits in the practical middle ground: Docker Compose, Nginx, and GitHub Actions on servers you own, with a proper dashboard, deployment history, secret management, and resource bindings all in one place.&lt;/p&gt;

&lt;p&gt;Building it meant I had to think hard about every security decision because the platform itself holds the keys to your production servers. A deployment pipeline that manages other deployment pipelines has no room for the mistakes I described earlier in this article. So the principles around &lt;strong&gt;encrypted credential storage&lt;/strong&gt;, &lt;strong&gt;explicit deployment gates&lt;/strong&gt;, &lt;strong&gt;secret redaction&lt;/strong&gt;, and &lt;strong&gt;role-based access with MFA (Multi-Factor Authentication)&lt;/strong&gt; were not optional extras I bolted on. They were the foundation.&lt;/p&gt;

&lt;p&gt;The most important lesson I took from incidents like Trivy's compromise is this: &lt;strong&gt;the best security is the kind that gets generated for you, not the kind you have to remember to configure.&lt;/strong&gt; When Nexloy creates a GitHub Actions workflow for your project, the secure patterns are already in it. The burden should not fall on every individual team to get this right from scratch every time.&lt;/p&gt;

&lt;p&gt;I will be writing a full dedicated post about Nexloy soon, covering the architecture, the decisions behind it, and how to get started. If you want to be the first to know when it drops, follow along at &lt;a href="https://nexloy.dev" rel="noopener noreferrer"&gt;nexloy.dev&lt;/a&gt;, on GitHub at &lt;a href="https://github.com/Nexloy" rel="noopener noreferrer"&gt;github.com/Nexloy&lt;/a&gt;, or on X at &lt;a href="https://twitter.com/NexloyDev" rel="noopener noreferrer"&gt;@NexloyDev&lt;/a&gt;. It is coming.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your Quick Wins Checklist
&lt;/h2&gt;

&lt;p&gt;Here are the things you can start on today. Most of these take under 30 minutes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search every workflow file&lt;/strong&gt; for Actions referenced by tags like &lt;code&gt;@v1&lt;/code&gt;, &lt;code&gt;@v2&lt;/code&gt;, or &lt;code&gt;@main&lt;/code&gt; and convert each one to a SHA pin with a comment showing the version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go to Settings → Actions → General&lt;/strong&gt; and set your default &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; permissions to read-only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;permissions: {}&lt;/code&gt;&lt;/strong&gt; to the top of every workflow file, then declare only what each job specifically needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search your repository for &lt;code&gt;pull_request_target&lt;/code&gt;&lt;/strong&gt; and review every instance against &lt;a href="https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-tj-actionschanged-files-cve-2025-30066-and-reviewdogaction" rel="noopener noreferrer"&gt;CISA advisory guidance&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install Zizmor&lt;/strong&gt; and run it against your &lt;code&gt;.github/&lt;/code&gt; directory right now, before you close this tab&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a &lt;code&gt;.github/CODEOWNERS&lt;/code&gt; file&lt;/strong&gt; that routes all workflow file changes to your security team for review&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable &lt;a href="https://docs.github.com/en/code-security/dependabot" rel="noopener noreferrer"&gt;Dependabot&lt;/a&gt; for Actions&lt;/strong&gt; so it automatically opens pull requests to update your SHA pins when new legitimate versions are released&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up at least one production Environment&lt;/strong&gt; in GitHub settings with a required reviewer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable secret scanning and push protection&lt;/strong&gt; in your repository security settings, which catches accidental credential commits before they land on your main branch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do a third-party Actions audit&lt;/strong&gt; and for every Action you use, ask yourself: do I know who maintains this? Have I read the source code? Can I replace it with a SHA-pinned fork inside my own organisation?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If you genuinely cannot do all of these right now, do the first one.&lt;/strong&gt; Pin your Actions to commit SHAs. It is the single change that would have protected the vast majority of the 23,000 repositories hit by CVE-2025-30066.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;I have been building and securing software systems for long enough to know that most breaches do not happen because a team was careless. They happen because of a gap between how seriously we treat application security and how seriously we treat pipeline security. The application gets pentest reviews, code audits, and threat modelling. The pipeline gets trusted blindly.&lt;/p&gt;

&lt;p&gt;The attacks of 2024 and 2025 are the industry's correction. Supply chain attacks against GitHub Actions pipelines increased sharply across that period, hitting not just small teams but well-resourced companies and, in Trivy's case, security professionals who knew exactly what the risks were.&lt;/p&gt;

&lt;p&gt;The encouraging thing is that the defences are not exotic. &lt;strong&gt;SHA pinning, OIDC, least-privilege tokens, and automated scanning&lt;/strong&gt; — none of these require a large security team or a significant budget. They require intention, and they require treating your pipeline with the same seriousness you treat your code.&lt;/p&gt;

&lt;p&gt;The teams that got hit were not failures. They were running behind the curve on a threat the industry is only now taking seriously at scale. You do not have to be.&lt;/p&gt;

&lt;p&gt;If this was useful, I write about secure DevOps, API security, and real-world engineering lessons at &lt;a href="https://vincenttechblog.com" rel="noopener noreferrer"&gt;vincenttechblog.com&lt;/a&gt;. I am also building &lt;a href="https://nexloy.dev" rel="noopener noreferrer"&gt;Nexloy&lt;/a&gt;, an open source self-hosted deployment platform launching soon. And if you learn better by watching than reading, I break down topics like this on my &lt;a href="https://youtube.com/@vincenttechblog" rel="noopener noreferrer"&gt;YouTube channel&lt;/a&gt;, where I am approaching 5,000 subscribers and have crossed 1 million views. Come find me. I would love to hear what you are building and what security challenges you are working through.&lt;/p&gt;




&lt;p&gt;If any of this was useful, I write about secure DevOps, API security, and real engineering lessons from the field at &lt;a href="https://vincenttechblog.com" rel="noopener noreferrer"&gt;vincenttechblog.com&lt;/a&gt;. Also, in the middle of building &lt;a href="https://nexloy.dev" rel="noopener noreferrer"&gt;Nexloy&lt;/a&gt;, an open source self-hosted deployment platform launching soon at &lt;a href="https://nexloy.dev" rel="noopener noreferrer"&gt;Nexloy&lt;/a&gt;, and if you prefer video, I cover topics like this on my &lt;a href="https://youtube.com/vincenttechblog" rel="noopener noreferrer"&gt;Youtube Channel&lt;/a&gt; too. Would love to hear how your team currently handles pipeline security. Drop a comment below.&lt;/p&gt;

</description>
      <category>github</category>
      <category>security</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How Encrisoft Achieved 20x Speedup Without More Hardware</title>
      <dc:creator>Vincent Olagbemide</dc:creator>
      <pubDate>Fri, 03 Oct 2025 20:03:27 +0000</pubDate>
      <link>https://dev.to/vincentayorinde/how-encrisoft-achieved-20x-speedup-without-more-hardware-1657</link>
      <guid>https://dev.to/vincentayorinde/how-encrisoft-achieved-20x-speedup-without-more-hardware-1657</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://encrisoft.com" rel="noopener noreferrer"&gt;Encrisoft&lt;/a&gt;, we are genuinely excited about &lt;a href="https://usealerta.com" rel="noopener noreferrer"&gt;Alerta&lt;/a&gt;, our flexible instant alerting API that’s built to fit the unique needs of businesses. It has been amazing to see it in action, delivering over 80 million alerts so far, helping companies stay on top of things with timely, tailored notifications that make a real difference.&lt;/p&gt;

&lt;p&gt;But here’s the thing about growth: it’s a double-edged sword. More alerts means more value for customers, but it also means more pressure on the system. Over time, we started noticing signs of strain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboards that once loaded instantly began to lag.&lt;/li&gt;
&lt;li&gt;Queries that were lightning-fast slowed to a crawl.&lt;/li&gt;
&lt;li&gt;Uptime felt fragile because the database was under stress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is a technical deep-dive into how we fixed that. You’ll see exactly how we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Optimised PostgreSQL with the right indexes.&lt;/li&gt;
&lt;li&gt;Introduced Redis caching (the smart way).&lt;/li&gt;
&lt;li&gt;Achieved a 20× speedup without adding a single new server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And most importantly, you’ll get the code, reasoning, and metrics so you can apply the same lessons in your own systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scale Problem
&lt;/h2&gt;

&lt;p&gt;Let’s make it concrete.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Our &lt;code&gt;Alert&lt;/code&gt; table grew past &lt;strong&gt;80 million rows&lt;/strong&gt;, and it was still growing every month.&lt;/li&gt;
&lt;li&gt;Queries that powered critical dashboards, things like &lt;em&gt;“how many alerts in the last 30 days?”&lt;/em&gt; or &lt;em&gt;“show me transaction counts by type”&lt;/em&gt; started taking seconds instead of milliseconds.&lt;/li&gt;
&lt;li&gt;Even modest delays caused real pain. For businesses who rely on Alerta to see what’s happening right now, waiting two seconds for a dashboard to load is like driving with foggy glasses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It was clear - the system wasn’t scaling gracefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Index to the Rescue
&lt;/h2&gt;

&lt;p&gt;We started by looking at the data. PostgreSQL has a wonderful feature: &lt;strong&gt;slow query logs&lt;/strong&gt;. By turning those on, we could see exactly which queries were dragging.&lt;/p&gt;

&lt;p&gt;The culprit? &lt;strong&gt;Sequential&lt;/strong&gt; scans. Our queries were walking through millions of rows because they didn’t have the right indexes to guide them.&lt;/p&gt;

&lt;p&gt;So we fixed that by creating &lt;strong&gt;composite indexes&lt;/strong&gt;, indexes tailored specifically to the queries users were actually running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- For queries by user and time
CREATE INDEX CONCURRENTLY idx_alert_uid_createdat
  ON "Alert" ("userId", "createdAt" DESC);

-- For queries by user, type, and time
CREATE INDEX CONCURRENTLY idx_alert_uid_type_createdat
  ON "Alert" ("userId", "type", "createdAt" DESC);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The impact was immediate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboard queries that once took &lt;strong&gt;~2 seconds now returned in ~100 ms.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Database CPU usage during peak hours dropped by &lt;strong&gt;60%.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One small change, just two indexes, transformed performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis Caching (Smart, Per-User)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Indexes solved the first problem, but we noticed another: users kept hitting the &lt;strong&gt;same dashboards again and again&lt;/strong&gt;. That meant the same queries were being re-run, even though the results hadn’t changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution? &lt;strong&gt;Redis caching&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But we didn’t just cache everything blindly. Instead, we cached at the per-user level, so each user got their own cached results, isolated from others.&lt;/p&gt;

&lt;p&gt;Here’s what the cache key looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;u:${userId}:alert_count:${filter}:${start}:${end}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here’s what that meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;u:${userId}&lt;/code&gt; → cache scoped to a specific user (no data leaks).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;alert_count&lt;/code&gt;→ the kind of query.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;${filter}:${start}:${end}&lt;/code&gt; → captures the parameters so the cache is precise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a short &lt;strong&gt;TTL (time-to-live) of 60 seconds,&lt;/strong&gt; users received nearly instant responses most of the time and had access to fresh data whenever needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results:
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repeat queries came back in &amp;lt;50 ms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Database load dropped even further, with &lt;strong&gt;80% fewer queries during peak.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results Snapshot
&lt;/h2&gt;

&lt;p&gt;Here’s the before-and-after in one table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before Optimization&lt;/th&gt;
&lt;th&gt;After Optimization&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard load time&lt;/td&gt;
&lt;td&gt;~2 seconds&lt;/td&gt;
&lt;td&gt;~100–150 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query load on database&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;70–80% lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU usage during peak&lt;/td&gt;
&lt;td&gt;80–90%&lt;/td&gt;
&lt;td&gt;30–40%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Alerta went from fragile at millions of rows to scalable at &lt;strong&gt;billions&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Personal Lessons as a Founder
&lt;/h2&gt;

&lt;p&gt;Looking back, here’s what this journey taught me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure before you optimise:&lt;/strong&gt;  We didn’t guess, we looked at slow query logs. That saved us from “fixing” the wrong thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index smart, not more:&lt;/strong&gt;  A single composite index delivered massive gains. Randomly adding indexes just bloats your database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache with precision:&lt;/strong&gt;  User-level caching avoided the pitfalls of global caching (like data leaks or stale results).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling ≠ buying hardware:&lt;/strong&gt; Throwing servers at the problem would have been expensive and temporary. Engineering finesse gave us a long-term win.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Performance is more than numbers; it’s about trust.&lt;/p&gt;

&lt;p&gt;When dashboards respond instantly, users feel confident. When queries drag, even for a second, trust erodes.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://encrisoft.com" rel="noopener noreferrer"&gt;Encrisoft&lt;/a&gt;, we restored that confidence in Alerta with nothing more than &lt;strong&gt;two indexes and smart caching.&lt;/strong&gt; The result was a &lt;strong&gt;20× speedup&lt;/strong&gt;, a database that runs cooler, and a platform ready for billions of alerts.&lt;/p&gt;

&lt;p&gt;And want to know the best part? These aren’t exotic tricks. Any engineering team can apply them. Measure, index smartly, cache wisely, and you’ll be amazed how far your existing infrastructure can go.&lt;/p&gt;

</description>
      <category>encrisoft</category>
      <category>alerta</category>
      <category>postgres</category>
      <category>indexing</category>
    </item>
    <item>
      <title>How to integrate Alerta into your Business</title>
      <dc:creator>Vincent Olagbemide</dc:creator>
      <pubDate>Tue, 01 Oct 2024 18:34:17 +0000</pubDate>
      <link>https://dev.to/vincentayorinde/how-to-integrate-alerta-into-your-business-2f4p</link>
      <guid>https://dev.to/vincentayorinde/how-to-integrate-alerta-into-your-business-2f4p</guid>
      <description>&lt;p&gt;In this post, we'll be creating a Slack channel called #finance where we'll send alerts on the transfer of funds and also send a reply to the same message after the transfer has been delivered.&lt;/p&gt;

&lt;p&gt;Prerequisite:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You should be a user of Slack, or have your own slack.com account, &lt;/li&gt;
&lt;li&gt;Create Channels for your notifications&lt;/li&gt;
&lt;li&gt;Create an Alerta account on (app.usealerta.com)&lt;/li&gt;
&lt;li&gt;Create a basic fintech app to plug in our alerts&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We are going to use a Sample Fintech app,&lt;/p&gt;

&lt;h2&gt;
  
  
  Create Slack Channel
&lt;/h2&gt;

&lt;p&gt;On Slack, let's assume we have a #finance team, #marketing team, and #security team.&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%2F9x82mk1u2gtes42utdy0.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%2F9x82mk1u2gtes42utdy0.png" alt="Create a channel for in slack" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll go ahead and create a channel for each team and add each employee respectively.&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%2F3w6jp48k4dyewzm17vy7.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%2F3w6jp48k4dyewzm17vy7.png" alt="Create Slack Channel for Alerta Integration" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this is done, you should have your channels created and ready to be integrated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create Alerta Account
&lt;/h2&gt;

&lt;p&gt;Create an account to get your API key from the Alerta App &lt;a href="https://app.usealerta.com/auth/create-account" rel="noopener noreferrer"&gt;Here&lt;/a&gt;&lt;br&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%2F4ab0rurtq0b5hw5tmvwq.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%2F4ab0rurtq0b5hw5tmvwq.png" alt="Create alerta account" width="800" height="666"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After successfully creating an account, save your API key, then switch to the &lt;code&gt;integrations&lt;/code&gt; page and select slack&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%2Fge5afiisc0z4od5nj7pv.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%2Fge5afiisc0z4od5nj7pv.png" alt="alerta integration page" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select the channel you created from the Slack Oauth page. See below&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%2F8m537fj5n6bogy3237kq.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%2F8m537fj5n6bogy3237kq.png" alt="alerta slack integration oauth page" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After successful integration, the channel should be integrated&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%2F8xy4q2z3py3dm2svslfr.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%2F8xy4q2z3py3dm2svslfr.png" alt="Alerta integration success" width="799" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: This is how you can add any channels to your alerta account&lt;/p&gt;
&lt;h2&gt;
  
  
  Create the Sample Fintech App
&lt;/h2&gt;

&lt;p&gt;Now that we have successfully added our channels to the slack workspace.&lt;/p&gt;

&lt;p&gt;Let us see how to integrate Alerta into our code base. In case you want o see the full app, See &lt;a href="https://github.com/vincentayorinde/sample-fintech-app" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; to skip the step-by-step process  &lt;/p&gt;

&lt;p&gt;Also, you can watch the &lt;a href="https://www.youtube.com/watch?v=7D9O_BVwMH8" rel="noopener noreferrer"&gt;Youtube video&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Otherwise, let us continue&lt;/p&gt;

&lt;p&gt;Installing Nest JS and create a nest app using the following commands&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ npm i -g @nestjs/cli
❯ nest new sample-fintech-app
❯ cd sample-fintech-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's create the wallet service, module and controller&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ npm i @nestjs/config @nestjs/axios    
❯ nest g module wallet
❯ nest g service wallet
❯ nest g controller wallet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's create the alerta service, module and controller&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ nest g module alerta
❯ nest g service alerta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Folder API Structure
&lt;/h2&gt;

&lt;p&gt;After running all the commands above, your sample finance app which should have this folder structure, feel free to remove the &lt;code&gt;.spec&lt;/code&gt; files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sample-wallet-app/
├── src/
│   ├── alerta/
│   │   ├── alerta.module.ts        # Module for alerta functions
│   │   └── alerta.service.ts       # for alerta external api calls
│   ├── wallet/
│   │   ├── wallet.controller.ts    # Controller handling wallet
│   │   ├── wallet.module.ts        # Module for Wallet functions
│   │   └── wallet.service.ts       # handling business logic
│   ├── app.module.ts               # Main application module
│   └── main.ts                     # Entry point for the application
├── node_modules/                   # Node dependencies
├── package.json                    # Dependencies
├── package-lock.json               # Lock file extracting version of our dependencies
├── tsconfig.json                   # TypeScript configuration file
└── .eslintrc.js                    # ESLint configuration (optional)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add Wallet Services
&lt;/h2&gt;

&lt;p&gt;We'll create two functions in our &lt;code&gt;wallet.service.ts&lt;/code&gt; file. One to transfer funds and the second to withdraw funds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Injectable } from '@nestjs/common';
import { AlertaService } from 'src/alerta/alerta.service';

@Injectable()
export class WalletService {
  constructor(private readonly alertaService: AlertaService) {}

  async walletTransfer(transferDto: {
    fromWalletID: string;
    toWalletID: string;
    amount: number;
  }) {
    const { fromWalletID, toWalletID, amount } = transferDto;
    const message = `Transferred ${amount} from wallet ${fromWalletID} to wallet ${toWalletID}`;

    return { message, success: true };
  }

  withdrawToBank(withDrawDto: {
    walletID: string;
    bankAccount: string;
    amount: number;
  }) {
    const { walletID, bankAccount, amount } = withDrawDto;
    const message = `Withdrawn ${amount} from wallet ${walletID} to bank account ${bankAccount}`;
    return { message, success: true };
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add Wallet Controller
&lt;/h2&gt;

&lt;p&gt;We'll create two functions in our &lt;code&gt;wallet.controller.ts&lt;/code&gt; file. One initiates the transfer of funds and the second to withdraws funds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Body, Controller, Post } from '@nestjs/common';
import { WalletService } from './wallet.service';

@Controller('wallet')
export class WalletController {
  constructor(private readonly walletService: WalletService) {}
  @Post('transfer')
  walletTransfer(
    @Body()
    transferDto: {
      fromWalletID: string;
      toWalletID: string;
      amount: number;
    },
  ) {
    return this.walletService.walletTransfer(transferDto);
  }

  @Post('withdraw')
  withdrawToBank(
    @Body()
    withDrawDto: {
      walletID: string;
      bankAccount: string;
      amount: number;
    },
  ) {
    return this.walletService.withdrawToBank(withDrawDto);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have created our service and controller for the wallet service, lets add the module&lt;/p&gt;

&lt;p&gt;for &lt;code&gt;wallet.module.ts&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;import { Module } from '@nestjs/common';
import { WalletController } from './wallet.controller';
import { WalletService } from './wallet.service';

@Module({
  controllers: [WalletController],
  providers: [WalletService],
})
export class WalletModule {}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add Env Variables
&lt;/h2&gt;

&lt;p&gt;To secure our API keys, we'll add a &lt;code&gt;.env&lt;/code&gt; file and specify the api key and url there&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALERTA_URL=place your api url
ALERTA_KEY=place your api key here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add Alerta Service
&lt;/h2&gt;

&lt;p&gt;After setting up our wallet and our env file, we'll create two functions in our &lt;code&gt;alerta.service.ts&lt;/code&gt; file. One to send the message and the second to reply to messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { HttpService } from '@nestjs/axios';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { catchError, lastValueFrom } from 'rxjs';

@Injectable()
export class AlertaService {
  private alertaUrl = this.config.get('ALERTA_URL');
  private alertaKey = this.config.get('ALERTA_KEY');

  constructor(
    private config: ConfigService,
    private httpService: HttpService,
  ) {}
  async sendAlert(alertaDto: {
    message: string;
    channel: string;
    replyTo: boolean;
  }): Promise&amp;lt;any&amp;gt; {
    const { message, channel, replyTo } = alertaDto;

    try {
      const headers = {
        secretKey: `secret ${this.alertaKey}`,
        'Content-Type': 'application/json',
      };

      const res = await lastValueFrom(
        this.httpService
          .post(
            `${this.alertaUrl}/send`,
            { message, channel, replyTo },
            {
              headers,
            },
          )
          .pipe(
            catchError((error) =&amp;gt; {
              throw new HttpException(
                `Error fetching data from external API: ${error.message}`,
                HttpStatus.BAD_REQUEST,
              );
            }),
          ),
      );
      return res.data;
      return res.data;
    } catch (error) {
      return error;
    }
  }

  async replyAlert(alertaDto: {
    channelId: string;
    threadId: string;
    channelRef: string;
    message: string;
  }): Promise&amp;lt;any&amp;gt; {
    const { channelId, threadId, channelRef, message } = alertaDto;

    try {
      const headers = {
        secretKey: `secret ${this.alertaKey}`,
        'Content-Type': 'application/json',
      };

      const res = await lastValueFrom(
        this.httpService
          .post(
            `${this.alertaUrl}/reply`,
            { channelId, threadId, channelRef, message },
            { headers },
          )
          .pipe(
            catchError((error) =&amp;gt; {
              throw new HttpException(
                `Error processing data from API ${error}`,
                HttpStatus.BAD_REQUEST,
              );
            }),
          ),
      );
      return res.data;
    } catch (error) {}
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add Alerta Module
&lt;/h2&gt;

&lt;p&gt;After setting up our alerta service, we'll update our module file &lt;code&gt;alerta.module.ts&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { AlertaService } from './alerta.service';
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [HttpModule.register({ timeout: 5000, maxRedirects: 5 })],
  providers: [AlertaService, ConfigService],
  exports: [AlertaService],
})
export class AlertaModule {}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are almost there,&lt;br&gt;
Before we test, lets us add the alerts to some places in our wallet service so we get the alerts when there is an operation.&lt;/p&gt;

&lt;p&gt;First lets update the &lt;code&gt;wallet.module.ts&lt;/code&gt; file to know our Alerta service&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
.
.
import { AlertaModule } from 'src/alerta/alerta.module';

@Module({
  imports: [AlertaModule],
  controllers: [WalletController],
  providers: [WalletService],
})
export class WalletModule {}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;also, let us update the &lt;code&gt;app.module.ts&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;import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { WalletService } from './wallet/wallet.service';
import { WalletModule } from './wallet/wallet.module';
import { WalletController } from './wallet/wallet.controller';
import { AlertaService } from './alerta/alerta.service';
import { AlertaModule } from './alerta/alerta.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HttpModule } from '@nestjs/axios';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    WalletModule,
    AlertaModule,
    HttpModule,
  ],
  controllers: [AppController],
  providers: [AppService, ConfigService],
})
export class AppModule {}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will notice that we added the &lt;code&gt;ConfigModule.forRoot({ isGlobal: true })&lt;/code&gt; this will ensure that all env config is availble and can be used app-wide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plug in Alerta into the codebase
&lt;/h2&gt;

&lt;p&gt;Now, in the &lt;code&gt;wallet.service.ts&lt;/code&gt;, let us assume we want to know when any user does a transfer or does a withdraw, we can send a message to the &lt;code&gt;#finance&lt;/code&gt; channel that we created and integrated earlier.&lt;br&gt;
Lets update our code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Injectable } from '@nestjs/common';
import { AlertaService } from 'src/alerta/alerta.service';

@Injectable()
export class WalletService {
// bring the service in by adding it the contructor
  constructor(private readonly alertaService: AlertaService) {}

  async walletTransfer(transferDto: {
    fromWalletID: string;
    toWalletID: string;
    amount: number;
  }) {
    const { fromWalletID, toWalletID, amount } = transferDto;
    const message = `Transferred ${amount} from wallet ${fromWalletID} to wallet ${toWalletID}`;

// Add the alert from the alerta service here. Also, because we want 
// to reply to it later, we'll set `replyTo` to true
    await this.alertaService.sendAlert({
      message,
      channel: 'finance',
      replyTo: true,
    });

    return { message, success: true };
  }
.
.
.

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can send messages to Slack in the channel. Let's assume we want to reply to the same message that was sent earlier. &lt;br&gt;
Usually, you can save the data from the response after you have sent a message so the message can be replied to in the future.&lt;/p&gt;

&lt;p&gt;The data needed for the reply is &lt;code&gt;threadId&lt;/code&gt;, &lt;code&gt;channelId&lt;/code&gt; and &lt;code&gt;channelRef&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  To add the Reply
&lt;/h2&gt;

&lt;p&gt;Before adding the reply feature, we have to mention the @Alerta App in the channel&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%2Fjuda846ymll2pcup4vdk.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%2Fjuda846ymll2pcup4vdk.png" alt="Add the bot to the channel" width="799" height="139"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Injectable } from '@nestjs/common';
import { AlertaService } from 'src/alerta/alerta.service';

@Injectable()
export class WalletService {
  constructor(private readonly alertaService: AlertaService) {}

  async walletTransfer(transferDto: {
    fromWalletID: string;
    toWalletID: string;
    amount: number;
  }) {
    const { fromWalletID, toWalletID, amount } = transferDto;
    const message = `Transferred ${amount} from wallet ${fromWalletID} to wallet ${toWalletID}`;

    const alertRes = await this.alertaService.sendAlert({
      message,
      channel: 'finance',
      replyTo: true,
    });

// triggering the reply
    setTimeout(() =&amp;gt; {
      this.alertaService.replyAlert({
        message: 'Transfer delivered!',
        threadId: alertRes.data.replyData.threadId,
        channelId: alertRes.data.replyData.channelId,
        channelRef: alertRes.data.replyData.channelRef,
      });
    }, 5000);

    return { message, success: true };
  }

  withdrawToBank(withDrawDto: {
    walletID: string;
    bankAccount: string;
    amount: number;
  }) {
    const { walletID, bankAccount, amount } = withDrawDto;
    const message = `Withdrawn ${amount} from wallet ${walletID} to bank account ${bankAccount}`;
    return { message, success: true };
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's test our implementation&lt;/p&gt;

&lt;p&gt;Test transfer&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%2Fl127pyru9urvmtyn0yx9.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%2Fl127pyru9urvmtyn0yx9.png" alt="Postman test of integration for alerta" width="799" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Slack Message
&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.amazonaws.com%2Fuploads%2Farticles%2F8lyjsfr86gs2pexgi9eq.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%2F8lyjsfr86gs2pexgi9eq.png" alt="send message to slack" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reply to the existing message
&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.amazonaws.com%2Fuploads%2Farticles%2Foivvpeumb4anp1c4uktc.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%2Foivvpeumb4anp1c4uktc.png" alt="Reply to exiting message" width="800" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In conclusion, we created a Slack channel called #finance where we'll send updates messages on the transfer of funds and also send a reply to the same message after the transfer has been delivered.&lt;/p&gt;

&lt;p&gt;Thank you.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>alerta</category>
      <category>slack</category>
      <category>nestjs</category>
    </item>
  </channel>
</rss>
