<?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: Kjudeh</title>
    <description>The latest articles on DEV Community by Kjudeh (@kjudeh).</description>
    <link>https://dev.to/kjudeh</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%2F3780467%2F32d4a5e0-6849-456a-8707-730673f072ec.png</url>
      <title>DEV Community: Kjudeh</title>
      <link>https://dev.to/kjudeh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kjudeh"/>
    <language>en</language>
    <item>
      <title>An AI receptionist on a real WhatsApp number: architecture, routing logic, and failure modes</title>
      <dc:creator>Kjudeh</dc:creator>
      <pubDate>Tue, 25 Aug 2026 21:13:39 +0000</pubDate>
      <link>https://dev.to/bubblesstudio/an-ai-receptionist-on-a-real-whatsapp-number-architecture-routing-logic-and-failure-modes-3ila</link>
      <guid>https://dev.to/bubblesstudio/an-ai-receptionist-on-a-real-whatsapp-number-architecture-routing-logic-and-failure-modes-3ila</guid>
      <description>&lt;p&gt;In most of the Gulf and Levant, WhatsApp isn't &lt;em&gt;a&lt;/em&gt; support channel — it's the front desk. Customers ask prices, book appointments, and choose between competitors based on who answers first. Which makes the default setup remarkable: a human checking a phone between tasks, going silent at 8pm.&lt;/p&gt;

&lt;p&gt;This post walks through the architecture of a self-hosted AI receptionist on a normal WhatsApp number — what the pieces are, why each exists, and the failure modes to design around. At the end there's a one-click version if you'd rather skip the assembly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Four services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WhatsApp ⇄ Evolution API → webhook → n8n workflow → LLM (Claude/GPT)
                ↑                        │
                └── send reply ──────────┘
        Postgres (sessions, messages) · Redis (instance cache)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/evolution-foundation/evolution-api" rel="noopener noreferrer"&gt;Evolution API&lt;/a&gt;&lt;/strong&gt; (9k★) speaks the WhatsApp Web protocol — you pair it with a QR scan like WhatsApp Web, and it exposes a REST API + webhooks for messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;n8n&lt;/strong&gt; hosts the receptionist logic as a visual workflow: filter → route → LLM call → reply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres + a small volume&lt;/strong&gt; persist the session, so redeploys don't ask you to re-scan the QR code.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;LLM is yours&lt;/strong&gt; — an Anthropic or OpenAI key, with the business context in the system prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The routing logic that makes it a receptionist (not a toy)
&lt;/h2&gt;

&lt;p&gt;The naive version — "forward every message to an LLM" — fails in production within a day. The workflow needs four branches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Ignore what should be ignored.&lt;/strong&gt; Group messages, your own outgoing messages, non-text payloads. Skipping this floods your LLM bill and spams groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Human handoff, on a keyword.&lt;/strong&gt; When a customer types "human" (or your word of choice), the bot acknowledges, stops replying in that thread, and notifies your team. An AI that traps customers in bot-loop hell costs more goodwill than it saves labor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Office-hours awareness.&lt;/strong&gt; Out of hours, an instant "we're closed, here's when we open, leave your message" beats an AI improvising commitments at 3am.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Only then, the LLM.&lt;/strong&gt; With a system prompt carrying the business's real facts (services, prices, policies, tone), a length cap, and instructions to offer the handoff word when unsure. Bilingual businesses: prompt it to answer in the customer's language — Arabic in, Arabic out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment failure modes to design around
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session loss.&lt;/strong&gt; If WhatsApp auth lives only in the container, every redeploy needs a QR re-scan. Persist sessions in Postgres &lt;em&gt;and&lt;/em&gt; a volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version drift.&lt;/strong&gt; Pin images by digest. (If you use Evolution API specifically: stay on the v2.3.x line — v2.4.0+ requires a license activation.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unofficial-client caveat, honestly:&lt;/strong&gt; this rides the WhatsApp Web protocol, not the official Business API. Meta can restrict accounts that spam. Use a dedicated number, answer inbound rather than broadcasting, and use the official API for regulated workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one-click version
&lt;/h2&gt;

&lt;p&gt;We packaged this exact architecture — Evolution API (pinned v2.3.7), n8n with the four-branch receptionist workflow pre-loaded and auto-activated, Postgres, Redis, session persistence — as an open-source (MIT) Railway template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploy:&lt;/strong&gt; &lt;a href="https://railway.com/deploy/whatsapp-ai-receptionist?referralCode=qxxcuG" rel="noopener noreferrer"&gt;https://railway.com/deploy/whatsapp-ai-receptionist?referralCode=qxxcuG&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source + workflow JSON:&lt;/strong&gt; &lt;a href="https://github.com/Kjudeh/whatsapp-ai-receptionist" rel="noopener noreferrer"&gt;https://github.com/Kjudeh/whatsapp-ai-receptionist&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deploy, paste an LLM key, scan one QR code, and message your own number.&lt;/p&gt;

&lt;p&gt;And if you'd rather have it &lt;strong&gt;done for you&lt;/strong&gt; — installed on your number, persona tuned to your business in Arabic and English, live in 48 hours, optionally managed — that's what we do at &lt;a href="https://kjudeh.github.io/bubbles-services/" rel="noopener noreferrer"&gt;Bubbles Studio&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions welcome in the comments — and the repo takes issues and PRs.&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>ai</category>
      <category>automation</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Running n8n in production: queue mode, workers, and the settings that keep it alive</title>
      <dc:creator>Kjudeh</dc:creator>
      <pubDate>Tue, 25 Aug 2026 19:21:55 +0000</pubDate>
      <link>https://dev.to/bubblesstudio/running-n8n-in-production-queue-mode-workers-and-the-settings-that-keep-it-alive-3o9</link>
      <guid>https://dev.to/bubblesstudio/running-n8n-in-production-queue-mode-workers-and-the-settings-that-keep-it-alive-3o9</guid>
      <description>&lt;p&gt;Self-hosted n8n has a failure mode almost everyone hits eventually: everything runs fine for months, then one day the editor won't load, webhooks time out, and workflows silently stop. It's rarely a mystery when you dig in — it's almost always one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single-instance execution.&lt;/strong&gt; By default, n8n executes workflows in the same process that serves the UI and receives webhooks. One heavy execution (a big loop, an AI agent chain, a large file) and everything else starves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unbounded executions table.&lt;/strong&gt; Every run writes execution data to your database. With no pruning, the table grows until Postgres becomes the bottleneck — or the disk fills.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both have first-class fixes in n8n itself. They're just not the defaults. This post walks through the production setup n8n's own docs recommend — &lt;strong&gt;queue mode&lt;/strong&gt; — plus the hardening settings that prevent the slow-motion failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  How queue mode works
&lt;/h2&gt;

&lt;p&gt;In queue mode, n8n splits into two roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;main instance&lt;/strong&gt; serves the editor UI, receives webhooks, and manages schedules — but executes nothing. When a workflow should run, it pushes a job onto a Redis queue (Bull).&lt;/li&gt;
&lt;li&gt;One or more &lt;strong&gt;workers&lt;/strong&gt; pull jobs from the queue and execute them.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Webhook/schedule → main instance → Redis queue → worker(s) → Postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A heavy execution can't freeze your editor or drop incoming webhooks — intake and execution are different processes.&lt;/li&gt;
&lt;li&gt;Scaling is horizontal: more load → more worker replicas. Workers are stateless (all state lives in Postgres), so replicas "just work".&lt;/li&gt;
&lt;li&gt;A worker crash loses nothing: queued jobs are picked up when a worker returns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The core configuration
&lt;/h2&gt;

&lt;p&gt;Both main and workers share most configuration. The critical rule: &lt;strong&gt;main and workers must run the same n8n version and the same encryption key&lt;/strong&gt;, or credentials break in confusing ways.&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;# Shared by main + workers&lt;/span&gt;
&lt;span class="nv"&gt;DB_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresdb
&lt;span class="nv"&gt;DB_POSTGRESDB_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;postgres-host&amp;gt;
&lt;span class="nv"&gt;DB_POSTGRESDB_DATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;db&amp;gt;
&lt;span class="nv"&gt;DB_POSTGRESDB_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;user&amp;gt;
&lt;span class="nv"&gt;DB_POSTGRESDB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;password&amp;gt;
&lt;span class="nv"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;same value everywhere!&amp;gt;

&lt;span class="nv"&gt;EXECUTIONS_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;queue
&lt;span class="nv"&gt;QUEUE_BULL_REDIS_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;redis-host&amp;gt;
&lt;span class="nv"&gt;QUEUE_BULL_REDIS_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6379
&lt;span class="nv"&gt;QUEUE_BULL_REDIS_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;redis-password&amp;gt;

&lt;span class="c"&gt;# Main instance only&lt;/span&gt;
&lt;span class="nv"&gt;N8N_WEBHOOK_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://your-domain/
&lt;span class="nv"&gt;OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# Workers: start with `n8n worker`; enable its health endpoint&lt;/span&gt;
&lt;span class="nv"&gt;QUEUE_HEALTH_CHECK_ACTIVE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker is the same image/binary started with &lt;code&gt;n8n worker&lt;/code&gt; — it connects to the same Postgres and Redis and begins consuming jobs (default concurrency: 10 parallel executions per worker).&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardening most setups skip
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Execution pruning.&lt;/strong&gt; This is the one that kills instances. Cap the history:&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;EXECUTIONS_DATA_PRUNE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;EXECUTIONS_DATA_MAX_AGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;336        &lt;span class="c"&gt;# keep 14 days&lt;/span&gt;
&lt;span class="nv"&gt;EXECUTIONS_DATA_PRUNE_MAX_COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Database backups you can actually restore.&lt;/strong&gt; Workflows, credentials, and history all live in Postgres — it's the single point of loss. A compressed nightly &lt;code&gt;pg_dump&lt;/code&gt; to any S3-compatible bucket costs cents; the part people skip is &lt;em&gt;verifying restores&lt;/em&gt;. A backup you've never restored is a hope, not a backup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boring but worth setting:&lt;/strong&gt; &lt;code&gt;N8N_DIAGNOSTICS_ENABLED=false&lt;/code&gt; (telemetry off), pin your n8n image by version &lt;em&gt;and&lt;/em&gt; digest so main and workers can never drift apart, and keep Postgres/Redis off the public internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling checklist
&lt;/h2&gt;

&lt;p&gt;When executions start queuing up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add worker replicas (they're stateless — this is safe).&lt;/li&gt;
&lt;li&gt;Watch Postgres — execution writes are the next bottleneck; pruning keeps it manageable.&lt;/li&gt;
&lt;li&gt;Only then think about the main instance — it rarely needs scaling, since it executes nothing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The one-click version
&lt;/h2&gt;

&lt;p&gt;If you'd rather not assemble this by hand: we packaged this exact architecture — main + worker on a single digest-pinned image, Redis, Postgres, pruning defaults, and S3 database backups wired in — as an open-source (MIT) Railway template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploy:&lt;/strong&gt; &lt;a href="https://railway.com/deploy/n8n-production-stack-queue-mode" rel="noopener noreferrer"&gt;https://railway.com/deploy/n8n-production-stack-queue-mode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/Kjudeh/n8n-production-stack" rel="noopener noreferrer"&gt;https://github.com/Kjudeh/n8n-production-stack&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full disclosure: we (Bubbles Studio) built and maintain it — versions are bumped through weekly reviewed PRs and the stack is test-deployed from scratch before every update. Everything in this post applies to it 1:1, because it &lt;em&gt;is&lt;/em&gt; this post's architecture.&lt;/p&gt;

&lt;p&gt;Questions about queue mode or the template — comments are open, and so are issues on the repo.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>selfhosted</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your PostgreSQL Backups Are Probably Broken (Here's How to Know for Sure)</title>
      <dc:creator>Kjudeh</dc:creator>
      <pubDate>Thu, 19 Feb 2026 05:17:25 +0000</pubDate>
      <link>https://dev.to/kjudeh/your-postgresql-backups-are-probably-broken-heres-how-to-know-for-sure-2ihb</link>
      <guid>https://dev.to/kjudeh/your-postgresql-backups-are-probably-broken-heres-how-to-know-for-sure-2ihb</guid>
      <description>&lt;p&gt;73% of backup restores fail in production.&lt;/p&gt;

&lt;p&gt;Not because the backup software broke. Because &lt;strong&gt;no one tested the restore&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I've been running PostgreSQL in production for years. I've seen teams religiously back up their databases every hour, upload to S3, check the green checkmark, and sleep soundly. Then disaster hits, they try to restore, and... nothing works. Corrupted dumps. Wrong permissions. Missing extensions. Schema drift.&lt;/p&gt;

&lt;p&gt;The backup worked. The restore didn't. And they only found out when it mattered most.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With "Just Back It Up"
&lt;/h2&gt;

&lt;p&gt;Most backup setups look like this:&lt;/p&gt;

&lt;p&gt;pg_dump → gzip → S3 →✓ done&lt;/p&gt;

&lt;p&gt;That checkmark is a lie. It tells you the file uploaded. It doesn't tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can this dump actually be restored?&lt;/li&gt;
&lt;li&gt;Will the data be intact?&lt;/li&gt;
&lt;li&gt;Do the row counts match?&lt;/li&gt;
&lt;li&gt;Are the indexes valid?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't know until you try. And most teams never try until it's too late.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Automated Restore Verification
&lt;/h2&gt;

&lt;p&gt;I built a backup system that doesn't just dump and pray. Every day, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Downloads the latest backup from S3&lt;/li&gt;
&lt;li&gt;Restores it to an &lt;strong&gt;isolated verification database&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Runs integrity checks (table counts, row validation, custom queries)&lt;/li&gt;
&lt;li&gt;Reports success or failure&lt;/li&gt;
&lt;li&gt;Cleans up automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the restore fails, I know immediately — not when production is on fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The system has two services:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backup Service&lt;/strong&gt; — Runs &lt;code&gt;pg_dump&lt;/code&gt; on a schedule (default: hourly), compresses with gzip, uploads to any S3-compatible storage. Handles retention automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify Service&lt;/strong&gt; — Downloads backups daily and restores them to a separate PostgreSQL instance. Runs sanity checks. Proves the backup works.&lt;/p&gt;

&lt;p&gt;Both run as lightweight containers. Works with AWS S3, Backblaze B2, Cloudflare R2, DigitalOcean Spaces, Wasabi, or MinIO.&lt;/p&gt;

&lt;h2&gt;
  
  
  One-Click Deploy on Railway
&lt;/h2&gt;

&lt;p&gt;If you're running PostgreSQL on Railway, you can deploy this in minutes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://railway.com/template/sparkling-creation" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Frailway.com%2Fbutton.svg" alt="Deploy on Railway" width="183" height="40"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just add your database URL and S3 credentials. The services start working immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hourly backups to S3 (configurable)&lt;/li&gt;
&lt;li&gt;Daily restore verification&lt;/li&gt;
&lt;li&gt;7-day retention (configurable)&lt;/li&gt;
&lt;li&gt;AES-256 encryption (optional)&lt;/li&gt;
&lt;li&gt;Slack/Discord alerts on failure&lt;/li&gt;
&lt;li&gt;Zero maintenance after setup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;Fully open source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Kjudeh/railway-postgres-backups" rel="noopener noreferrer"&gt;github.com/Kjudeh/railway-postgres-backups&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Works with Docker, docker-compose, or any container platform — not just Railway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop Hoping. Start Verifying.
&lt;/h2&gt;

&lt;p&gt;Every backup you haven't restored is a backup you can't trust.&lt;/p&gt;

&lt;p&gt;Set up automated verification once. Sleep better forever.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions? Drop a comment or open an issue on GitHub.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
