<?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: Taqin</title>
    <description>The latest articles on DEV Community by Taqin (@abdulm).</description>
    <link>https://dev.to/abdulm</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%2F187749%2Fe5198901-12a8-4aed-85a1-c57540c83a53.jpg</url>
      <title>DEV Community: Taqin</title>
      <link>https://dev.to/abdulm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abdulm"/>
    <language>en</language>
    <item>
      <title>I built a 7 MB XAMPP alternative for Windows in pure Go (no Docker, no WSL)</title>
      <dc:creator>Taqin</dc:creator>
      <pubDate>Sat, 11 Apr 2026 08:41:10 +0000</pubDate>
      <link>https://dev.to/abdulm/i-built-a-7-mb-xampp-alternative-for-windows-in-pure-go-no-docker-no-wsl-3kn7</link>
      <guid>https://dev.to/abdulm/i-built-a-7-mb-xampp-alternative-for-windows-in-pure-go-no-docker-no-wsl-3kn7</guid>
      <description>&lt;p&gt;A few weekends ago I got fed up with the same Windows web-dev ritual and wrote my own control panel. The result is &lt;strong&gt;GoAMPP&lt;/strong&gt; — a 7 MB single-binary native Win32 app that knows how to download and manage Apache, Nginx, MariaDB, PostgreSQL, Redis, PHP, phpMyAdmin, Adminer, plus four language runtimes (Node.js, Python, Go, Java) and seventeen framework scaffolders.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/imtaqin/goampp" rel="noopener noreferrer"&gt;github.com/imtaqin/goampp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post is a brain dump of the design choices I made, the bugs I hit, and the parts of Win32 that bit me along the way. Hopefully somebody else building a desktop app in Go finds the post-mortem useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use XAMPP
&lt;/h2&gt;

&lt;p&gt;Three reasons I ran out of patience with the existing options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The installers are huge.&lt;/strong&gt; XAMPP for Windows is around 200 MB downloaded, more after install, even though I only ever use Apache + MariaDB + PHP + phpMyAdmin. The rest is dead weight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versions go stale.&lt;/strong&gt; When PHP 8.5.0 dropped, the bundled version in XAMPP took months to catch up. Same story with MariaDB LTS bumps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The control panels are fragile.&lt;/strong&gt; XAMPP's panel hangs when Apache crashes mid-startup and the pid file is stale. Laragon is better but ships with Cmder, a Telegram notifier, and a bunch of stuff I didn't ask for.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mental model I wanted: ship a tiny control panel, fetch the services on demand, cache them locally, and never bundle anything I'm not actively using. Closer to a package manager than an installer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture in 30 seconds
&lt;/h2&gt;

&lt;p&gt;GoAMPP is a single Go binary built with &lt;code&gt;-ldflags="-H windowsgui -s -w"&lt;/code&gt; so it has no console window and is stripped down to ~7 MB. The GUI uses &lt;a href="https://github.com/rodrigocfd/windigo" rel="noopener noreferrer"&gt;windigo&lt;/a&gt;, a pure-Go binding for the Win32 widget set — no CGO, no webview, no Electron.&lt;/p&gt;

&lt;p&gt;Inside the binary there's a &lt;code&gt;DownloadCatalog&lt;/code&gt; map keyed on service name. Each entry has the download URL, the install directory, an optional &lt;code&gt;StripTop&lt;/code&gt; for archives that wrap everything in a top-level folder, and a &lt;code&gt;PostInstall&lt;/code&gt; hook for things like running &lt;code&gt;mariadb-install-db.exe&lt;/code&gt; or patching &lt;code&gt;httpd.conf&lt;/code&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Screenshot
&lt;/h1&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%2F79swjza2xu7lia0td96h.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%2F79swjza2xu7lia0td96h.png" alt=" " width="800" height="658"&gt;&lt;/a&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%2Fdibb74dhc97qpovz0kxf.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%2Fdibb74dhc97qpovz0kxf.png" alt=" " width="800" height="658"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="s"&gt;"Apache"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"2.4.66 (VS18, win64)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;"https://www.apachelounge.com/download/VS18/binaries/httpd-2.4.66-260223-Win64-VS18.zip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;InstallDir&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"bin/apache"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;StripTop&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;"Apache24/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;"zip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CheckFile&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"bin/httpd.exe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PostInstall&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installDir&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// patch httpd.conf to fix SRVROOT, ServerName, mod_cgi, etc.&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you click &lt;strong&gt;Start&lt;/strong&gt; on a service whose binary isn't on disk yet, the flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the catalog for an entry. Bail if missing.&lt;/li&gt;
&lt;li&gt;Spawn a goroutine so the UI stays responsive.&lt;/li&gt;
&lt;li&gt;Stream the zip into &lt;code&gt;downloads/&amp;lt;filename&amp;gt;.part&lt;/code&gt;. Atomic rename on success.&lt;/li&gt;
&lt;li&gt;Walk the archive entries, strip &lt;code&gt;StripTop&lt;/code&gt; if set, write to &lt;code&gt;bin/&amp;lt;service&amp;gt;/&lt;/code&gt;. Guard against zip-slip — refuse any entry whose resolved path escapes the destination.&lt;/li&gt;
&lt;li&gt;Run the post-install hook.&lt;/li&gt;
&lt;li&gt;Start the process.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The download progress fires through a &lt;code&gt;ProgressFunc&lt;/code&gt; callback that the UI uses to drive a &lt;code&gt;ProgressBar&lt;/code&gt; widget at ~30 Hz. Text logging fires at 1 Hz in parallel because writing to a multi-line &lt;code&gt;Edit&lt;/code&gt; control is expensive — every line is a full &lt;code&gt;WM_SETTEXT&lt;/code&gt; round trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Apache bug I lost an evening to
&lt;/h2&gt;

&lt;p&gt;This one's worth its own section because it's the kind of bug that doesn't show up on Linux at all.&lt;/p&gt;

&lt;p&gt;The first version of GoAMPP used &lt;code&gt;mod_proxy_fcgi&lt;/code&gt; with the standard idiom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;FilesMatch&lt;/span&gt;&lt;span class="sr"&gt; \.php$&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;SetHandler&lt;/span&gt; "proxy:fcgi://127.0.0.1:9000"
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;FilesMatch&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apache logs the script's full filesystem path in the upstream URL. On Linux that produces something sane like &lt;code&gt;fcgi://127.0.0.1:9000/var/www/script.php&lt;/code&gt;. On Windows, the script path includes a drive letter (&lt;code&gt;C:/...&lt;/code&gt;), and Apache concatenates without inserting a slash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fcgi://127.0.0.1:9000 + C:/Users/.../script.php
= fcgi://127.0.0.1:9000C:/Users/.../script.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy URL parser then sees host=&lt;code&gt;127.0.0.1&lt;/code&gt;, port=&lt;code&gt;9000C&lt;/code&gt;, path=&lt;code&gt;/Users/...&lt;/code&gt;, and the DNS resolver loses its mind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;AH00898: &lt;span class="ss"&gt;DNS&lt;/span&gt; lookup failure for: 127.0.0.1:9000c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is Apache bug 55345, open since 2013. The workaround documented in the comments is to use &lt;code&gt;ProxyPassMatch&lt;/code&gt; with the docroot baked into the URL, but I tested that route and PHP-CGI then complained "No input file specified" because the SCRIPT_FILENAME ended up with a leading slash that Windows refused to resolve.&lt;/p&gt;

&lt;p&gt;The fix that actually works is to use &lt;code&gt;mod_cgi&lt;/code&gt; + &lt;code&gt;mod_actions&lt;/code&gt; + a &lt;code&gt;ScriptAlias&lt;/code&gt; to point at &lt;code&gt;php-cgi.exe&lt;/code&gt; directly. It's the classic CGI approach XAMPP has used for fifteen years. Slightly slower per request than FastCGI but bulletproof on Windows because there's no proxy URL to construct in the first place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;LoadModule&lt;/span&gt; cgi_module modules/mod_cgi.so
&lt;span class="nc"&gt;LoadModule&lt;/span&gt; actions_module modules/mod_actions.so

&lt;span class="nc"&gt;ScriptAlias&lt;/span&gt; "/__goampp-php-bin__/" "C:/.../bin/php/"
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;Directory&lt;/span&gt;&lt;span class="sr"&gt; "C:/.../bin/php"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;AllowOverride&lt;/span&gt; &lt;span class="ss"&gt;None&lt;/span&gt;
    &lt;span class="nc"&gt;Options&lt;/span&gt; +ExecCGI
    &lt;span class="nc"&gt;Require&lt;/span&gt; &lt;span class="ss"&gt;all&lt;/span&gt; granted
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="nc"&gt;AddHandler&lt;/span&gt; application/x-httpd-php .php
&lt;span class="nc"&gt;Action&lt;/span&gt; application/x-httpd-php "/__goampp-php-bin__/php-cgi.exe"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lesson learned: when a Windows Apache config uses &lt;code&gt;proxy:fcgi://&lt;/code&gt;, run &lt;code&gt;tail -f error_log&lt;/code&gt; and look for &lt;code&gt;9000c&lt;/code&gt; in the DNS error messages. It's never your firewall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Process management on Windows is harder than it looks
&lt;/h2&gt;

&lt;p&gt;The first time I clicked Stop on Apache, the process exited and the log said &lt;code&gt;[Apache] exited cleanly&lt;/code&gt;. Two seconds later I clicked Start again and got &lt;code&gt;port 80 already in use&lt;/code&gt;. Reboot. Repeat. The bug took a while to find.&lt;/p&gt;

&lt;p&gt;Here's what was happening: Apache's &lt;code&gt;winnt&lt;/code&gt; MPM forks a worker child. When you &lt;code&gt;cmd.Process.Kill()&lt;/code&gt; the parent, the child keeps running because Windows doesn't have process groups in the POSIX sense. The orphaned worker holds the listening socket on port 80 until you manually kill it via Task Manager.&lt;/p&gt;

&lt;p&gt;Fix: use &lt;code&gt;taskkill /F /T /PID &amp;lt;pid&amp;gt;&lt;/code&gt; instead of &lt;code&gt;cmd.Process.Kill()&lt;/code&gt;. The &lt;code&gt;/T&lt;/code&gt; flag ("kill tree") nukes the parent and every descendant in one shot. Since GoAMPP launches services with a known parent PID, the tree kill catches the worker too.&lt;/p&gt;

&lt;p&gt;I also added a startup sweep that walks all processes via PowerShell (&lt;code&gt;Get-Process | Where-Object { $_.Path }&lt;/code&gt;) and kills anything whose image path is inside &lt;code&gt;&amp;lt;goampp&amp;gt;/bin/&lt;/code&gt;. That cleans up zombies left from prior crashes — say, when Apache died because httpd.conf had a syntax error and the worker was already running. Without the sweep, the next launch would hit "port 80 in use" and look broken.&lt;/p&gt;

&lt;p&gt;I originally used &lt;code&gt;wmic&lt;/code&gt; for the sweep, but Windows 11 22H2+ removed it from the default install. PowerShell ships with every modern Windows so it's a safer dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native ListView groups: fragile
&lt;/h2&gt;

&lt;p&gt;I wanted the Services view to render with collapsible category headers (Web Servers / Languages / Databases / Tools) using native ListView groups. Win32 supports them via &lt;code&gt;LVM_ENABLEGROUPVIEW&lt;/code&gt; + &lt;code&gt;LVM_INSERTGROUP&lt;/code&gt;. windigo doesn't expose either, so I tried to do it via raw &lt;code&gt;SendMessage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;LVGROUP&lt;/code&gt; struct has a documented Vista+ size of 148 bytes on 32-bit. On 64-bit, with 8-byte pointers and Go's struct padding, &lt;code&gt;unsafe.Sizeof()&lt;/code&gt; returns 152. I tried both values for &lt;code&gt;cbSize&lt;/code&gt;. Both rejected. The Common Controls v6 DLL on Win11 silently returns -1 from &lt;code&gt;LVM_INSERTGROUP&lt;/code&gt; for reasons I never tracked down.&lt;/p&gt;

&lt;p&gt;Eventually I gave up and built a hand-rolled card grid: 12 child Control containers with their own &lt;code&gt;Static&lt;/code&gt; for the icon, &lt;code&gt;Static&lt;/code&gt; for the name, and four &lt;code&gt;Button&lt;/code&gt; widgets per card. Each card's button-click handler captures the source service index in a closure, so I never need a "currently selected service" lookup. The result is more code than the ListView would have been but it actually works and the per-card buttons read better visually.&lt;/p&gt;

&lt;p&gt;Sometimes the right answer is "give up on the fancy widget and draw it yourself".&lt;/p&gt;

&lt;h2&gt;
  
  
  The icons-on-cards trick
&lt;/h2&gt;

&lt;p&gt;Each card has a 32×32 service logo on the left. Apache, Nginx, PHP, MariaDB, PostgreSQL, Redis, phpMyAdmin, Adminer, plus all four runtimes — twelve logos total.&lt;/p&gt;

&lt;p&gt;windigo's icon API only supports icons embedded as Win32 resources, not files on disk. I needed to load &lt;code&gt;.ico&lt;/code&gt; files at runtime, so the workflow ended up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A build-time tool (&lt;code&gt;tools/makelogos&lt;/code&gt;) reads a PNG from &lt;code&gt;icon/&amp;lt;name&amp;gt;.png&lt;/code&gt;, resizes it to 32×32 with &lt;code&gt;golang.org/x/image/draw&lt;/code&gt;'s Catmull-Rom kernel, encodes the result as a PNG, wraps it in a single-entry ICO header, and writes &lt;code&gt;assets/icons/&amp;lt;name&amp;gt;.ico&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;At runtime, &lt;code&gt;installServiceIcons()&lt;/code&gt; calls &lt;code&gt;win.HINSTANCE(0).LoadImage(ResIdStr(icoPath), IMAGE_ICON, 32, 32, LR_LOADFROMFILE)&lt;/code&gt; for each service and stores the resulting &lt;code&gt;HICON&lt;/code&gt; in a per-name map.&lt;/li&gt;
&lt;li&gt;Each card's &lt;code&gt;SS_ICON&lt;/code&gt; Static gets its icon set via &lt;code&gt;SendMessage(STM_SETICON, hIcon, 0)&lt;/code&gt; — windigo doesn't expose &lt;code&gt;STM_SETICON&lt;/code&gt; so I define the constant (&lt;code&gt;0x0170&lt;/code&gt;) inline and dial &lt;code&gt;SendMessage&lt;/code&gt; directly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Modern Windows accepts PNG-encoded entries inside ICO files, which means each &lt;code&gt;.ico&lt;/code&gt; is roughly the size of the resized PNG plus 22 bytes of header. The whole &lt;code&gt;assets/icons/&lt;/code&gt; directory is ~18 KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the box today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Services:&lt;/strong&gt; Apache 2.4.66, Nginx 1.28.3, MariaDB 11.4.10 LTS, PostgreSQL 18.3, Redis 5.0.14.1, PHP 8.5.5 NTS, phpMyAdmin 5.2.3, Adminer 5.4.2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtimes:&lt;/strong&gt; Node.js 22 LTS, Python 3.13 (embeddable, with auto-bootstrapped pip), Go 1.26, Eclipse Temurin JDK 21&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frameworks:&lt;/strong&gt; Laravel, Laravel + Livewire, Symfony, CodeIgniter 4, WordPress, Next.js, Vite + React, Express, NestJS, AdonisJS, Flask, Django, FastAPI, Go net/http, Gin, Spring Boot, Static HTML&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System:&lt;/strong&gt; auto-vhost (writes hosts file + Apache vhosts.conf), system tray, minimize-to-tray, auto-start at Windows boot, "Add tools to PATH" (writes user PATH in HKCU registry), built-in text editor for service config files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The installer is 5.4 MB, per-user (no admin needed), and lives at &lt;a href="https://github.com/imtaqin/goampp/releases" rel="noopener noreferrer"&gt;github.com/imtaqin/goampp/releases&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;A few honest retrospectives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick a UI library that doesn't make you fight padding.&lt;/strong&gt; windigo is great for what it is, but laying out 12 cards in a 4×3 grid by hand-positioning every Static and Button taught me a lot about why Flutter and SwiftUI exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the elevation helper from day one.&lt;/strong&gt; I shipped the first version without a "Restart as Admin" button and immediately needed it the first time I tried to write the hosts file. Adding it later was easy; explaining to users why the vhost Apply button silently failed was not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't assume &lt;code&gt;wmic&lt;/code&gt; is on every Windows install.&lt;/strong&gt; Past me would have been less embarrassed by an empty &lt;code&gt;.gitignore&lt;/code&gt; than by a startup sweep that silently no-op'd on every Win11 22H2 machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building a desktop tool in Go and you want native Win32 widgets without the Electron tax, the answer is windigo + a lot of patience with &lt;code&gt;WM_*&lt;/code&gt; messages. Total code for GoAMPP including the installer script is around 6,000 lines.&lt;/p&gt;

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

&lt;p&gt;Latest installer: &lt;strong&gt;&lt;a href="https://github.com/imtaqin/goampp/releases/latest" rel="noopener noreferrer"&gt;github.com/imtaqin/goampp/releases/latest&lt;/a&gt;&lt;/strong&gt; (5.4 MB)&lt;/p&gt;

&lt;p&gt;Source: &lt;strong&gt;&lt;a href="https://github.com/imtaqin/goampp" rel="noopener noreferrer"&gt;github.com/imtaqin/goampp&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Issues, PRs, and questions all welcome. If you hit a download URL that's gone stale, file an issue with the service name and I'll bump the catalog.&lt;/p&gt;

</description>
      <category>xampp</category>
      <category>localdev</category>
      <category>php</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I manage 11 Cloudflare domains from my phone now</title>
      <dc:creator>Taqin</dc:creator>
      <pubDate>Sat, 04 Apr 2026 11:42:53 +0000</pubDate>
      <link>https://dev.to/abdulm/i-manage-11-cloudflare-domains-from-my-phone-now-27en</link>
      <guid>https://dev.to/abdulm/i-manage-11-cloudflare-domains-from-my-phone-now-27en</guid>
      <description>&lt;p&gt;Managing 11 Cloudflare domains used to mean opening the dashboard on a laptop every single time.&lt;/p&gt;

&lt;p&gt;Change a DNS record? Laptop. Purge cache? Laptop. Check if traffic is spiking? Laptop.&lt;/p&gt;

&lt;p&gt;Got tired of it.&lt;/p&gt;

&lt;p&gt;So I made &lt;strong&gt;CloudFlare Mobile&lt;/strong&gt; — an Android app that gives you full control of your Cloudflare account from your phone.&lt;/p&gt;

&lt;p&gt;Open source. MIT license. No tracking. No telemetry.&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%2F7fqkw34e4dcm44b3pom2.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%2F7fqkw34e4dcm44b3pom2.png" alt="Dashboard dark and light mode" width="800" height="1796"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Zone Management
&lt;/h3&gt;

&lt;p&gt;All your domains in one scrollable list. Search by name, see status badges (active/pending), nameserver count. Tap any zone to dive into its settings.&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%2F4b9odfr7ndrtfiwfeniw.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%2F4b9odfr7ndrtfiwfeniw.png" alt="Zones list" width="800" height="1796"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  DNS Records
&lt;/h3&gt;

&lt;p&gt;Full CRUD. A, AAAA, CNAME, MX, TXT, SRV — all record types supported. Toggle Cloudflare proxy (orange cloud) on or off per record. Filter by record type. Search by name.&lt;/p&gt;

&lt;p&gt;Adding a record is the same flow as the dashboard — pick type, enter name, content, TTL, proxy status, done.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSL/TLS
&lt;/h3&gt;

&lt;p&gt;Switch encryption mode between Off, Flexible, Full, and Full (Strict). See current certificate status and HTTPS settings. No more fumbling through the web dashboard to find that one toggle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firewall
&lt;/h3&gt;

&lt;p&gt;View and manage firewall rules, IP access rules, rate limiting. See which rules are active. Block or challenge traffic patterns when you spot something suspicious in analytics — right there on your phone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache
&lt;/h3&gt;

&lt;p&gt;One-tap purge all cache. Toggle development mode when you're pushing updates and need to bypass cache for 3 hours. Check cache level and browser TTL settings.&lt;/p&gt;

&lt;p&gt;This one alone saves me multiple laptop opens per week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analytics
&lt;/h3&gt;

&lt;p&gt;Traffic volume, bandwidth usage, cache hit ratio, threat overview. Quick health check on how your sites are performing without logging into the dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workers, KV, R2, Pages
&lt;/h3&gt;

&lt;p&gt;View Worker scripts. Browse KV namespaces. Check R2 buckets. Monitor Pages projects and their latest deployments.&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%2Fu8ekzfupjze23iaq74fi.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%2Fu8ekzfupjze23iaq74fi.png" alt="Services" width="800" height="1796"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Account
&lt;/h3&gt;

&lt;p&gt;Got personal and work Cloudflare accounts? Switch between them in settings. No need to log out and back in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Page Rules
&lt;/h3&gt;

&lt;p&gt;Configure URL-based rules per zone. Forwarding, cache level overrides, SSL mode per path — all manageable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;p&gt;This matters. You're handing an app your Cloudflare API credentials.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supports &lt;strong&gt;API Token&lt;/strong&gt; (scoped, recommended) and &lt;strong&gt;Global API Key&lt;/strong&gt; authentication&lt;/li&gt;
&lt;li&gt;Credentials are stored locally on your device using &lt;strong&gt;Android Keystore&lt;/strong&gt; — hardware-backed encryption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing&lt;/strong&gt; gets sent anywhere except directly to &lt;code&gt;api.cloudflare.com&lt;/code&gt; over HTTPS&lt;/li&gt;
&lt;li&gt;No analytics SDK. No tracking. No telemetry. No Firebase. No Sentry. Nothing.&lt;/li&gt;
&lt;li&gt;The app is open source — you can read every line of code before trusting it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use API Tokens with scoped permissions. You should too. Create a token at &lt;code&gt;dash.cloudflare.com/profile/api-tokens&lt;/code&gt; with only the permissions you need.&lt;/p&gt;




&lt;h2&gt;
  
  
  The UI
&lt;/h2&gt;

&lt;p&gt;Dark mode, light mode, or follow system. Switches instantly.&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%2Ffyb9ykik1y7uoibf28sy.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%2Ffyb9ykik1y7uoibf28sy.png" alt="Settings" width="800" height="1796"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;English and Bahasa Indonesia. More languages easy to add — it's just JSON files with i18next.&lt;/p&gt;

&lt;p&gt;Sensitive data (emails, account IDs) are masked by default in the UI. There's a toggle to reveal them in settings.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;For those who care about what's under the hood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React Native 0.81&lt;/strong&gt; + &lt;strong&gt;Expo 54&lt;/strong&gt; — cross-platform framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Router&lt;/strong&gt; — file-based navigation, feels native&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Secure Store&lt;/strong&gt; — wraps Android Keystore for credential storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Axios&lt;/strong&gt; — HTTP client hitting Cloudflare API v4 directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;i18next&lt;/strong&gt; — internationalization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;react-native-svg&lt;/strong&gt; — entire icon system is custom SVG, no icon font dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; — full type safety across the app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No state management library. Just React Context for auth and theme. Keeps it simple.&lt;/p&gt;

&lt;p&gt;The Cloudflare API client (&lt;code&gt;services/cloudflare.ts&lt;/code&gt;) has 100+ typed functions covering zones, DNS, SSL, firewall, cache, analytics, Workers, KV, R2, Pages, and more.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to get your API Token
&lt;/h2&gt;

&lt;p&gt;This is where most people get stuck, so here's the exact steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://dash.cloudflare.com" rel="noopener noreferrer"&gt;dash.cloudflare.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click your profile icon (top right) &amp;gt; &lt;strong&gt;My Profile&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;API Tokens&lt;/strong&gt; in the sidebar&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Token&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose a template or create custom:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Zone:Read&lt;/code&gt; — view zones&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DNS:Read/Edit&lt;/code&gt; — manage DNS records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Zone Settings:Read/Edit&lt;/code&gt; — SSL, cache, etc.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Firewall Services:Read/Edit&lt;/code&gt; — firewall rules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Analytics:Read&lt;/code&gt; — view analytics&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Worker Scripts:Read/Edit&lt;/code&gt; — manage Workers&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Continue to summary&lt;/strong&gt; &amp;gt; &lt;strong&gt;Create Token&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy the token — you only see it once&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Paste it into the app. Done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Known limitations
&lt;/h2&gt;

&lt;p&gt;Being transparent about what this can't do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — Cloudflare API allows 1,200 requests per 5 minutes. Heavy usage may hit the limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No offline mode&lt;/strong&gt; — everything is fetched live from the API. No local caching of zone data yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workers editor&lt;/strong&gt; — you can view scripts but can't edit code inline. Use the web dashboard for that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS&lt;/strong&gt; — not tested. Built Android-first. Should work with Expo but hasn't been validated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-factor auth&lt;/strong&gt; — the app doesn't handle 2FA directly. API Tokens bypass 2FA by design.&lt;/li&gt;
&lt;/ul&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/imtaqin/CFMobile.git
&lt;span class="nb"&gt;cd &lt;/span&gt;CFMobile
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npx expo start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scan the QR code with Expo Go, or run on an emulator with &lt;code&gt;npx expo run:android&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Some things on my mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Offline caching for zone and DNS data&lt;/li&gt;
&lt;li&gt;Push notifications for zone status changes&lt;/li&gt;
&lt;li&gt;Bulk DNS record operations&lt;/li&gt;
&lt;li&gt;More languages&lt;/li&gt;
&lt;li&gt;iOS testing and App Store submission&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open to suggestions. What would you actually use?&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/imtaqin/CFMobile" rel="noopener noreferrer"&gt;github.com/imtaqin/CFMobile&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not affiliated with Cloudflare, Inc. This is a personal project.&lt;/p&gt;




&lt;p&gt;If you manage Cloudflare domains and want to try it, the repo is public. Star it if you find it useful, open an issue if something breaks.&lt;/p&gt;

</description>
      <category>cloudflarechallenge</category>
      <category>reactnative</category>
      <category>api</category>
      <category>mobile</category>
    </item>
    <item>
      <title>I Spent 3 Years Learning How Big Tech Scales Their Databases. Here's What Actually Matters.</title>
      <dc:creator>Taqin</dc:creator>
      <pubDate>Tue, 03 Feb 2026 15:28:56 +0000</pubDate>
      <link>https://dev.to/abdulm/i-spent-3-years-learning-how-big-tech-scales-their-databases-heres-what-actually-matters-3dba</link>
      <guid>https://dev.to/abdulm/i-spent-3-years-learning-how-big-tech-scales-their-databases-heres-what-actually-matters-3dba</guid>
      <description>&lt;p&gt;Everyone talks about scaling. Most people have no idea what it actually means.&lt;/p&gt;

&lt;p&gt;I used to think scaling meant buying a bigger server. More RAM. Faster CPU. Problem solved. Then I started working on systems that handle millions of requests per second. Turns out, you can't just buy your way out of scale problems.&lt;/p&gt;

&lt;p&gt;Let me tell you what actually happens when you need to store billions of records and serve them fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment everything breaks
&lt;/h2&gt;

&lt;p&gt;Picture this. Your app is growing. Users are happy. Then one day, your database starts choking. Queries that took 50ms now take 5 seconds. Your single PostgreSQL instance is sweating bullets trying to handle 10,000 queries per second.&lt;/p&gt;

&lt;p&gt;You throw money at it. Bigger instance. More memory. It helps for a month. Then you're back to square one.&lt;/p&gt;

&lt;p&gt;This is where most startups panic. And this is exactly where big tech companies figured out something different decades ago.&lt;/p&gt;

&lt;p&gt;They stopped thinking vertically. They started thinking horizontally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The big idea that changes everything
&lt;/h2&gt;

&lt;p&gt;Instead of one massive database, you use hundreds of smaller ones.&lt;/p&gt;

&lt;p&gt;Sounds simple. It's not.&lt;/p&gt;

&lt;p&gt;The concept is called sharding. You take your data and split it across multiple databases. User 1 through 1 million goes to database A. User 1 million to 2 million goes to database B. And so on.&lt;/p&gt;

&lt;p&gt;Now instead of one database handling everything, you have 50 databases each handling a small piece. Your capacity just multiplied by 50.&lt;/p&gt;

&lt;p&gt;Google does this. Facebook does this. Every company operating at serious scale does this.&lt;/p&gt;

&lt;p&gt;But here's what nobody tells you about sharding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ugly parts nobody talks about
&lt;/h2&gt;

&lt;p&gt;Sharding sounds great until you actually implement it.&lt;/p&gt;

&lt;p&gt;First problem. How do you decide which data goes where? If you shard by user ID, what happens when you need to query across multiple users? You're suddenly hitting 50 databases for one request.&lt;/p&gt;

&lt;p&gt;Second problem. What happens when you need more shards? You've got 50 databases and they're all getting full. Now you need to move data around without taking the whole system down. This is called resharding and it's a nightmare.&lt;/p&gt;

&lt;p&gt;Third problem. Transactions across shards are basically impossible. Try updating two records that live on different databases atomically. Good luck.&lt;/p&gt;

&lt;p&gt;I've seen teams spend months fixing issues they created by rushing into sharding without understanding these tradeoffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack that actually works at scale
&lt;/h2&gt;

&lt;p&gt;After seeing what works and what doesn't, here's what I'd use today if I needed to build something that scales to billions of records.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For the database layer&lt;/strong&gt;, you've got a few solid options.&lt;/p&gt;

&lt;p&gt;CockroachDB or Google Spanner if you need SQL and horizontal scaling. They handle the sharding complexity for you. Your app talks to it like a normal database but underneath it's distributed across machines.&lt;/p&gt;

&lt;p&gt;Vitess if you're already on MySQL and can't migrate. YouTube runs on this. It's battle-tested at insane scale.&lt;/p&gt;

&lt;p&gt;Cassandra or ScyllaDB if you're write-heavy and can live with eventual consistency. These things can handle millions of writes per second if you design your data model right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For caching&lt;/strong&gt;, Redis Cluster is the standard. Most of your reads should hit cache and never touch the database. A good caching strategy can eliminate 90% of your database load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For coordination&lt;/strong&gt;, etcd or ZooKeeper. Someone needs to keep track of which shard lives where and which nodes are alive. These tools handle that.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a request actually flows through this mess
&lt;/h2&gt;

&lt;p&gt;Let me walk you through what happens when a user loads their profile on a system like this.&lt;/p&gt;

&lt;p&gt;Request hits a load balancer. Gets routed to one of hundreds of API servers.&lt;/p&gt;

&lt;p&gt;API server checks Redis cache first. "Do I already have this user's profile cached?" If yes, return it immediately. Request done in 2ms.&lt;/p&gt;

&lt;p&gt;Cache miss. Now we need the database. But which one? The shard router looks at the user ID, does some math, figures out this user lives on shard 47.&lt;/p&gt;

&lt;p&gt;Query goes to shard 47. Gets the data. Response flows back up.&lt;/p&gt;

&lt;p&gt;On the way back, we populate the cache so next time we don't hit the database.&lt;/p&gt;

&lt;p&gt;The whole thing takes maybe 50ms on a cache miss. 2ms on a cache hit.&lt;/p&gt;

&lt;p&gt;Multiply this by millions of concurrent users and you see why every layer matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stuff that keeps engineers up at night
&lt;/h2&gt;

&lt;p&gt;The technology is honestly the easy part. The operational challenges are what kill you.&lt;/p&gt;

&lt;p&gt;Backups at this scale are wild. You can't just dump a petabyte of data to a file. You need incremental backups, point-in-time recovery, and the ability to restore individual shards without touching others.&lt;/p&gt;

&lt;p&gt;Monitoring becomes critical. With 100 database instances, you need to know immediately when one starts acting weird. You need dashboards. Alerts. Automated failover.&lt;/p&gt;

&lt;p&gt;Schema migrations are terrifying. Changing a column on a billion-row table that's actively serving traffic? You need online schema change tools and a lot of patience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to actually start
&lt;/h2&gt;

&lt;p&gt;If you're building something new and thinking about scale, here's my honest advice.&lt;/p&gt;

&lt;p&gt;Don't prematurely optimize. Start with a single PostgreSQL instance. It can handle more than you think. Most apps never actually need to shard.&lt;/p&gt;

&lt;p&gt;But design with distribution in mind. Use UUIDs instead of auto-increment IDs. Keep your queries simple. Don't rely on joins that would be impossible across shards.&lt;/p&gt;

&lt;p&gt;When you actually need to scale, evaluate CockroachDB or PlanetScale first. They handle a lot of the distributed complexity so you don't have to.&lt;/p&gt;

&lt;p&gt;Add Redis caching early. It's easy to implement and gives you massive headroom before you need to think about database scaling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real secret
&lt;/h2&gt;

&lt;p&gt;Here's what took me years to understand.&lt;/p&gt;

&lt;p&gt;Scaling isn't really a technical problem. It's a design problem.&lt;/p&gt;

&lt;p&gt;The companies that scale well aren't the ones with the fanciest technology. They're the ones who designed their systems to be distributed from the start. They thought about data locality. They minimized cross-service dependencies. They built stateless services that can run anywhere.&lt;/p&gt;

&lt;p&gt;Get the design right and scaling becomes manageable. Get it wrong and no amount of technology will save you.&lt;/p&gt;




&lt;p&gt;What's the hardest scaling problem you've faced? I'm curious what others have run into.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>database</category>
    </item>
    <item>
      <title>I Spent Hours Googling Port Forwarding. Then I Found Cloudflare Tunnel</title>
      <dc:creator>Taqin</dc:creator>
      <pubDate>Sun, 11 Jan 2026 09:06:51 +0000</pubDate>
      <link>https://dev.to/abdulm/i-spent-hours-googling-port-forwarding-then-i-found-cloudflare-tunnel-k03</link>
      <guid>https://dev.to/abdulm/i-spent-hours-googling-port-forwarding-then-i-found-cloudflare-tunnel-k03</guid>
      <description>&lt;p&gt;I have this internal service running on a server behind NAT. Private IP, no public access. The usual nightmare.&lt;/p&gt;

&lt;p&gt;My ISP doesn't give me a static IP. My router's port forwarding UI looks like it was designed in 2003. And every time I finally get it working, my IP changes and everything breaks.&lt;/p&gt;

&lt;p&gt;Then I found Cloudflare Tunnel. Took me 10 minutes to expose my service to the internet with a custom domain. No port forwarding. No static IP needed. Free.&lt;/p&gt;

&lt;p&gt;Here's exactly how I did it.&lt;/p&gt;

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

&lt;p&gt;I had a service running on &lt;code&gt;192.168.39.231:8088&lt;/code&gt;. HTTPS with a self-signed cert. I needed to access it from anywhere, ideally at a nice domain like &lt;code&gt;myapp.mydomain.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The traditional approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get a static IP (costs money)&lt;/li&gt;
&lt;li&gt;Forward ports on your router (pain in the ass)&lt;/li&gt;
&lt;li&gt;Set up dynamic DNS (another thing to maintain)&lt;/li&gt;
&lt;li&gt;Deal with SSL certs (Let's Encrypt works but still annoying)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or just use Cloudflare Tunnel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Tunnel (The Fast Way)
&lt;/h2&gt;

&lt;p&gt;If you just want to test something quick, there's a zero-config option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudflared tunnel &lt;span class="nt"&gt;--url&lt;/span&gt; https://192.168.39.231:8088 &lt;span class="nt"&gt;--no-tls-verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Cloudflared gives you a random URL like &lt;code&gt;alice-ion-married-knights.trycloudflare.com&lt;/code&gt;. Your service is now public.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--no-tls-verify&lt;/code&gt; flag is important if your origin has a self-signed cert. Without it, cloudflared refuses to connect because it can't verify the certificate.&lt;/p&gt;

&lt;p&gt;But random URLs suck. Let's set up a proper custom domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Named Tunnel (The Right Way)
&lt;/h2&gt;

&lt;p&gt;You need a Cloudflare account and your domain added to Cloudflare for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Login&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudflared tunnel login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opens a browser. You authenticate. It downloads a cert to &lt;code&gt;~/.cloudflared/cert.pem&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Create the tunnel&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudflared tunnel create my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tunnel credentials written to /root/.cloudflared/8a0b7c44-4677-421e-810b-f5e6de9d0555.json
Created tunnel my-app with id 8a0b7c44-4677-421e-810b-f5e6de9d0555
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save that tunnel ID. You need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Route your domain&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudflared tunnel route dns my-app app.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a CNAME record in Cloudflare pointing your subdomain to the tunnel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Create config&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;~/.cloudflared/config.yml&lt;/code&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;tunnel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app&lt;/span&gt;
&lt;span class="na"&gt;credentials-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/root/.cloudflared/8a0b7c44-4677-421e-810b-f5e6de9d0555.json&lt;/span&gt;

&lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app.yourdomain.com&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://192.168.39.231:8088&lt;/span&gt;
    &lt;span class="na"&gt;originRequest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;noTLSVerify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http_status:404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the tunnel ID with yours. Replace the hostname with yours.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;noTLSVerify: true&lt;/code&gt; is the config file equivalent of &lt;code&gt;--no-tls-verify&lt;/code&gt;. You need this for self-signed certs.&lt;/p&gt;

&lt;p&gt;The last ingress rule catches everything else and returns 404. It's required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Run it&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudflared tunnel run my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Your service is now live at &lt;code&gt;https://app.yourdomain.com&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Issues I Hit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Unable to reach the origin service"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your service isn't running, or cloudflared can't reach it. Test with:&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;-k&lt;/span&gt; https://192.168.39.231:8088
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If curl can't reach it, cloudflared can't either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"tls: failed to verify certificate"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your origin has a self-signed cert. Add &lt;code&gt;--no-tls-verify&lt;/code&gt; for quick tunnels or &lt;code&gt;noTLSVerify: true&lt;/code&gt; in config for named tunnels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"credentials file doesn't exist"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You forgot to replace &lt;code&gt;&amp;lt;TUNNEL-ID&amp;gt;&lt;/code&gt; in the config with your actual tunnel ID. The credentials file path must match exactly what was created in step 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running as a Service
&lt;/h2&gt;

&lt;p&gt;You probably don't want to keep a terminal open forever. Install cloudflared as a system service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudflared service &lt;span class="nb"&gt;install
&lt;/span&gt;systemctl start cloudflared
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;cloudflared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it runs on boot and restarts if it crashes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This is Better
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No port forwarding headaches&lt;/li&gt;
&lt;li&gt;No static IP needed&lt;/li&gt;
&lt;li&gt;No dynamic DNS bullshit&lt;/li&gt;
&lt;li&gt;Free SSL from Cloudflare&lt;/li&gt;
&lt;li&gt;Works behind strict NATs and firewalls&lt;/li&gt;
&lt;li&gt;The tunnel connects outbound, so it works even when inbound ports are blocked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I should have found this years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you're exposing internal services to the internet the old way, stop. Cloudflare Tunnel takes 10 minutes to set up and just works.&lt;/p&gt;

&lt;p&gt;The quick tunnel is great for testing. Named tunnels with custom domains are what you want for anything real.&lt;/p&gt;

&lt;p&gt;What internal services are you exposing? Hit me up - curious what others are running through tunnels.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>devops</category>
      <category>cloud</category>
      <category>networking</category>
    </item>
    <item>
      <title>I Built a WhatsApp Gateway in Rust Because Node.js Wasn't Cutting It</title>
      <dc:creator>Taqin</dc:creator>
      <pubDate>Thu, 01 Jan 2026 08:29:03 +0000</pubDate>
      <link>https://dev.to/abdulm/i-built-a-whatsapp-gateway-in-rust-because-nodejs-wasnt-cutting-it-3910</link>
      <guid>https://dev.to/abdulm/i-built-a-whatsapp-gateway-in-rust-because-nodejs-wasnt-cutting-it-3910</guid>
      <description>&lt;p&gt;I was running 50+ WhatsApp sessions on a single Node.js server.&lt;/p&gt;

&lt;p&gt;Memory usage? 8GB. CPU? Constantly spiking. And every few hours, something would crash.&lt;/p&gt;

&lt;p&gt;So I rewrote the whole thing in Rust. Now it handles 200+ sessions on 512MB RAM.&lt;/p&gt;

&lt;p&gt;Here's the whole story.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With Every WhatsApp API Out There
&lt;/h2&gt;

&lt;p&gt;Every WhatsApp gateway I found had the same issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baileys + Node.js&lt;/strong&gt; — Works great until you scale. Then memory leaks eat you alive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python solutions&lt;/strong&gt; — Slow. Really slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid APIs&lt;/strong&gt; — $50/month per number? Fuck that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed something that could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle hundreds of sessions simultaneously&lt;/li&gt;
&lt;li&gt;Not crash every 6 hours&lt;/li&gt;
&lt;li&gt;Actually be fast&lt;/li&gt;
&lt;li&gt;Run on a $5 VPS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing fit. So I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Rust?
&lt;/h2&gt;

&lt;p&gt;I know, I know. "Rust is hard." "Just use Go."&lt;/p&gt;

&lt;p&gt;But here's the thing — Rust's memory safety isn't just a flex. When you're managing hundreds of WebSocket connections and encryption keys, you &lt;em&gt;need&lt;/em&gt; guarantees.&lt;/p&gt;

&lt;p&gt;Plus, the &lt;code&gt;whatsapp-rust&lt;/code&gt; library exists. Someone already did the hard part of reverse-engineering the WhatsApp Web protocol. I just needed to wrap it in an API.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built: WA-RS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WA-RS&lt;/strong&gt; is a multi-session WhatsApp REST API gateway. One server, unlimited WhatsApp accounts.&lt;/p&gt;

&lt;p&gt;The stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Rust (Nightly)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web Framework&lt;/td&gt;
&lt;td&gt;Axum 0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Templates&lt;/td&gt;
&lt;td&gt;Askama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API Docs&lt;/td&gt;
&lt;td&gt;OpenAPI 3.0 / Swagger&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Features That Actually Matter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Multi-session — Run 100+ WhatsApp accounts on one server
✓ QR Code &amp;amp; Pair Code — Two ways to link devices
✓ Rich Messages — Text, images, video, audio, docs, stickers, location
✓ Webhooks — Real-time events with HMAC-SHA256 signatures
✓ Web Dashboard — Visual management, no CLI needed
✓ Swagger UI — Test endpoints without Postman
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Getting Started (It's Stupid Simple)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Docker (Recommended)
&lt;/h3&gt;

&lt;p&gt;Pull from Docker Hub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull fdciabdul/wa-rs:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 2: Docker Compose (Full Stack)
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wa-rs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fdciabdul/wa-rs:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3451:3451"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_HOST=postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PORT=5432&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=wagateway&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;JWT_SECRET=change-this-in-production&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;wa_sessions:/app/whatsapp_sessions&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;

  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=wagateway&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pg_data:/var/lib/postgresql/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wa_sessions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pg_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Server running on &lt;code&gt;http://localhost:3451&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Build From Source
&lt;/h3&gt;

&lt;p&gt;For the masochists who want to compile themselves:&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;# Clone the repo&lt;/span&gt;
git clone https://github.com/fdciabdul/wa-rs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;wa-rs

&lt;span class="c"&gt;# Install Rust nightly (required)&lt;/span&gt;
rustup default nightly

&lt;span class="c"&gt;# Set up your .env&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# Edit .env with your Postgres credentials&lt;/span&gt;

&lt;span class="c"&gt;# Build and run&lt;/span&gt;
cargo run &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The API
&lt;/h2&gt;

&lt;p&gt;All endpoints require authentication. Get your token first (see Authentication section below).&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a Session
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "id": "my-session",
    "name": "My WhatsApp",
    "webhook": {
      "url": "https://your-server.com/webhook",
      "secret": "webhook-secret",
      "events": ["message", "connected", "disconnected"]
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Connect a Session
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/connect &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Get QR Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3451/api/v1/sessions/my-session/qr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns base64 PNG. Scan it with WhatsApp. Done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get Pair Code (Alternative to QR)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/pair-code &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"phone": "628123456789"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns an 8-digit code. Enter it in WhatsApp → Settings → Linked Devices → Link with phone number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Send a Text Message
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/messages/text &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789",
    "text": "Hello from WA-RS!"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send an Image
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/messages/image &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789",
    "url": "https://example.com/image.jpg",
    "caption": "Check this out!"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send a Video
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/messages/video &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789",
    "url": "https://example.com/video.mp4",
    "caption": "Watch this"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send Audio
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/messages/audio &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789",
    "url": "https://example.com/audio.mp3",
    "ptt": true
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;ptt: true&lt;/code&gt; for voice note style, &lt;code&gt;false&lt;/code&gt; for regular audio file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Send a Document
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/messages/document &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789",
    "url": "https://example.com/document.pdf",
    "filename": "invoice.pdf",
    "caption": "Here is the invoice"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send a Sticker
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/messages/sticker &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789",
    "url": "https://example.com/sticker.webp"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send Location
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/messages/location &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789",
    "latitude": -6.2088,
    "longitude": 106.8456,
    "name": "Jakarta",
    "address": "Jakarta, Indonesia"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send Contact Card
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/messages/contact &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789",
    "contact": {
      "display_name": "John Doe",
      "phones": [
        {"number": "+1234567890", "phone_type": "CELL"}
      ]
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Edit a Message
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/messages/edit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789",
    "message_id": "3EB0ABC123...",
    "text": "Updated message text"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  React to a Message
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/messages/react &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789",
    "message_id": "3EB0ABC123...",
    "emoji": "👍"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Disconnect a Session
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/disconnect &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Delete a Session
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE http://localhost:3451/api/v1/sessions/my-session &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Dashboard
&lt;/h2&gt;

&lt;p&gt;I hate CLIs for management. So I built a web dashboard.&lt;/p&gt;

&lt;p&gt;Go to &lt;code&gt;http://localhost:3451/dashboard&lt;/code&gt; and you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session overview&lt;/strong&gt; — See all accounts, connection status at a glance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create sessions&lt;/strong&gt; — Point and click, no curl commands needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QR codes&lt;/strong&gt; — Scan right from the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pair codes&lt;/strong&gt; — Link with phone number instead of QR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook config&lt;/strong&gt; — Set up integrations visually&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settings&lt;/strong&gt; — View your API token, endpoints, version info&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design is dark mode only. Because light mode is a war crime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboard Routes
&lt;/h3&gt;

&lt;p&gt;All public, no auth needed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;What it shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dashboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Main overview with stats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dashboard/sessions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dashboard/sessions/new&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create new session form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dashboard/sessions/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session detail, QR code, actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dashboard/settings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;API token, endpoints, version&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Authentication
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Getting Your Token
&lt;/h3&gt;

&lt;p&gt;On first startup, the server generates a superadmin token. Two ways to find it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Check the logs&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs wa-rs | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"SUPERADMIN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│  SUPERADMIN TOKEN                                            │
├─────────────────────────────────────────────────────────────┤
│  eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdXBlcmFkbWluIiwicm9sZSI... │
├─────────────────────────────────────────────────────────────┤
│  Tip: Set SUPERADMIN_TOKEN in .env to use a fixed token     │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 2: Dashboard&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to &lt;code&gt;http://localhost:3451/dashboard/settings&lt;/code&gt; — the token is right there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the Token
&lt;/h3&gt;

&lt;p&gt;Include it in every API request:&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;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://localhost:3451/api/v1/sessions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using Swagger UI
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;code&gt;http://localhost:3451/swagger-ui&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;Authorize&lt;/strong&gt; button (top right)&lt;/li&gt;
&lt;li&gt;Paste your token&lt;/li&gt;
&lt;li&gt;Now all requests include the token automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Fixed Token (Production)
&lt;/h3&gt;

&lt;p&gt;Don't want a random token every restart? Set it in your environment:&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="nv"&gt;SUPERADMIN_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-fixed-token-here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Webhooks
&lt;/h2&gt;

&lt;p&gt;Real-time events. When someone messages your WhatsApp, your server knows instantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supported Events
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;When it fires&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;message&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Incoming message received&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session connected to WhatsApp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disconnected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session disconnected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;receipt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Message delivered/read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;presence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Contact online/offline status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qr_code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New QR code generated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Webhook Payload Example
&lt;/h3&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;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-session"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1704067200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&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;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"628123456789@s.whatsapp.net"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3EB0ABC123..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello!"&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;h3&gt;
  
  
  Signature Verification
&lt;/h3&gt;

&lt;p&gt;Every webhook includes an &lt;code&gt;X-Signature&lt;/code&gt; header — HMAC-SHA256 of the payload using your secret.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it. Don't trust unverified webhooks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Register Webhook via API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/webhooks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://your-server.com/webhook",
    "secret": "your-webhook-secret",
    "events": ["message", "connected"]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Contacts &amp;amp; Groups
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Check if Numbers are on WhatsApp
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/contacts/check &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "phones": ["628123456789", "628987654321"]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Get Contact Info
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/contacts/info &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "phones": ["628123456789"]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Get Profile Picture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3451/api/v1/sessions/my-session/contacts/628123456789@s.whatsapp.net/picture &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  List Groups
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3451/api/v1/sessions/my-session/groups &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Get Group Info
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3451/api/v1/sessions/my-session/groups/123456789@g.us/info &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Presence &amp;amp; Chat State
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Set Online/Offline Status
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/presence/set &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "status": "available"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Options: &lt;code&gt;available&lt;/code&gt;, &lt;code&gt;unavailable&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Send Typing Indicator
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/chatstate/typing &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789@s.whatsapp.net"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send Chat State
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/chatstate/send &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "to": "628123456789@s.whatsapp.net",
    "state": "composing"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Options: &lt;code&gt;composing&lt;/code&gt;, &lt;code&gt;recording&lt;/code&gt;, &lt;code&gt;paused&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Blocking
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Get Block List
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3451/api/v1/sessions/my-session/blocking/list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Block a Contact
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/blocking/block &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "jid": "628123456789@s.whatsapp.net"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Unblock a Contact
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3451/api/v1/sessions/my-session/blocking/unblock &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "jid": "628123456789@s.whatsapp.net"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check if Blocked
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3451/api/v1/sessions/my-session/blocking/check/628123456789@s.whatsapp.net &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;Real numbers from production:&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;Node.js (Baileys)&lt;/th&gt;
&lt;th&gt;Rust (WA-RS)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Memory (50 sessions)&lt;/td&gt;
&lt;td&gt;~4GB&lt;/td&gt;
&lt;td&gt;~200MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory (200 sessions)&lt;/td&gt;
&lt;td&gt;Crashes&lt;/td&gt;
&lt;td&gt;~800MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup time&lt;/td&gt;
&lt;td&gt;3-5 seconds&lt;/td&gt;
&lt;td&gt;&amp;lt;1 second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message latency&lt;/td&gt;
&lt;td&gt;100-300ms&lt;/td&gt;
&lt;td&gt;20-50ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU usage (idle)&lt;/td&gt;
&lt;td&gt;5-15%&lt;/td&gt;
&lt;td&gt;&amp;lt;1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rust isn't just faster. It's a different league.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full API Reference
&lt;/h2&gt;

&lt;p&gt;Every endpoint at a glance:&lt;/p&gt;

&lt;h3&gt;
  
  
  Sessions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&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;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get session info&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get session status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/connect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/disconnect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disconnect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/qr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get QR code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/pair&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get pair code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/device&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get device info&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Messages
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&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;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/messages/text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/messages/image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/messages/video&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/messages/audio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send audio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/messages/document&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/messages/sticker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send sticker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/messages/location&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send location&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/messages/contact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send contact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/messages/edit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Edit message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/messages/react&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;React to message&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Contacts
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&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;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/contacts/check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check on WhatsApp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/contacts/info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get contact info&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/contacts/:jid/picture&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get profile picture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/contacts/users&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get user info&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Groups
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&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;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/groups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List groups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/groups/:jid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/groups/:jid/info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get group info&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Presence &amp;amp; Chat State
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&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;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/presence/set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set presence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/chatstate/send&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send chat state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/chatstate/typing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send typing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Blocking
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&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;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/blocking/list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get block list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/blocking/block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Block contact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/blocking/unblock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unblock contact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/blocking/check/:jid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check if blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Media
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&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;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/media/upload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Upload media&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Webhooks
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&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;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/webhooks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List webhooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/webhooks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Register webhook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/sessions/:id/webhooks/:webhook_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unregister webhook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Other
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&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;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Health check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/swagger-ui&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Swagger UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api-docs/openapi.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAPI spec&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Environment Variables
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Default&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;POSTGRES_HOST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;localhost&lt;/td&gt;
&lt;td&gt;PostgreSQL host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POSTGRES_PORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5432&lt;/td&gt;
&lt;td&gt;PostgreSQL port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POSTGRES_USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;postgres&lt;/td&gt;
&lt;td&gt;PostgreSQL user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;postgres&lt;/td&gt;
&lt;td&gt;PostgreSQL password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POSTGRES_DB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wagateway&lt;/td&gt;
&lt;td&gt;Database name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;JWT_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(generated)&lt;/td&gt;
&lt;td&gt;Token signing secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SUPERADMIN_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(generated)&lt;/td&gt;
&lt;td&gt;Fixed admin token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHATSAPP_STORAGE_PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;./whatsapp_sessions&lt;/td&gt;
&lt;td&gt;Session data directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RUST_LOG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;info&lt;/td&gt;
&lt;td&gt;Log level (debug, info, warn, error)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Session won't connect
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Check if the session exists: &lt;code&gt;GET /api/v1/sessions/:id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Try disconnecting first: &lt;code&gt;POST /api/v1/sessions/:id/disconnect&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Then reconnect: &lt;code&gt;POST /api/v1/sessions/:id/connect&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check logs: &lt;code&gt;docker compose logs wa-rs&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  QR code not appearing
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Make sure session is in &lt;code&gt;connecting&lt;/code&gt; state&lt;/li&gt;
&lt;li&gt;Wait a few seconds after calling connect&lt;/li&gt;
&lt;li&gt;Refresh the QR endpoint&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Webhooks not firing
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Verify the webhook URL is accessible from the server&lt;/li&gt;
&lt;li&gt;Check if events are configured correctly&lt;/li&gt;
&lt;li&gt;Verify signature validation isn't failing&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Database connection failed
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Check if PostgreSQL is running&lt;/li&gt;
&lt;li&gt;Verify credentials in environment variables&lt;/li&gt;
&lt;li&gt;Make sure the database exists&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is open source. MIT license. Do whatever you want with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/fdciabdul/wa-rs" rel="noopener noreferrer"&gt;https://github.com/fdciabdul/wa-rs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Don't forget to give a star 😜&lt;br&gt;
&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://wa-rs.imtaqin.id/" rel="noopener noreferrer"&gt;https://wa-rs.imtaqin.id/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker Hub:&lt;/strong&gt; &lt;a href="https://hub.docker.com/r/fdciabdul/wa-rs" rel="noopener noreferrer"&gt;https://hub.docker.com/r/fdciabdul/wa-rs&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I spent way too long fighting with Node.js memory leaks before I made this switch.&lt;/p&gt;

&lt;p&gt;If you're running WhatsApp automation at scale, stop torturing yourself. Rust handles it better. Period.&lt;/p&gt;

&lt;p&gt;The learning curve is real. But the payoff is worth it.&lt;/p&gt;

&lt;p&gt;Star the repo if this helped. Open an issue if something's broken. PRs welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://imtaqin.id" rel="noopener noreferrer"&gt;@taqin&lt;/a&gt;. Written in Rust because life's too short for garbage collection.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>whatsapp</category>
      <category>api</category>
      <category>docker</category>
    </item>
    <item>
      <title>I Built SonarQube Export Scripts So You Don't Have To</title>
      <dc:creator>Taqin</dc:creator>
      <pubDate>Mon, 29 Dec 2025 16:24:24 +0000</pubDate>
      <link>https://dev.to/abdulm/i-built-sonarqube-export-scripts-so-you-dont-have-to-1ilg</link>
      <guid>https://dev.to/abdulm/i-built-sonarqube-export-scripts-so-you-dont-have-to-1ilg</guid>
      <description>&lt;p&gt;Last week I spent 3 hours clicking through SonarQube trying to copy-paste coverage data into a spreadsheet for our sprint planning.&lt;/p&gt;

&lt;p&gt;Three. Hours.&lt;/p&gt;

&lt;p&gt;My PM wanted a simple list: which files need tests? Which lines aren't covered? &lt;/p&gt;

&lt;p&gt;SonarQube has all this data. But getting it out in a format humans can actually use? Pain in the ass.&lt;/p&gt;

&lt;p&gt;So I built export scripts that run in your browser console. No installs. No auth tokens. Just paste and go.&lt;/p&gt;

&lt;p&gt;Why SonarQube's Export Sucks&lt;/p&gt;

&lt;p&gt;Look, SonarQube is great at showing you code quality metrics. The dashboard is nice. The drill-downs work.&lt;/p&gt;

&lt;p&gt;But try to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Export all issues with file paths and line numbers&lt;/li&gt;
&lt;li&gt;Get a list of files below 80% coverage&lt;/li&gt;
&lt;li&gt;See exactly which lines need tests&lt;/li&gt;
&lt;li&gt;Share this with your team in a readable format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're fu***.&lt;/p&gt;

&lt;p&gt;The built-in export gives you JSON or CSV that's basically useless. No proper formatting. Missing context. Can't sort by what matters.&lt;/p&gt;

&lt;p&gt;I needed something better.&lt;/p&gt;

&lt;p&gt;The Hack: Browser Console Scripts&lt;/p&gt;

&lt;p&gt;Here's the thing about SonarQube - it's all browser-based. Which means the data is already there in your browser. You just need to grab it.&lt;/p&gt;

&lt;p&gt;Browser console + fetch API = instant access to everything.&lt;/p&gt;

&lt;p&gt;No backend needed. No API tokens. No complex setup.&lt;/p&gt;

&lt;p&gt;Just open DevTools (F12), paste a script, hit Enter. Done.&lt;/p&gt;

&lt;p&gt;Script 1: Export All Issues with File and Line&lt;/p&gt;

&lt;p&gt;This one gets every issue - bugs, vulnerabilities, code smells - with exact file paths and line numbers.&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;PROJECT_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-project-key&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;exportIssuesWithLines&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="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="s1"&gt; Fetching issues...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&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;allIssues&lt;/span&gt; &lt;span class="o"&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasMore&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/api/issues/search?componentKeys=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;statuses=OPEN,CONFIRMED&amp;amp;ps=500&amp;amp;p=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&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="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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&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;data&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;allIssues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allIssues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&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;issues&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
    &lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&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;issues&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;issues&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;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="o"&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;`  Fetched &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;allIssues&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; issues...`&lt;/span&gt;&lt;span class="p"&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;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ISSUES REPORT&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`Total: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;allIssues&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;\n\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;allIssues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&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;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;N/A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&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;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;severity&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;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&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="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`   File: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filePath&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="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`   Line: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;line&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="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`   Message: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`   Effort: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;effort&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;N/A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&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;blob&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;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain&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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&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="nx"&gt;download&lt;/span&gt; &lt;span class="o"&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;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-issues.txt`&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="nf"&gt;click&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;` Exported &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;allIssues&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; issues`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;exportIssuesWithLines&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does:&lt;/p&gt;

&lt;p&gt;The pagination loop is key. SonarQube limits API responses to 500 items max. So we keep calling the API with increasing page numbers until we've got everything.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;replace&lt;/code&gt; call strips the project key from the component path. SonarQube returns "PROJECT:path/to/file.ts" but we just want "path/to/file.ts".&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Blob&lt;/code&gt; and &lt;code&gt;createElement('a')&lt;/code&gt; trick downloads the file directly. No server needed. The browser does all the work.&lt;/p&gt;

&lt;p&gt;Script 2: Files That Need Coverage&lt;/p&gt;

&lt;p&gt;This one answers: "Which files need tests?"&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;PROJECT_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-project-key&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;exportFilesNeedingCoverage&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="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="s1"&gt; Fetching coverage data...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&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;allFiles&lt;/span&gt; &lt;span class="o"&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasMore&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/api/measures/component_tree?ps=500&amp;amp;asc=true&amp;amp;metricSort=coverage&amp;amp;s=metric&amp;amp;metricSortFilter=withMeasuresOnly&amp;amp;p=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;component=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;metricKeys=coverage,uncovered_lines,lines_to_cover`&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&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;data&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &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;components&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;allFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&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;components&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&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;components&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;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;page&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;filesBelow80&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;coverage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;measures&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;coverage&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="nx"&gt;coverage&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&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;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`FILES NEEDING COVERAGE (Below 80%)\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`Total: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filesBelow80&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; files\n\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;filesBelow80&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&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;coverage&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;uncoveredLines&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;linesToCover&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="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;measures&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;measure&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;measure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;coverage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;coverage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uncovered_lines&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;uncoveredLines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lines_to_cover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;linesToCover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;file&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;report&lt;/span&gt; &lt;span class="o"&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;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;filePath&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="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`   Coverage: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&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="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`   Uncovered Lines: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uncoveredLines&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="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`   Lines to Cover: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;linesToCover&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&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;blob&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;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain&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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&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="nx"&gt;download&lt;/span&gt; &lt;span class="o"&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;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-coverage.txt`&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="nf"&gt;click&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;` Exported &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filesBelow80&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; files`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;exportFilesNeedingCoverage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The breakdown:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;metricSort=coverage&lt;/code&gt; sorts files by coverage percentage (lowest first). So the files that need the most work show up first.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;metricSortFilter=withMeasuresOnly&lt;/code&gt; skips files without coverage data. Config files, type definitions, etc.&lt;/p&gt;

&lt;p&gt;The filter for files below 80% is arbitrary. Change it to whatever threshold your team uses.&lt;/p&gt;

&lt;p&gt;Script 3: Get Exact Uncovered Line Numbers&lt;/p&gt;

&lt;p&gt;This is the nuclear option. For each file, it fetches the actual source code and identifies which specific lines aren't covered.&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;PROJECT_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-project-key&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;exportUncoveredLineNumbers&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="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="s1"&gt; Fetching files...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&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;allFiles&lt;/span&gt; &lt;span class="o"&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasMore&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/api/measures/component_tree?ps=500&amp;amp;asc=true&amp;amp;metricSort=coverage&amp;amp;p=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;component=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;metricKeys=coverage,uncovered_lines`&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&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;data&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &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;components&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;allFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&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;components&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&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;components&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;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;page&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;filesBelow80&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;coverage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;measures&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;coverage&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="nx"&gt;coverage&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;80&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;`Found &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filesBelow80&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; files. Fetching line details...\n`&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;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`UNCOVERED LINES REPORT\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`Total Files: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filesBelow80&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;\n\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &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;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;filesBelow80&lt;/span&gt;&lt;span class="p"&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;coverage&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="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;measures&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;coverage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;coverage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;file&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;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`File: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filePath&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="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`Coverage: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&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;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;linesResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`/api/sources/lines?key=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;from=1&amp;amp;to=5000`&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;linesResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;linesData&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;linesResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uncoveredLines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="nx"&gt;linesData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&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;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lineHits&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coverageStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uncovered&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;uncoveredLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uncoveredLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`Uncovered Lines: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uncoveredLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;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;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`Could not fetch line details\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&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;blob&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;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain&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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&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="nx"&gt;download&lt;/span&gt; &lt;span class="o"&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;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-uncovered-lines.txt`&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="nf"&gt;click&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="s1"&gt; Export complete!&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="nf"&gt;exportUncoveredLineNumbers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this matters:&lt;/p&gt;

&lt;p&gt;When you tell a dev "AuthController.ts needs more coverage," they have no idea where to start.&lt;/p&gt;

&lt;p&gt;When you say "AuthController.ts lines 42, 67, 89-105 aren't covered," they can fix it in 10 minutes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;sources/lines&lt;/code&gt; endpoint returns every line of code with coverage status. We filter for &lt;code&gt;lineHits === 0&lt;/code&gt; which means that line never executed during tests.&lt;/p&gt;

&lt;p&gt;CSV Export (For Excel People)&lt;/p&gt;

&lt;p&gt;Some teams want spreadsheets. Fine.&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;PROJECT_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-project-key&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;exportIssuesCSV&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;allIssues&lt;/span&gt; &lt;span class="o"&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasMore&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/api/issues/search?componentKeys=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;statuses=OPEN,CONFIRMED&amp;amp;ps=500&amp;amp;p=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&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="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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&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;data&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;allIssues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allIssues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&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;issues&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
    &lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&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;issues&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;issues&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;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;page&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;csv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;File,Line,Severity,Type,Message,Effort&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;allIssues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&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;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&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="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/"/g&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;csv&lt;/span&gt; &lt;span class="o"&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;filePath&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;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;N/A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;severity&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;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&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;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;effort&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;N/A&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&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;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/csv&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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&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="nx"&gt;download&lt;/span&gt; &lt;span class="o"&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;PROJECT_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-issues.csv`&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="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;exportIssuesCSV&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The double-quote escaping (&lt;code&gt;.replace(/"/g, '""')&lt;/code&gt;) prevents CSV injection. If an issue message contains quotes, Excel won't break.&lt;/p&gt;

&lt;p&gt;How to Use These&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your SonarQube project in browser&lt;/li&gt;
&lt;li&gt;Press F12 to open DevTools&lt;/li&gt;
&lt;li&gt;Go to Console tab&lt;/li&gt;
&lt;li&gt;Paste one of these scripts&lt;/li&gt;
&lt;li&gt;Change &lt;code&gt;PROJECT_KEY&lt;/code&gt; to your project&lt;/li&gt;
&lt;li&gt;Hit Enter&lt;/li&gt;
&lt;li&gt;File downloads automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No npm install. No build step. No deployment. Just works.&lt;/p&gt;

&lt;p&gt;Why This Matters&lt;/p&gt;

&lt;p&gt;Code quality tools are pointless if the data stays trapped in a dashboard.&lt;/p&gt;

&lt;p&gt;Your PM needs to see which features are risky. Your team lead needs to plan test coverage sprints. Your devs need specific line numbers to fix.&lt;/p&gt;

&lt;p&gt;These scripts give you that. In seconds.&lt;/p&gt;

&lt;p&gt;I use them every sprint planning. Export issues on Monday. Assign them Tuesday. Done by Friday.&lt;/p&gt;

&lt;p&gt;SonarQube finally becomes useful instead of just another dashboard nobody looks at.&lt;/p&gt;

&lt;p&gt;The Complete Toolkit&lt;/p&gt;

&lt;p&gt;I've got like 10 variations of these now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Export only CRITICAL/BLOCKER issues&lt;/li&gt;
&lt;li&gt;Group issues by file&lt;/li&gt;
&lt;li&gt;Export duplicated code blocks&lt;/li&gt;
&lt;li&gt;Export security hotspots&lt;/li&gt;
&lt;li&gt;Quality gate status reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All browser console. All instant.&lt;/p&gt;

&lt;p&gt;Want them? They're in this conversation thread. Pick what you need and customize the &lt;code&gt;PROJECT_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Or build your own. The SonarQube API is actually pretty straightforward once you realize you can just... call it from the browser.&lt;/p&gt;

&lt;p&gt;Stop wasting hours on manual exports. Automate this shit and get back to building.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>tooling</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Placeholder Contributor</title>
      <dc:creator>Taqin</dc:creator>
      <pubDate>Thu, 02 Nov 2023 01:49:56 +0000</pubDate>
      <link>https://dev.to/abdulm/placeholder-contributor-3b8o</link>
      <guid>https://dev.to/abdulm/placeholder-contributor-3b8o</guid>
      <description>&lt;h3&gt;
  
  
  Intro
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Highs and Lows
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Growth
&lt;/h3&gt;

</description>
      <category>hack23contributor</category>
    </item>
    <item>
      <title>Placeholder Maintainer</title>
      <dc:creator>Taqin</dc:creator>
      <pubDate>Thu, 02 Nov 2023 01:49:22 +0000</pubDate>
      <link>https://dev.to/abdulm/placeholder-maintainer-2h7c</link>
      <guid>https://dev.to/abdulm/placeholder-maintainer-2h7c</guid>
      <description>&lt;h3&gt;
  
  
  Intro
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Project
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Highs and Lows
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Growth
&lt;/h3&gt;

</description>
      <category>hack23maintainer</category>
    </item>
    <item>
      <title>Placeholder Contributor</title>
      <dc:creator>Taqin</dc:creator>
      <pubDate>Wed, 01 Nov 2023 22:17:55 +0000</pubDate>
      <link>https://dev.to/abdulm/placeholder-contributor-59g4</link>
      <guid>https://dev.to/abdulm/placeholder-contributor-59g4</guid>
      <description>&lt;h3&gt;
  
  
  Intro
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Highs and Lows
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Growth
&lt;/h3&gt;

</description>
      <category>hack23contributor</category>
    </item>
  </channel>
</rss>
