<?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: Sabry Dawood</title>
    <description>The latest articles on DEV Community by Sabry Dawood (@sabrydawood_79).</description>
    <link>https://dev.to/sabrydawood_79</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%2F2958272%2F0e7ddeda-b44b-468e-b8a7-14a18127abf3.jpg</url>
      <title>DEV Community: Sabry Dawood</title>
      <link>https://dev.to/sabrydawood_79</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sabrydawood_79"/>
    <language>en</language>
    <item>
      <title>I built a self-hosted CI/CD platform with persistent queue, encrypted secrets, and rollback UI — here's what I learned</title>
      <dc:creator>Sabry Dawood</dc:creator>
      <pubDate>Sun, 24 May 2026 20:18:07 +0000</pubDate>
      <link>https://dev.to/sabrydawood_79/i-built-a-self-hosted-cicd-platform-with-persistent-queue-encrypted-secrets-and-rollback-ui--2jph</link>
      <guid>https://dev.to/sabrydawood_79/i-built-a-self-hosted-cicd-platform-with-persistent-queue-encrypted-secrets-and-rollback-ui--2jph</guid>
      <description>&lt;p&gt;For the past several months I've been building &lt;strong&gt;Deploy Center&lt;/strong&gt;, a self-hosted CI/CD deployment platform. v3.0 shipped recently, and I want to share the architecture decisions, what worked, and what I'd do differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;a href="https://github.com/FutureSolutionDev/Deploy-Center-Server" rel="noopener noreferrer"&gt;https://github.com/FutureSolutionDev/Deploy-Center-Server&lt;/a&gt; — MIT licensed, TypeScript + Express + React + MySQL/MariaDB + Redis.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I was trying to solve
&lt;/h2&gt;

&lt;p&gt;Most small teams I've worked with deploy through one of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A bash script triggered by a cron or a manual SSH&lt;/li&gt;
&lt;li&gt;GitHub Actions running ad-hoc scripts on the target server&lt;/li&gt;
&lt;li&gt;A heavyweight platform like Jenkins that nobody wants to maintain&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first two have no audit trail, no rollback, no concept of "who is allowed to deploy what." The third needs a dedicated person to keep it healthy.&lt;/p&gt;

&lt;p&gt;I wanted something in between: &lt;strong&gt;the simplicity of "git push and it deploys" with the safety net of audit logs, RBAC, encrypted secrets, and one-click rollback.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;

&lt;p&gt;Three tiers, nothing exotic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React 19 + MUI + React Query + Socket.IO client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Express + TypeScript + Sequelize&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data:&lt;/strong&gt; MySQL/MariaDB + Redis (for the queue)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting parts are how the queue and the secrets work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistent queue with BullMQ
&lt;/h2&gt;

&lt;p&gt;Earlier versions used an in-memory queue. It worked fine until the process restarted mid-deployment — and then the deployment was just &lt;em&gt;gone&lt;/em&gt;. No log, no retry, nothing.&lt;/p&gt;

&lt;p&gt;v3.0 moved to &lt;strong&gt;BullMQ + Redis&lt;/strong&gt;. The key behaviors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jobs persist across restarts. On boot, the server does a one-shot re-enqueue of any &lt;code&gt;Queued&lt;/code&gt; deployment rows that don't have a matching active job.&lt;/li&gt;
&lt;li&gt;Retry policy is 3 attempts with exponential backoff (1s → 5s → 25s).&lt;/li&gt;
&lt;li&gt;There's a &lt;code&gt;QueueReadyMiddleware&lt;/code&gt; that 503s API requests when Redis is unreachable, so the UI gets a clean error instead of silently dropping requests.&lt;/li&gt;
&lt;li&gt;Bull Board is mounted at &lt;code&gt;/admin/queues&lt;/code&gt; (Admin-only) for inspection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Encrypted environment variables
&lt;/h2&gt;

&lt;p&gt;Every project has its own env vars table. Each row is encrypted with &lt;strong&gt;AES-256-GCM with a unique IV&lt;/strong&gt;, decrypted only at deploy time, and the values are redacted from logs by name.&lt;/p&gt;

&lt;p&gt;The encryption key lives in &lt;code&gt;.env&lt;/code&gt; as a 64-char hex string. Rotating it is a documented step (re-encrypt all rows under the new key).&lt;/p&gt;

&lt;p&gt;The trade-off: if someone has shell access to the server &lt;em&gt;and&lt;/em&gt; the env file, they have the keys. But for the "stolen DB dump" scenario — the most common breach vector for small teams — the secrets stay opaque.&lt;/p&gt;

&lt;h2&gt;
  
  
  RBAC: four roles + project membership
&lt;/h2&gt;

&lt;p&gt;Two layers of permission:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User role&lt;/strong&gt; (system-wide): Admin, Manager, Developer, Viewer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project membership&lt;/strong&gt; (per project): Owner, Member&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A Developer can only see and deploy projects they're a member of. A Viewer can read logs but can't trigger anything. The permission matrix is in the README if you want the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notifications: Provider / Channel / Subscription
&lt;/h2&gt;

&lt;p&gt;This is the part I'm proudest of architecturally.&lt;/p&gt;

&lt;p&gt;Most notification systems hard-code "this event goes to this webhook." Deploy Center splits it into three tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NotificationProvider&lt;/strong&gt; — the credentials (one Discord workspace, one SMTP server)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NotificationChannel&lt;/strong&gt; — a specific delivery target under a provider (channel ID, recipient list)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ProjectNotificationSubscription&lt;/strong&gt; — M:N: which projects fire which events to which channels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you can have one Discord provider with five channels, and each project subscribes to whichever channels make sense for it. Adding Slack support was just adding a new provider type — no changes to the project model.&lt;/p&gt;

&lt;p&gt;Fan-out uses &lt;code&gt;Promise.allSettled&lt;/code&gt; so one failing channel doesn't block the others. Each failure is logged with channel + provider context.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Should have started with BullMQ.&lt;/strong&gt; The in-memory queue was technical debt from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequelize migrations are painful.&lt;/strong&gt; I'd consider Drizzle or Kysely on a future project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time via Socket.IO is fine for logs&lt;/strong&gt;, but I'd evaluate Server-Sent Events first — they're simpler for one-way streams.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next (v3.1)
&lt;/h2&gt;

&lt;p&gt;Remote deployment targets — right now Deploy Center deploys to the same machine it runs on. v3.1 will add SSH-based remote targets so you can run one Deploy Center instance and deploy to many servers.&lt;/p&gt;

&lt;p&gt;The full roadmap is in &lt;code&gt;docs/ROADMAP.md&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/FutureSolutionDev/Deploy-Center-Server" rel="noopener noreferrer"&gt;https://github.com/FutureSolutionDev/Deploy-Center-Server&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's MIT licensed. PRs welcome — &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; walks through the dev setup. The codebase is TypeScript strict mode, ESLint + Prettier configured, Jest on the server and Vitest on the client, GitHub Actions running typecheck + lint + tests on every PR.&lt;/p&gt;

&lt;p&gt;If you try it and it breaks, please open an issue. If you try it and it works, a star would mean a lot.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>devops</category>
      <category>typescript</category>
      <category>react</category>
    </item>
  </channel>
</rss>
