<?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: Nicolas Keller</title>
    <description>The latest articles on DEV Community by Nicolas Keller (@nicolas_keller).</description>
    <link>https://dev.to/nicolas_keller</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2216313%2F526cf2c3-a9da-4faf-a7ed-4fd5d1b8fe54.png</url>
      <title>DEV Community: Nicolas Keller</title>
      <link>https://dev.to/nicolas_keller</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nicolas_keller"/>
    <language>en</language>
    <item>
      <title>How we built a RabbitMQ desktop client with Electron, tRPC and SQLite</title>
      <dc:creator>Nicolas Keller</dc:creator>
      <pubDate>Thu, 05 Nov 2026 14:00:11 +0000</pubDate>
      <link>https://dev.to/nicolas_keller/how-we-built-a-rabbitmq-desktop-client-with-electron-trpc-and-sqlite-3kid</link>
      <guid>https://dev.to/nicolas_keller/how-we-built-a-rabbitmq-desktop-client-with-electron-trpc-and-sqlite-3kid</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;RabbitGUI is a desktop client for RabbitMQ that runs on macOS, Windows and Linux. It is built with Electron, and this article explains how the pieces fit together. The main process talks to RabbitMQ, exposes everything to the UI through a typed tRPC API, and stores messages in a local SQLite database. The renderer is a plain React app that never touches the network or the disk itself.&lt;/p&gt;

&lt;p&gt;If you are building a developer tool on Electron, most of this applies to you too. None of it is specific to RabbitMQ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a desktop app at all?
&lt;/h2&gt;

&lt;p&gt;RabbitMQ already ships a web UI, the &lt;a href="https://rabbitgui.com/blog/a-better-rabbitmq-ui" rel="noopener noreferrer"&gt;management plugin&lt;/a&gt;. We could have built another web app on top of the same HTTP API. We didn't, for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A browser can't speak AMQP.&lt;/strong&gt; The management HTTP API is fine for listing queues and reading stats, but the interesting features need a real AMQP 0-9-1 connection over a raw TCP socket: consuming a whole queue, spying on live traffic, republishing thousands of messages with throttling. Browsers only get TCP through WebSockets, which would need an extra plugin on the broker. A Node.js process gets TCP sockets out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No server in the middle.&lt;/strong&gt; A hosted web app would need a backend that holds your broker credentials and can reach your private network. A desktop app connects from your machine directly to your broker, the same way your own services do. Credentials never leave your computer (more on that in &lt;a href="https://rabbitgui.com/blog/how-security-is-built-into-rabbitgui" rel="noopener noreferrer"&gt;how security is built into RabbitGUI&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local storage that can handle real volumes.&lt;/strong&gt; Debugging a dead-letter queue often means looking at tens of thousands of messages. We wanted to copy them once and then search, filter and group them as often as needed without loading the broker again. That calls for a real database on disk, not &lt;code&gt;localStorage&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Electron gives us all three while keeping a single language, TypeScript, from the AMQP socket down to the React components.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture in one picture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────── Electron app ────────────────────────────┐
│                                                                      │
│  Renderer process (Chromium)          Main process (Node.js)         │
│  ─────────────────────────            ──────────────────────         │
│  React 19                             tRPC router                    │
│  TanStack Router + Query   ── IPC ──▶ ├─ fetch  ──▶ Management API   │
│  tRPC client (ipcLink)     ◀───────── ├─ amqplib ─▶ AMQP 0-9-1       │
│  Monaco, React Flow,                  └─ Kysely + better-sqlite3     │
│  Recharts, Tailwind                        │                         │
│                                            ▼                         │
│                                    user-data.db (SQLite, WAL)        │
└──────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
                         Your RabbitMQ broker(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule is simple: &lt;strong&gt;the renderer only renders&lt;/strong&gt;. Anything that touches the network, the file system or a secret lives in the main process, and the renderer reaches it only through typed tRPC procedures.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main process speaks two protocols
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The management HTTP API
&lt;/h3&gt;

&lt;p&gt;Most of what you see in RabbitGUI comes from the RabbitMQ management API: queues, exchanges, bindings, policies, users, vhosts and the message rate time series. In the main process it is just &lt;code&gt;fetch&lt;/code&gt; with basic auth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="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;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="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Basic &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&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;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&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="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&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="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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Map RabbitMQ errors to typed tRPC errors (404 =&amp;gt; NOT_FOUND, etc.)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="kd"&gt;as &lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the request comes from Node.js and not from a web page, there is no CORS to configure on the broker. You point RabbitGUI at the same URL you would open in your browser, and it works.&lt;/p&gt;

&lt;p&gt;The API is paginated, so the main process walks every page before returning. The renderer always receives the full list and can sort and filter it instantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;queues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page_count&lt;/span&gt; &lt;span class="o"&gt;&amp;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;page&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;next&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;queues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page&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;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="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&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;items&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;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;data&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="nx"&gt;next&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  AMQP 0-9-1 with amqplib
&lt;/h3&gt;

&lt;p&gt;The management API has a "get messages" endpoint, but the RabbitMQ docs describe it as a diagnostics tool for sysadmins: it does not implement reliable delivery and pulls messages one HTTP request at a time. For anything involving many messages we open a real AMQP connection with &lt;a href="https://github.com/amqp-node/amqplib" rel="noopener noreferrer"&gt;amqplib&lt;/a&gt;, still from the main process.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://rabbitgui.com/blog/how-to-spy-on-real-time-queue-traffic-in-rabbitmq" rel="noopener noreferrer"&gt;live queue spy&lt;/a&gt; is a good example. To watch a queue without stealing messages from your consumers, RabbitGUI declares a temporary, exclusive queue and copies the bindings of the queue you are watching onto it. RabbitMQ then delivers a copy of each matching message to both queues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connection&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;amqp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amqpUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChannel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Server-named, exclusive, deleted when the connection closes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tempQueue&lt;/span&gt; &lt;span class="p"&gt;}&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;exclusive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Same bindings as the queue we are spying on&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;binding&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;queueBindings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;vhost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;queue&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;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bindQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tempQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;routing_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arguments&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;await&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tempQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;msg&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertInto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consumed_messages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;execute&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;Because the spy queue is exclusive and auto-delete, closing the app or the connection removes it. Nothing is left behind on your broker.&lt;/p&gt;

&lt;p&gt;The same idea powers &lt;a href="https://rabbitgui.com/blog/how-to-browse-rabbitmq-messages-without-consuming-them" rel="noopener noreferrer"&gt;browsing a queue without consuming it&lt;/a&gt;. RabbitGUI consumes the queue, writes every message to SQLite, and either requeues them or removes them, depending on the mode you pick. Requeued messages come back with the redelivered flag set, and on quorum queues with an incremented &lt;code&gt;x-delivery-count&lt;/code&gt;. So we strip those headers and hash the body plus the remaining headers. If you extract the same queue twice, you don't get duplicates in the local database.&lt;/p&gt;

&lt;h2&gt;
  
  
  tRPC over Electron IPC
&lt;/h2&gt;

&lt;p&gt;Electron's IPC is untyped. You &lt;code&gt;ipcRenderer.invoke('some-channel', args)&lt;/code&gt; and hope the main process handles that string. With a few dozen features, that gets fragile fast. So we put &lt;a href="https://trpc.io" rel="noopener noreferrer"&gt;tRPC&lt;/a&gt; on top of it with &lt;a href="https://github.com/mat-sz/trpc-electron" rel="noopener noreferrer"&gt;trpc-electron&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The main process defines ordinary tRPC routers, validated with zod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queuesRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;router&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;publicProcedure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;connexionId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// management API + local SQLite counts&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;purge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;publicProcedure&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;connexionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;vhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;purgeQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getCredentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connexionId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vhost&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;router&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connexions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;connexionsRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;queuesRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;exchanges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;exchangesRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;localMessages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localMessagesRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;spy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;spyRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;appRouter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is wired to the window with a single call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;createIPCHandler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mainWindow&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The preload script exposes the bridge (&lt;code&gt;exposeElectronTRPC()&lt;/code&gt;), and the renderer creates a client with an IPC link instead of an HTTP one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trpcClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createTRPCClient&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppRouter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ipcLink&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createTRPCOptionsProxy&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppRouter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trpcClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;queryClient&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;What we get out of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end types.&lt;/strong&gt; The renderer imports only the &lt;code&gt;AppRouter&lt;/code&gt; &lt;em&gt;type&lt;/em&gt;. Renaming a procedure or changing an input breaks the renderer build, not the app at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TanStack Query for free.&lt;/strong&gt; Every procedure becomes &lt;code&gt;useQuery(trpc.queues.list.queryOptions(connexionId))&lt;/code&gt;, with caching, refetch intervals, loading states and invalidation after mutations. Live dashboards are just a &lt;code&gt;refetchInterval&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed errors.&lt;/strong&gt; The main process maps RabbitMQ failures to &lt;code&gt;TRPCError&lt;/code&gt;s. The renderer can, for example, skip retries on a &lt;code&gt;NOT_FOUND&lt;/code&gt; when a queue was deleted under your feet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No channel names to maintain.&lt;/strong&gt; There is not a single hand-written &lt;code&gt;ipcMain.handle&lt;/code&gt; for app features.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Long-running jobs, like extracting 50,000 messages, don't block a request. The mutation starts the job in the main process and returns. The renderer polls a &lt;code&gt;status&lt;/code&gt; query that reports progress until the job is done.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQLite as the local source of truth
&lt;/h2&gt;

&lt;p&gt;Messages you extract, spy on or publish go into a SQLite database in Electron's &lt;code&gt;userData&lt;/code&gt; folder. We use &lt;a href="https://github.com/WiseLibs/better-sqlite3" rel="noopener noreferrer"&gt;better-sqlite3&lt;/a&gt; with &lt;a href="https://kysely.dev" rel="noopener noreferrer"&gt;Kysely&lt;/a&gt; as a type-safe query builder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sqlite&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;SQLite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userData&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-data.db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nx"&gt;sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pragma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;journal_mode = WAL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Kysely&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;dialect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SqliteDialect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sqlite&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;Schema changes ship as Kysely migrations defined in code and run at startup with &lt;code&gt;migrator.migrateToLatest()&lt;/code&gt;, so a user who skips a few versions still ends up with the right schema. In development, &lt;code&gt;kysely-codegen&lt;/code&gt; regenerates the &lt;code&gt;DB&lt;/code&gt; type from the real database after each migration. Queries stay typed without maintaining types by hand.&lt;/p&gt;

&lt;p&gt;The main table, &lt;code&gt;consumed_messages&lt;/code&gt;, stores the payload, the AMQP properties, and a few columns lifted out of the headers for fast filtering: delivery count, and the first and last death exchange, queue and reason from &lt;code&gt;x-death&lt;/code&gt;. Composite indexes on &lt;code&gt;(connexionId, vhost, queue, …)&lt;/code&gt; keep filters and facet counts fast on large extractions.&lt;/p&gt;

&lt;p&gt;Having the messages in SQL is what makes the &lt;a href="https://rabbitgui.com/blog/rabbit-mq-dead-letter-queues-introspection" rel="noopener noreferrer"&gt;dead-letter workflow&lt;/a&gt; possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filters, full-text search and facets ("how many messages per routing key or death reason?") are SQL queries, not something RabbitMQ can answer.&lt;/li&gt;
&lt;li&gt;You can republish a hand-picked subset, to its original queue or anywhere else, with throttling.&lt;/li&gt;
&lt;li&gt;The queue list joins live stats from the management API with local counts, so you can see which queues have messages saved on your machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The spy view keeps the newest 1,000 messages per queue and deletes older rows, so a busy queue can't fill your disk.&lt;/p&gt;

&lt;p&gt;Small settings, like connections, saved table views and appearance, live in a JSON config file managed by &lt;code&gt;electron-conf&lt;/code&gt; and not in SQLite.&lt;/p&gt;

&lt;h2&gt;
  
  
  The renderer: just a React app
&lt;/h2&gt;

&lt;p&gt;Because everything heavy happens in the main process, the renderer is a regular single-page React app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React 19&lt;/strong&gt; with &lt;strong&gt;TanStack Router&lt;/strong&gt; for file-based routes, and &lt;strong&gt;TanStack Query&lt;/strong&gt; through the tRPC proxy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; and &lt;strong&gt;Radix UI&lt;/strong&gt; primitives for the interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monaco&lt;/strong&gt;, the editor behind VS Code, for the &lt;a href="https://rabbitgui.com/blog/how-to-manually-publish-messages-to-rabbitmq" rel="noopener noreferrer"&gt;JSON message editor&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Flow&lt;/strong&gt; with &lt;strong&gt;elkjs&lt;/strong&gt; for automatic layout of the &lt;a href="https://rabbitgui.com/blog/how-to-visually-explore-rabbitmq-queue-bindings" rel="noopener noreferrer"&gt;routing graph&lt;/a&gt; of exchanges, bindings, queues and dead-letter paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recharts&lt;/strong&gt; for the message rate charts and the &lt;a href="https://rabbitgui.com/blog/rabbitmq-queue-drain-time" rel="noopener noreferrer"&gt;drain-time forecast&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no API base URL to configure, no auth token to refresh, no CORS. The renderer could run in a browser tab against a different link without code changes, which also makes it easy to iterate on with hot reload through &lt;code&gt;electron-vite&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building and shipping
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;electron-vite&lt;/strong&gt; builds the main, preload and renderer bundles with Vite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;electron-builder&lt;/strong&gt; packages the app for macOS (Apple Silicon and Intel, signed and notarized), Windows and Linux. Linux builds run in the &lt;code&gt;electronuserland/builder&lt;/code&gt; Docker image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;better-sqlite3 is a native module&lt;/strong&gt;, so it has to be compiled against Electron's Node ABI and not your system Node. &lt;code&gt;electron-builder install-app-deps&lt;/code&gt; runs on &lt;code&gt;postinstall&lt;/code&gt; to take care of that. It is the one part of the stack that occasionally needs attention after an Electron upgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;electron-updater&lt;/strong&gt; checks for a new version a few seconds after launch and installs updates in the background.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trade-offs we accepted
&lt;/h2&gt;

&lt;p&gt;Electron isn't free. The download is bigger than a native app's, and an idle window uses more memory than a terminal tool would. For a tool you open to debug a broker, we think it's the right trade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One language and one type system from the AMQP consumer to the button that triggers it.&lt;/li&gt;
&lt;li&gt;Mature Node.js libraries for exactly what we need (amqplib, better-sqlite3), running in-process with no sidecar.&lt;/li&gt;
&lt;li&gt;The same codebase and UI on macOS, Windows and Linux.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Start your own app from a boilerplate
&lt;/h2&gt;

&lt;p&gt;Wiring all of this by hand takes days: the IPC bridge, native module rebuilds, migrations, code signing, notarization and auto updates. If you want the same architecture for your own desktop app, start from a boilerplate instead.&lt;/p&gt;

&lt;p&gt;We extracted the stack behind RabbitGUI into &lt;a href="https://electron-appstack.com/" rel="noopener noreferrer"&gt;Electron AppStack&lt;/a&gt;, a free, production-ready Electron + TypeScript starter. It ships with React, tRPC over IPC, SQLite, auto updates and signed releases, plus documentation for each piece:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit nick-keller/electron-appstack/template my-app
&lt;span class="nb"&gt;cd &lt;/span&gt;my-app
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get the main process / renderer split described in this article on day one, and can spend your time on the features that make your app different.&lt;/p&gt;

&lt;p&gt;If you are curious what this architecture adds up to in practice, &lt;a href="https://rabbitgui.com/download" rel="noopener noreferrer"&gt;RabbitGUI is available for macOS, Windows and Linux&lt;/a&gt;, or have a look at &lt;a href="https://rabbitgui.com/compare/rabbitgui-vs-rabbitmq-management-ui" rel="noopener noreferrer"&gt;what it does compared to the management UI&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://rabbitgui.com/blog/how-we-built-rabbitgui-with-electron" rel="noopener noreferrer"&gt;RabbitGUI blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>electron</category>
      <category>typescript</category>
      <category>react</category>
      <category>sqlite</category>
    </item>
    <item>
      <title>A Better RabbitMQ UI: What the Management Plugin Gets Wrong</title>
      <dc:creator>Nicolas Keller</dc:creator>
      <pubDate>Thu, 05 Nov 2026 13:58:29 +0000</pubDate>
      <link>https://dev.to/nicolas_keller/a-better-rabbitmq-ui-what-the-management-plugin-gets-wrong-5fh7</link>
      <guid>https://dev.to/nicolas_keller/a-better-rabbitmq-ui-what-the-management-plugin-gets-wrong-5fh7</guid>
      <description>&lt;p&gt;RabbitMQ comes with a UI, and it does the job. But if you use RabbitMQ every day, the small frictions add up: logging in again for every environment, "getting" messages that you then have to requeue, typing JSON into a plain text area. This is the story of why we built RabbitGUI, a desktop RabbitMQ UI for Mac, Windows and Linux, and what we changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The built-in RabbitMQ UI
&lt;/h2&gt;

&lt;p&gt;Yes, RabbitMQ comes with a built-in UI, the management plugin. This is what 99% of people using RabbitMQ use today. And to be frank, it is an incredibly useful piece of software. It might be old and not the most user-friendly tool you've used, but it gets the job done. It lives at &lt;code&gt;http://localhost:15672&lt;/code&gt; (see &lt;a href="https://rabbitgui.com/blog/rabbitmq-default-port" rel="noopener noreferrer"&gt;RabbitMQ ports&lt;/a&gt; for why 15672 and not 5672), and on a fresh install you log in with the &lt;a href="https://rabbitgui.com/blog/rabbitmq-default-login" rel="noopener noreferrer"&gt;RabbitMQ default login&lt;/a&gt;, &lt;code&gt;guest&lt;/code&gt; / &lt;code&gt;guest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqm291ygb37k06hmqlk9x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqm291ygb37k06hmqlk9x.png" alt="rabbitmq management plugin screenshot" width="800" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interface is also incredibly stable. It has barely changed in a decade, and it never breaks on you as a result. If you are new to it, our &lt;a href="https://rabbitgui.com/blog/rabbitmq-management-ui" rel="noopener noreferrer"&gt;complete guide to the RabbitMQ Management UI&lt;/a&gt; walks through every tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the management plugin gets wrong
&lt;/h2&gt;

&lt;p&gt;None of these are bugs. They are design choices that made sense for a web admin panel bundled with the broker, but they hurt when the UI is your daily debugging tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  One broker per tab, one login per broker
&lt;/h3&gt;

&lt;p&gt;The management UI is served by each broker. Local, staging and production are three URLs, three logins, and three tabs that look exactly the same. Sessions expire, so you log in again. Most teams end up with a folder of bookmarks and a password manager doing the work a connection list should do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading a message means consuming it
&lt;/h3&gt;

&lt;p&gt;There is no way to look at a message without taking it off the queue. "Get messages" consumes messages and, depending on the ack mode you pick, either requeues them (which sets the &lt;code&gt;redelivered&lt;/code&gt; flag and can change their order) or drops them. You also read them one long list at a time, with no search and no filter. That is fine for one test message. It is painful for a dead-letter queue with 4,000 messages, and the wrong ack mode deletes data. We wrote more about this in &lt;a href="https://rabbitgui.com/blog/how-to-browse-rabbitmq-messages-without-consuming-them" rel="noopener noreferrer"&gt;how to browse RabbitMQ messages without consuming them&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Large payloads are truncated
&lt;/h3&gt;

&lt;p&gt;The UI cuts payloads off at 50,000 characters, so big JSON documents show up incomplete (see &lt;a href="https://rabbitgui.com/blog/rabbitmq-max-message-size" rel="noopener noreferrer"&gt;RabbitMQ max message size&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Publishing is a text area
&lt;/h3&gt;

&lt;p&gt;To send a test message you type the payload into a plain text box and the headers into key/value fields. No syntax highlighting, no validation, no formatting. One missing comma and your consumer rejects the message.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routing is spread across pages
&lt;/h3&gt;

&lt;p&gt;Bindings live on the exchange page and on the queue page. Dead-letter exchanges and alternate exchanges are hidden in arguments and policies. Names are mostly plain text, so following a message from an exchange to its queue and on to its dead-letter queue means a lot of copy, paste and filter.&lt;/p&gt;

&lt;h3&gt;
  
  
  No search across the broker
&lt;/h3&gt;

&lt;p&gt;The list pages have a filter box, but there is no global "jump to" that finds a queue, exchange or vhost from anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we needed something better
&lt;/h2&gt;

&lt;p&gt;When you use a tool every day, those frustrations add up. Combine that with the addiction of building tools, and the spark goes off pretty quickly. After fighting the urge to stop everything and build the tool I wished existed, I ended up writing the first lines of code for what would become RabbitGUI.&lt;/p&gt;

&lt;p&gt;It was a tedious start, since the management HTTP API is only lightly documented, so there was a lot of reverse engineering at first. Once the first bricks were in place, I could finally focus on the experience I had in mind. If you are curious about the internals, we wrote about &lt;a href="https://rabbitgui.com/blog/how-we-built-rabbitgui-with-electron" rel="noopener noreferrer"&gt;how we built RabbitGUI with Electron&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our proudest improvements for RabbitGUI
&lt;/h2&gt;

&lt;p&gt;Here are the features I am the most proud of, the ones that really motivated me to get RabbitGUI started. They are not just a polished version of something you could already do, but a rethink of how to get the job done much, much faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  All your connections in a single place
&lt;/h3&gt;

&lt;p&gt;This is the most common complaint about the management UI: switching environments means switching tabs and logging in again. Can you imagine doing that for database connections?&lt;/p&gt;

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

&lt;p&gt;This is the welcome screen of RabbitGUI. Just like a database tool, you add as many connections as you need and never see a login screen again. Your credentials are stored encrypted on your machine, see &lt;a href="https://rabbitgui.com/blog/how-security-is-built-into-rabbitgui" rel="noopener noreferrer"&gt;how security is built into RabbitGUI&lt;/a&gt; for how encryption and offline use work.&lt;/p&gt;

&lt;p&gt;To switch environment later, a quick access menu at the top lets you jump to any connection instantly while keeping your context.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmqhn9ngg7mqbg76odyko.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmqhn9ngg7mqbg76odyko.png" alt="quick switch between rabbitmq connections" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Finally, a way to manage dead-letter queues
&lt;/h3&gt;

&lt;p&gt;This could be an app of its own, given how bad the problem is. RabbitMQ has no way to explore the messages sitting in a queue, which makes dead-letter management tedious and painful.&lt;/p&gt;

&lt;p&gt;RabbitGUI consumes the queue locally and stores the messages in a database on your machine. With every message local, you can search and filter at will, without the constraints of RabbitMQ.&lt;/p&gt;

&lt;p&gt;Explore, search and filter messages, select the ones you are interested in, and republish only those, either to their original queue or to any queue you choose.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo1a1sujpu1ob6ro429oj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo1a1sujpu1ob6ro429oj.png" alt="screenshot explorer rabbit mq dead letter queue messages" width="800" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to know more about how RabbitGUI handles dead letters, we wrote a dedicated article: &lt;a href="https://rabbitgui.com/blog/rabbit-mq-dead-letter-queues-introspection" rel="noopener noreferrer"&gt;dead-letter queue introspection with RabbitGUI&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch live traffic without touching consumers
&lt;/h3&gt;

&lt;p&gt;To see what is flowing through a queue right now, RabbitGUI creates a temporary shadow queue with the same bindings and captures a copy of every message. Your consumers keep working as if nothing happened. The details are in &lt;a href="https://rabbitgui.com/blog/how-to-spy-on-real-time-queue-traffic-in-rabbitmq" rel="noopener noreferrer"&gt;how to spy on real-time queue traffic&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  An actual JSON editor to send messages
&lt;/h3&gt;

&lt;p&gt;This one is so obvious, I do not understand why the management plugin does not have it. Writing JSON by hand in a text area is painful, and the chance of making a mistake is high. For RabbitGUI, I took the editor from VS Code and put it right into the app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmy7vdgrzi9nigwh9fxae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmy7vdgrzi9nigwh9fxae.png" alt="json editor to publish messages to rabbitmq" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have a dedicated article that goes more in depth about &lt;a href="https://rabbitgui.com/blog/how-to-manually-publish-messages-to-rabbitmq#using-a-ui-to-send-messages-to-rabbitmq" rel="noopener noreferrer"&gt;publishing messages to RabbitMQ&lt;/a&gt;. As a bonus, it also compares it with the publish form of the management plugin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Buttery-smooth navigation
&lt;/h3&gt;

&lt;p&gt;Navigation was a big focus for RabbitGUI. I didn't see it as a single feature, but as a set of principles that make the user's life much easier.&lt;/p&gt;

&lt;p&gt;The first thing users expect is a reliable quick search from anywhere. So we implemented the classic &lt;code&gt;CMD+K&lt;/code&gt; to search for anything (queue, exchange, vhost...) using the same string matching and highlighting algorithm as WebStorm.&lt;/p&gt;

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

&lt;p&gt;Once you can jump to anything, the next cornerstone of navigation is hopping from one entity to another by clicking on it. Anytime you see the name of an entity, whether in bindings, settings, a dead-letter exchange, an alternate exchange or a policy, you can click on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgawmqh4j1grotr0vld7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgawmqh4j1grotr0vld7n.png" alt="clickable dead letter from policy in rabbitmq" width="660" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The last piece of the puzzle is managing routing in a single place. It builds on clickable names, but most importantly it groups bindings, dead-letter queues and alternate exchanges in one view. We wrote an entire &lt;a href="https://rabbitgui.com/blog/how-to-visually-explore-rabbitmq-queue-bindings" rel="noopener noreferrer"&gt;article on exploring RabbitMQ bindings&lt;/a&gt; if you are curious.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd11xycvcl0o21qtfpruu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd11xycvcl0o21qtfpruu.png" alt="a queue with bindings and a dead letter exchange in rabbitmq" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Management plugin vs RabbitGUI at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Management plugin&lt;/th&gt;
&lt;th&gt;RabbitGUI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Switch between brokers&lt;/td&gt;
&lt;td&gt;New tab and login per broker&lt;/td&gt;
&lt;td&gt;Saved connections, one click&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read messages in a queue&lt;/td&gt;
&lt;td&gt;"Get messages", consumes then requeues or drops&lt;/td&gt;
&lt;td&gt;Browse, search and filter locally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replay dead letters&lt;/td&gt;
&lt;td&gt;Shovel or a script&lt;/td&gt;
&lt;td&gt;Select messages, republish, throttle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watch live traffic&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;Spy via a temporary shadow queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publish a JSON message&lt;/td&gt;
&lt;td&gt;Plain text area&lt;/td&gt;
&lt;td&gt;VS Code editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find a queue or exchange&lt;/td&gt;
&lt;td&gt;Filter box per list page&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CMD+K&lt;/code&gt; from anywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Follow routing&lt;/td&gt;
&lt;td&gt;Bindings split across pages&lt;/td&gt;
&lt;td&gt;Clickable names, one routing view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it runs&lt;/td&gt;
&lt;td&gt;In the broker, in your browser&lt;/td&gt;
&lt;td&gt;Desktop app for Mac, Windows and Linux&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The management plugin still wins on one thing: it is already there, on every broker, for free. RabbitGUI talks to the same management API, so you keep the plugin enabled and use whichever fits the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compared with other tools
&lt;/h2&gt;

&lt;p&gt;RabbitGUI is not the only alternative. If you are evaluating options, we keep sourced, side-by-side comparisons up to date:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rabbitgui.com/best/rabbitmq-gui-clients" rel="noopener noreferrer"&gt;The best RabbitMQ GUI clients&lt;/a&gt;, an overview of every desktop, web and CLI option&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rabbitgui.com/compare/rabbitmq-management-ui-alternatives" rel="noopener noreferrer"&gt;RabbitMQ Management UI alternatives&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rabbitgui.com/compare/rabbitgui-vs-rabbitmq-management-ui" rel="noopener noreferrer"&gt;RabbitGUI vs the Management UI&lt;/a&gt;, feature by feature&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rabbitgui.com/compare/rabbitgui-vs-queueexplorer" rel="noopener noreferrer"&gt;RabbitGUI vs QueueExplorer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Those are only the features we are the most proud of. RabbitGUI is built with love, and a lot of small details went into it. For backlog ETAs, see &lt;a href="https://rabbitgui.com/blog/rabbitmq-queue-drain-time" rel="noopener noreferrer"&gt;queue drain time&lt;/a&gt;. If you want to know what is coming next, check out our &lt;a href="https://rabbitgui.com/roadmap" rel="noopener noreferrer"&gt;public roadmap&lt;/a&gt;. And if there is something you've always wanted in a RabbitMQ UI, &lt;a href="https://forms.gle/CqnMXJkHVMVMwpBX9" rel="noopener noreferrer"&gt;give us some feedback&lt;/a&gt; to shape what we build next.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://rabbitgui.com/download" rel="noopener noreferrer"&gt;download RabbitGUI&lt;/a&gt; and connect it to your local broker in a minute.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://rabbitgui.com/blog/a-better-rabbitmq-ui" rel="noopener noreferrer"&gt;RabbitGUI blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>devops</category>
      <category>tooling</category>
      <category>productivity</category>
    </item>
    <item>
      <title>After 10 years of frustration, I finally built the perfect RabbitMQ IDE</title>
      <dc:creator>Nicolas Keller</dc:creator>
      <pubDate>Thu, 21 Aug 2025 16:39:14 +0000</pubDate>
      <link>https://dev.to/nicolas_keller/after-10-years-of-frustration-i-finally-built-the-perfect-rabbitmq-ide-o0d</link>
      <guid>https://dev.to/nicolas_keller/after-10-years-of-frustration-i-finally-built-the-perfect-rabbitmq-ide-o0d</guid>
      <description>&lt;p&gt;I’ve been working with RabbitMQ for about ten years now. Over that time, I’ve relied heavily on its web UI. It’s useful, but I’ve always found it frustrating to work with day to day.&lt;/p&gt;

&lt;p&gt;Back then, many of us shared little scripts to improve the experience: adding JSON formatting, quick filters, and other small tweaks that made it a bit more practical. These scripts helped, but they always felt like band-aids.&lt;/p&gt;

&lt;p&gt;At one point, I even proposed a redesign of the web UI on the RabbitMQ Slack. The response I got was: “don’t fix what is not broken.” Looking back, that was fair. RabbitMQ itself works as it should, and its maintainers are right to keep things stable.&lt;/p&gt;

&lt;p&gt;So instead of trying to push changes upstream, I decided to build the tool I wished existed: a desktop IDE for RabbitMQ.&lt;/p&gt;

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

&lt;p&gt;The main goal is to make working with RabbitMQ more productive and less frustrating. Here are the key features I focused on first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dead letter queues management: allows you to explore, search, filter, drop, and re-publish messages as needed, even in the middle of the queue&lt;/li&gt;
&lt;li&gt;Multi-connection support: quickly switch between local, staging, and production environments.&lt;/li&gt;
&lt;li&gt;JSON editor: both for publishing messages and displaying them.&lt;/li&gt;
&lt;li&gt;Search across everything: queues, exchanges, vhosts, all searchable from one place.&lt;/li&gt;
&lt;li&gt;Interactive charts: with automatic level-of-detail adjustments when zooming.&lt;/li&gt;
&lt;li&gt;Unified routing view: Bindings, dead letters, alternate exchanges, all in a single screen.&lt;/li&gt;
&lt;li&gt;Built-in docs and formatted inputs, no more raw, hard-to-read argument lists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why a desktop app?
&lt;/h2&gt;

&lt;p&gt;I decided to make it a standalone app instead of another web UI. That way it can integrate better with the desktop environment, work offline, and handle multiple connections more cleanly.&lt;/p&gt;

&lt;p&gt;It’s available for Mac (Windows comming soon) as a one-time purchase of $29. A SaaS subscription model didn’t make sense for this kind of tool.&lt;/p&gt;

&lt;p&gt;You can check it out &lt;a href="https://rabbitgui.com/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;The roadmap is mostly driven by feedback from users and is publicly available on &lt;a href="https://rabbitgui.com/roadmap" rel="noopener noreferrer"&gt;our roadmap page&lt;/a&gt; for those interested.&lt;/p&gt;

&lt;p&gt;I’d also like to hear from other RabbitMQ users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What’s your biggest frustration with RabbitMQ today?&lt;/li&gt;
&lt;li&gt;What features would you like to see in tooling around it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your feedback would help me refine the app in next versions.&lt;/p&gt;

&lt;p&gt;Thanks for reading, and if you’ve been hacking around the RabbitMQ web UI for years like I have, I hope this project is something you’ll find useful.&lt;/p&gt;

</description>
      <category>tooling</category>
      <category>devex</category>
      <category>development</category>
    </item>
  </channel>
</rss>
