<?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: Zil Norvilis</title>
    <description>The latest articles on DEV Community by Zil Norvilis (@zilton7).</description>
    <link>https://dev.to/zilton7</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%2F352433%2F2aff65b6-dba1-4f8c-8aaf-0bc84763ae23.jpg</url>
      <title>DEV Community: Zil Norvilis</title>
      <link>https://dev.to/zilton7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zilton7"/>
    <language>en</language>
    <item>
      <title>Real-Time Engagement: Web Push Notifications in Rails 8</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Tue, 23 Jun 2026 18:27:06 +0000</pubDate>
      <link>https://dev.to/zilton7/real-time-engagement-web-push-notifications-in-rails-8-5gdp</link>
      <guid>https://dev.to/zilton7/real-time-engagement-web-push-notifications-in-rails-8-5gdp</guid>
      <description>&lt;p&gt;For a long time, if you wanted to send a "Push Notification" to a user’s phone or laptop, you needed a native mobile app. For solo developers, this was a huge barrier. We had to rely on emails that nobody reads or expensive SMS services.&lt;/p&gt;

&lt;p&gt;In 2026, the &lt;strong&gt;Web Push API&lt;/strong&gt; is supported by almost every browser, including Safari on iOS. This means you can send native-style alerts directly from your Rails app to your user's device without them ever downloading anything from an App Store.&lt;/p&gt;

&lt;p&gt;It sounds complex because it involves Service Workers and Cryptography, but with the right gems, it is actually very manageable. Here is how to set up Web Push in Rails 8.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mental Model: How Web Push Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Subscription:&lt;/strong&gt; The user clicks "Allow" in their browser. The browser gives us a "Subscription" object (a URL and some secret keys).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Storage:&lt;/strong&gt; We save that object in our database, linked to the &lt;code&gt;User&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Push:&lt;/strong&gt; When an event happens, our Rails server signs a message and sends it to the browser's "Push Service" (Google, Apple, or Mozilla).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Delivery:&lt;/strong&gt; The Service Worker on the user's device wakes up and shows the notification.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  STEP 1: The VAPID Keys
&lt;/h2&gt;

&lt;p&gt;To send secure notifications, you need a pair of VAPID keys (Public and Private). These identify your server so that the browser knows the notification isn't from a hacker.&lt;/p&gt;

&lt;p&gt;Add the gem to your &lt;code&gt;Gemfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"web-push"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in your terminal to generate your keys:&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;# Generate keys&lt;/span&gt;
web-push generate-vapid-keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy these keys into your &lt;code&gt;.kamal/secrets&lt;/code&gt; or your credentials file.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2: The Database Setup
&lt;/h2&gt;

&lt;p&gt;We need to store the subscription for each user. A user might have multiple subscriptions (one for their phone, one for their laptop).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g model WebPushSubscription user:references endpoint:string p256dh_key:string auth_key:string
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 3: The Service Worker (Javascript)
&lt;/h2&gt;

&lt;p&gt;Service Workers must live in the &lt;code&gt;public/&lt;/code&gt; folder to have access to the whole site. Create a file at &lt;code&gt;public/service-worker.js&lt;/code&gt;. This file listens for the "push" event even when your website is closed.&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="c1"&gt;// public/service-worker.js&lt;/span&gt;
&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;push&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;event&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showNotification&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;title&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;data&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="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/logo.png&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, register this worker in your &lt;code&gt;app/javascript/application.js&lt;/code&gt;:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/service-worker.js&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 4: Creating the Subscription
&lt;/h2&gt;

&lt;p&gt;We need a Stimulus controller to ask the user for permission and send the subscription to Rails.&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="c1"&gt;// app/javascript/controllers/push_subscription_controller.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;subscribe&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;registration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&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;subscription&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;registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pushManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;userVisibleOnly&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;applicationServerKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_PUBLIC_VAPID_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Send this JSON to your Rails controller&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/web_push_subscriptions&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="s2"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-CSRF-Token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[name='csrf-token']&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;content&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;subscription&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Subscribed!&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 5: Sending the Notification (The Ruby Part)
&lt;/h2&gt;

&lt;p&gt;Finally, we send the notification from a background job so we don't slow down the app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/jobs/send_web_push_job.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendWebPushJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&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="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;web_push_subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;sub&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;Webpush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;body: &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;endpoint: &lt;/span&gt;&lt;span class="nb"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;p256dh: &lt;/span&gt;&lt;span class="nb"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;p256dh_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;auth: &lt;/span&gt;&lt;span class="nb"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;auth_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;vapid: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;subject: &lt;/span&gt;&lt;span class="s2"&gt;"mailto:admin@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;public_key: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"VAPID_PUBLIC_KEY"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="ss"&gt;private_key: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"VAPID_PRIVATE_KEY"&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;rescue&lt;/span&gt; &lt;span class="no"&gt;Webpush&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;InvalidSubscription&lt;/span&gt; &lt;span class="c1"&gt;# If user revoked permission&lt;/span&gt;
      &lt;span class="nb"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Web Push is the "Pro" way to bring users back to your app. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;VAPID keys&lt;/strong&gt; handle the security.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Service Workers&lt;/strong&gt; handle the delivery while the user is away.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Solid Queue&lt;/strong&gt; handles the background sending.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a solo developer, you can now build an app that feels like a native mobile experience using nothing but Rails and a tiny bit of Javascript. &lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>webdev</category>
      <category>notifications</category>
    </item>
    <item>
      <title>The Budget-Friendly Monolith: Rails File Uploads with Cloudflare R2</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sun, 21 Jun 2026 18:13:36 +0000</pubDate>
      <link>https://dev.to/zilton7/the-budget-friendly-monolith-rails-file-uploads-with-cloudflare-r2-29ml</link>
      <guid>https://dev.to/zilton7/the-budget-friendly-monolith-rails-file-uploads-with-cloudflare-r2-29ml</guid>
      <description>&lt;p&gt;If you have been using AWS S3 for your Rails file uploads, you know the pain of the monthly bill. It’s not just the storage cost; it’s the &lt;strong&gt;Egress Fees&lt;/strong&gt;. Every time a user downloads a file or views an image, AWS charges you for the data leaving their servers.&lt;/p&gt;

&lt;p&gt;For a solo developer or a small startup, these "hidden" fees can grow very fast. &lt;/p&gt;

&lt;p&gt;In 2026, the best alternative is &lt;strong&gt;Cloudflare R2&lt;/strong&gt;. It is an object storage service that is 100% compatible with the S3 API, but with one massive advantage: &lt;strong&gt;Zero Egress Fees.&lt;/strong&gt; You only pay for the space you use. &lt;/p&gt;

&lt;p&gt;Because it uses the S3 API, setting it up with Rails ActiveStorage is incredibly easy. Here is how to do it in 4 steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: The Credentials
&lt;/h2&gt;

&lt;p&gt;First, log into your Cloudflare dashboard and create a new &lt;strong&gt;R2 Bucket&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Once the bucket is created, you need to generate "API Tokens." &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;R2&lt;/strong&gt; &amp;gt; &lt;strong&gt;Manage R2 API Tokens&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create a new token with &lt;strong&gt;Edit&lt;/strong&gt; permissions.&lt;/li&gt;
&lt;li&gt;Save your &lt;strong&gt;Access Key ID&lt;/strong&gt;, &lt;strong&gt;Secret Access Key&lt;/strong&gt;, and the &lt;strong&gt;Jurisdiction-specific endpoint&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The endpoint will look something like this:&lt;br&gt;
&lt;code&gt;https://&amp;lt;account_id&amp;gt;.r2.cloudflorage.com&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  STEP 2: The Gems
&lt;/h2&gt;

&lt;p&gt;Even though we are using Cloudflare, we still use the official AWS S3 gem because Cloudflare built R2 to be a "drop-in" replacement.&lt;/p&gt;

&lt;p&gt;Add this to your &lt;code&gt;Gemfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"aws-sdk-s3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;require: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;bundle install&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: Configuration (&lt;code&gt;storage.yml&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Now we need to tell Rails how to talk to the R2 bucket. Open your &lt;code&gt;config/storage.yml&lt;/code&gt; and add a new section for Cloudflare.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/storage.yml&lt;/span&gt;
&lt;span class="na"&gt;cloudflare&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;S3&lt;/span&gt;
  &lt;span class="na"&gt;access_key_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= ENV['CLOUDFLARE_R2_ACCESS_KEY_ID'] %&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;secret_access_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= ENV['CLOUDFLARE_R2_SECRET_ACCESS_KEY'] %&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto&lt;/span&gt; &lt;span class="c1"&gt;# R2 handles regions automatically&lt;/span&gt;
  &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app-uploads&lt;/span&gt;
  &lt;span class="c1"&gt;# This is the endpoint you got from Step 1&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://&amp;lt;account_id&amp;gt;.r2.cloudflarestorage.com&lt;/span&gt;
  &lt;span class="na"&gt;force_path_style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Make sure to add those ENV variables to your &lt;code&gt;.kamal/secrets&lt;/code&gt; or your production server!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 4: Tell Rails to use R2
&lt;/h2&gt;

&lt;p&gt;Finally, update your production environment to use the new service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/environments/production.rb&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:cloudflare&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus: Enabling Direct Uploads (CORS)
&lt;/h2&gt;

&lt;p&gt;In a previous article, I talked about &lt;strong&gt;Direct Uploads&lt;/strong&gt; to save your server from freezing. If you want to use Direct Uploads with R2, you &lt;strong&gt;must&lt;/strong&gt; configure CORS (Cross-Origin Resource Sharing). &lt;/p&gt;

&lt;p&gt;Cloudflare makes this easy. Go to your Bucket settings in the dashboard and paste this JSON into the CORS section:&lt;br&gt;
&lt;/p&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AllowedOrigins"&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="s2"&gt;"https://yourdomain.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AllowedMethods"&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="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PUT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AllowedHeaders"&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="s2"&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;"ExposeHeaders"&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="s2"&gt;"Content-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;"ETag"&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="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Cloudflare: &lt;em&gt;"It is safe to let users from my website upload files directly to this bucket."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Switching from AWS S3 to Cloudflare R2 is one of the smartest "business" moves a solo developer can make. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compatibility:&lt;/strong&gt; You don't have to change your Ruby code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictability:&lt;/strong&gt; No more surprise bills because an image went viral.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity:&lt;/strong&gt; One less AWS console to navigate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By combining &lt;strong&gt;Rails 8&lt;/strong&gt;, &lt;strong&gt;Kamal 2&lt;/strong&gt;, and &lt;strong&gt;Cloudflare R2&lt;/strong&gt;, you are building a production stack that is fast, secure, and incredibly cheap to run.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>storage</category>
      <category>cloudflare</category>
      <category>devops</category>
    </item>
    <item>
      <title>Pro File Uploads in Rails 8: Speed and Scalability with Direct Uploads</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Fri, 19 Jun 2026 18:28:00 +0000</pubDate>
      <link>https://dev.to/zilton7/pro-file-uploads-in-rails-8-speed-and-scalability-with-direct-uploads-116c</link>
      <guid>https://dev.to/zilton7/pro-file-uploads-in-rails-8-speed-and-scalability-with-direct-uploads-116c</guid>
      <description>&lt;p&gt;Imagine a user trying to upload a 100MB video or a high-resolution photo to your app. If you use the standard Rails file upload, that file travels from the user's browser to your Rails server, and &lt;em&gt;then&lt;/em&gt; your server sends it to S3 or Google Cloud.&lt;/p&gt;

&lt;p&gt;This is a terrible way to do it. While that 100MB file is transferring, your Rails worker (Puma) is frozen. It can't handle other users. If three people upload large files at once, your whole app will stop responding.&lt;/p&gt;

&lt;p&gt;In 2026, the professional way to handle this is &lt;strong&gt;Direct Uploads&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;With Direct Uploads, the file goes directly from the user's browser to your cloud storage (S3, R2, etc.). Your Rails server only handles a tiny bit of metadata. It is faster for the user and much safer for your server. Here is how to set it up in Rails 8.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: Configure Your Storage
&lt;/h2&gt;

&lt;p&gt;First, make sure you aren't using the local disk for production. You need a cloud provider like AWS S3 or Cloudflare R2.&lt;/p&gt;

&lt;p&gt;In your &lt;code&gt;config/storage.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;amazon&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;S3&lt;/span&gt;
  &lt;span class="na"&gt;access_key_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= ENV['AWS_ACCESS_KEY_ID'] %&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;secret_access_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= ENV['AWS_SECRET_ACCESS_KEY'] %&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
  &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app-uploads&lt;/span&gt;
  &lt;span class="c1"&gt;# Crucial for Direct Uploads!&lt;/span&gt;
  &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You must configure &lt;strong&gt;CORS&lt;/strong&gt; in your S3/R2 dashboard to allow requests from your domain. If you don't do this, the browser will block the upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2: The Rails Form
&lt;/h2&gt;

&lt;p&gt;Rails makes the backend part incredibly easy. You just add one attribute to your file field: &lt;code&gt;direct_upload: true&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/users/_form.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:avatar&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file_field&lt;/span&gt; &lt;span class="ss"&gt;:avatar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;direct_upload: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="s2"&gt;"Save Profile"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you add &lt;code&gt;direct_upload: true&lt;/code&gt;, Rails automatically includes a JavaScript library that handles the "handshake" with S3.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: Adding a Progress Bar (The UX Win)
&lt;/h2&gt;

&lt;p&gt;Direct uploads can take a few seconds. If nothing happens on the screen, the user will think your app is broken. We can use the built-in ActiveStorage events to show a beautiful progress bar.&lt;/p&gt;

&lt;p&gt;First, add a small piece of HTML to your form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"upload-progress hidden"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"progress-bar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-blue-600 h-2 transition-all"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"progress-fill"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 0%"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we create a tiny &lt;strong&gt;Stimulus&lt;/strong&gt; controller to watch the upload progress.&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="c1"&gt;// app/javascript/controllers/upload_controller.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;direct-upload:progress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bar&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;progress-fill&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;container&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;progress-bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&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;progress&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;direct-upload:error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upload failed! Please check your connection.&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="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;Attach this to your form: &lt;code&gt;&amp;lt;%= form_with(model: user, data: { controller: "upload" }) do |f| %&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 4: Why this is the "One-Person" Superpower
&lt;/h2&gt;

&lt;p&gt;As a solo developer, you want to avoid "Scaling Issues" as long as possible. &lt;/p&gt;

&lt;p&gt;The traditional upload method requires you to have a large server with lots of RAM to handle big file streams. If your app goes viral, you’ll have to pay for a massive server just to move files around.&lt;/p&gt;

&lt;p&gt;By using &lt;strong&gt;Direct Uploads&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; You can stay on a tiny $5 VPS because S3 does 99% of the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed:&lt;/strong&gt; Users see a progress bar immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability:&lt;/strong&gt; If your server restarts in the middle of an upload, the upload doesn't necessarily fail because it’s not talking to your server!&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Don't let file uploads slow down your monolith. It takes 5 minutes to switch to Direct Uploads, but it makes your app feel like an enterprise product.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;strong&gt;Cloud Storage&lt;/strong&gt; (S3/R2).&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;CORS&lt;/strong&gt; on your bucket.&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;&lt;code&gt;direct_upload: true&lt;/code&gt;&lt;/strong&gt; to your form.&lt;/li&gt;
&lt;li&gt;Add a &lt;strong&gt;Stimulus&lt;/strong&gt; progress bar for that "premium" feel.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your Puma threads will thank you, and your users will love the snappy experience.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>storage</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Zero-JS Search: Building a Live-Filter with Turbo 8 Page Morphing</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Thu, 18 Jun 2026 18:15:43 +0000</pubDate>
      <link>https://dev.to/zilton7/zero-js-search-building-a-live-filter-with-turbo-8-page-morphing-2ihm</link>
      <guid>https://dev.to/zilton7/zero-js-search-building-a-live-filter-with-turbo-8-page-morphing-2ihm</guid>
      <description>&lt;p&gt;I’ve lost count of the number of hours I’ve spent in the past building "Live Search" features. &lt;/p&gt;

&lt;p&gt;In the old days, you had to write custom AJAX listeners, handle the "white flash" of reloads, and manually manage the browser's history. Later, with Turbo 7, we used &lt;code&gt;Turbo Frames&lt;/code&gt;, but that meant wrapping everything in specific tags and losing the "state" of the rest of the page.&lt;/p&gt;

&lt;p&gt;In 2026, the game has changed. With &lt;strong&gt;Turbo 8 Page Morphing&lt;/strong&gt;, we can build a live-filtering search bar that feels like a heavy React app, but uses 100% standard Rails controllers and almost zero custom JavaScript.&lt;/p&gt;

&lt;p&gt;The secret is that Turbo 8 can "morph" the page - it refreshes the HTML but keeps your scroll position and the focus inside the search input perfectly intact. Here is how to build it in 3 steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: Enable Morphing
&lt;/h2&gt;

&lt;p&gt;First, we need to tell our application that we want to use the new Morphing behavior instead of the old "Replace" behavior. &lt;/p&gt;

&lt;p&gt;Open your main application layout and add these two tags to the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/layouts/application.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_refreshes_with&lt;/span&gt; &lt;span class="ss"&gt;method: :morph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scroll: :preserve&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="ss"&gt;:head&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 2: The Search Form and Controller
&lt;/h2&gt;

&lt;p&gt;We are going to use a standard HTML form. No &lt;code&gt;remote: true&lt;/code&gt;, no complex setups. Just a simple &lt;code&gt;GET&lt;/code&gt; request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/products_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
      &lt;span class="vi"&gt;@products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"name ILIKE ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&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;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your view, create the search bar. The "trick" here is that we want the form to submit as the user types. Since HTML doesn't do this natively, we use a tiny 2-line &lt;strong&gt;Stimulus&lt;/strong&gt; controller that I keep in every project as a "utility" tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/products/index.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"p-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"autosave"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;products_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                  &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;autosave_target: &lt;/span&gt;&lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;turbo_frame: &lt;/span&gt;&lt;span class="s2"&gt;"_top"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"Search products..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"input-&amp;gt;autosave#save"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"border-2 border-black p-2 w-full"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"products_list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-8 grid grid-cols-3 gap-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@products&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 3: The Utility Controller (The only JS you need)
&lt;/h2&gt;

&lt;p&gt;This is the only JavaScript we need. It is a reusable "autosave" controller that simply submits the form whenever you type.&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="c1"&gt;// app/javascript/controllers/autosave_controller.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This triggers the standard Rails form submission&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestSubmit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it works (The Magic)
&lt;/h2&gt;

&lt;p&gt;In the old days, calling &lt;code&gt;requestSubmit()&lt;/code&gt; as you typed would be a disaster. The page would reload, you would lose focus on the text box, and the cursor would jump to the beginning of the line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With Turbo 8 Morphing:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You type the letter "A".&lt;/li&gt;
&lt;li&gt;Stimulus tells the form to submit.&lt;/li&gt;
&lt;li&gt;Rails returns the &lt;em&gt;entire&lt;/em&gt; index page with only the "A" products.&lt;/li&gt;
&lt;li&gt;Turbo 8 compares the new HTML to the current page.&lt;/li&gt;
&lt;li&gt;It sees that the text input is the same, so it &lt;strong&gt;leaves it alone&lt;/strong&gt; (preserving your cursor and focus).&lt;/li&gt;
&lt;li&gt;It sees the product list has changed, so it &lt;strong&gt;morphs&lt;/strong&gt; those elements seamlessly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The user perceives a lightning-fast, reactive live filter, but you are just writing standard, boring Rails code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;As a solo developer, your goal is to stay out of "JavaScript Land" as much as possible. By leveraging Turbo 8 Page Morphing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;No JSON APIs:&lt;/strong&gt; You don't need to build a search endpoint.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;No Manual DOM Updates:&lt;/strong&gt; You don't have to write code to append or remove items.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;No Focus Issues:&lt;/strong&gt; Browsers handle the state of the input automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the "One-Person Framework" at its best: high-end features with zero-maintenance code.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>hotwire</category>
      <category>search</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Datastar in Rails 8: Real-Time UI Without the WebSocket Headache</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Wed, 17 Jun 2026 18:28:00 +0000</pubDate>
      <link>https://dev.to/zilton7/datastar-in-rails-8-real-time-ui-without-the-websocket-headache-2m0g</link>
      <guid>https://dev.to/zilton7/datastar-in-rails-8-real-time-ui-without-the-websocket-headache-2m0g</guid>
      <description>&lt;p&gt;I am a huge fan of Hotwire. It’s what allowed me to stop writing massive React apps and go back to enjoying Ruby. But recently, a new player has entered the "Hypermedia" world that every Rails developer should keep an eye on: &lt;strong&gt;Datastar&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you think of &lt;strong&gt;HTMX&lt;/strong&gt; as the "venerable grandfather" and &lt;strong&gt;Hotwire&lt;/strong&gt; as the "official Omakase choice," then &lt;strong&gt;Datastar&lt;/strong&gt; is the "high-performance specialist." &lt;/p&gt;

&lt;p&gt;The biggest difference? While Hotwire uses WebSockets (ActionCable) for real-time updates, Datastar uses &lt;strong&gt;Server-Sent Events (SSE)&lt;/strong&gt; by default. It is incredibly lightweight (only 13kb), requires zero dependencies, and makes fine-grained reactivity feel like magic.&lt;/p&gt;

&lt;p&gt;Here is how to get Datastar running in your Rails 8 app in 4 easy steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Concept: What makes Datastar different?
&lt;/h2&gt;

&lt;p&gt;In Hotwire, when you want real-time updates, you have to set up a WebSocket connection. WebSockets are powerful, but they are "stateful" - they keep a heavy connection open. &lt;/p&gt;

&lt;p&gt;Datastar uses SSE. This is a standard HTTP connection that stays open just to "stream" data from the server to the browser. It’s much easier to scale, easier to debug in your Network tab, and it feels faster because it uses a technology called &lt;strong&gt;"Signals"&lt;/strong&gt; (like Solid.js or Preact) to update only the tiny parts of the page that changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: Installation (The No-Build Way)
&lt;/h2&gt;

&lt;p&gt;Since we are on Rails 8, we don't want &lt;code&gt;node_modules&lt;/code&gt;. We just pin the library using Importmaps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/importmap pin @starfederation/datastar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your &lt;code&gt;app/javascript/application.js&lt;/code&gt;, just import it to initialize the global listeners:&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="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@starfederation/datastar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 2: The Frontend "Signal"
&lt;/h2&gt;

&lt;p&gt;Datastar uses attributes that look a lot like Alpine.js or Vue. Let’s build a simple counter that updates on the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/counters/show.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-signals=&lt;/span&gt;&lt;span class="s"&gt;"{count: 0}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;data-text=&lt;/span&gt;&lt;span class="s"&gt;"$count"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-on-click=&lt;/span&gt;&lt;span class="s"&gt;"@post('/increment')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Increment on Server
  &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What is happening here?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;data-signals&lt;/code&gt;: This is a tiny piece of client-side memory.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;data-text&lt;/code&gt;: This binds the header text to that memory.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;data-on-click&lt;/code&gt;: This sends a POST request to our Rails controller.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  STEP 3: The Rails Controller (Streaming SSE)
&lt;/h2&gt;

&lt;p&gt;This is where the magic happens. We aren't returning a regular HTML page or a Turbo Stream. We are returning a &lt;strong&gt;Datastar SSE stream&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You’ll want to add a tiny helper or a gem like &lt;code&gt;datastar-rails&lt;/code&gt; to handle the formatting, but here is what the raw response looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/counters_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CountersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Live&lt;/span&gt; &lt;span class="c1"&gt;# Allows us to keep the connection open&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Update your logic&lt;/span&gt;
    &lt;span class="n"&gt;new_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:count&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Set the correct header for SSE&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'text/event-stream'&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Send the Datastar "Fragment"&lt;/span&gt;
    &lt;span class="c1"&gt;# This tells Datastar to update the 'count' signal on the client&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt; &lt;span class="s2"&gt;"event: datastar-merge-signals&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;data: signals {count: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;new_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;ensure&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 4: The Real-Time Win
&lt;/h2&gt;

&lt;p&gt;Because Datastar is built on SSE, you can keep the connection open and "push" updates as many times as you want. &lt;/p&gt;

&lt;p&gt;Imagine a background job is processing an image. You can keep the &lt;code&gt;increment&lt;/code&gt; method open and send:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;data: signals {status: 'Processing...'}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  Wait 2 seconds...&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;data: signals {status: 'Done!', progress: 100}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser will update the UI every time a new line is sent from Ruby, without the user ever clicking anything again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why try this over Hotwire?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Fine-grained control:&lt;/strong&gt; You can update individual variables (Signals) inside an HTML element without re-rendering the whole element.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;No WebSocket Overhead:&lt;/strong&gt; SSE is just plain HTTP. It works perfectly with standard load balancers and doesn't require "Sticky Sessions" or complex Redis setups (though Rails 8 &lt;code&gt;Solid Cable&lt;/code&gt; helps with that!).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Client-side logic:&lt;/strong&gt; You get a little bit of client-side state (&lt;code&gt;data-signals&lt;/code&gt;) for things like toggling menus or form validation without needing to write a full Stimulus controller.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Datastar is perfect for the &lt;strong&gt;"Pragmatic Solo Developer"&lt;/strong&gt; who wants real-time features but finds ActionCable or heavy JS frameworks a bit too much. &lt;/p&gt;

&lt;p&gt;It keeps the logic in Ruby, uses the browser's native streaming capabilities, and results in a UI that feels incredibly snappy. If you are starting a new project this weekend, give Datastar a look—it might just become your new favorite tool in the hypermedia stack.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>datastar</category>
      <category>hotwire</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Go for Rubyists: A Practical Guide to Learning Golang</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Tue, 16 Jun 2026 18:05:10 +0000</pubDate>
      <link>https://dev.to/zilton7/go-for-rubyists-a-practical-guide-to-learning-golang-4gfo</link>
      <guid>https://dev.to/zilton7/go-for-rubyists-a-practical-guide-to-learning-golang-4gfo</guid>
      <description>&lt;p&gt;I love Ruby. It is the language that made me happy to write code. But as a Rails developer, I eventually hit a wall. Maybe I needed to process a 2GB CSV file, or build a real-time notification service that handles 10,000 connections. &lt;/p&gt;

&lt;p&gt;In those moments, Ruby can feel a bit slow or memory-heavy. This is why many Rails developers (including the team at Basecamp) have started using &lt;strong&gt;Go&lt;/strong&gt; (Golang) for high-performance side-services.&lt;/p&gt;

&lt;p&gt;If you are a Ruby developer, Go will feel very different. It has no "magic," it is strictly typed, and it is incredibly fast. Here is my guide on how to learn Go without losing your mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Big Shift: No More Magic
&lt;/h2&gt;

&lt;p&gt;In Rails, we are used to "magic." You type &lt;code&gt;User.find(1)&lt;/code&gt; and it just works. You don't see the SQL, you don't see the imports, and you don't worry about types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go is the opposite.&lt;/strong&gt; There is zero magic. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  If you use a variable, you must define its type.&lt;/li&gt;
&lt;li&gt;  If you import a library and don't use it, the code won't even compile.&lt;/li&gt;
&lt;li&gt;  Everything is explicit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first, this is very annoying. You will feel like you are typing too much. But after a week, you realize that because there is no magic, you can read any Go file and know exactly what it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Syntax Translation
&lt;/h2&gt;

&lt;p&gt;Let's look at a simple function comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ruby:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="s2"&gt;"Hello, &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Zil"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Go:&lt;/strong&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="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"fmt"&lt;/span&gt;

&lt;span class="c"&gt;// We must say name is a string, and the function returns a string&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Hello, "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"!"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Zil"&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;Notice the &lt;code&gt;package main&lt;/code&gt; and &lt;code&gt;import&lt;/code&gt;. Every Go file needs these. Also, notice the curly braces &lt;code&gt;{}&lt;/code&gt;. As a Rubyist, you'll miss &lt;code&gt;do...end&lt;/code&gt; at first, but you'll get used to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Dealing with Errors (The "if err != nil" loop)
&lt;/h2&gt;

&lt;p&gt;In Ruby, we use &lt;code&gt;begin...rescue&lt;/code&gt; to handle errors. Or we just let the app crash and check the logs.&lt;/p&gt;

&lt;p&gt;In Go, errors are not "exceptions." They are just values that functions return. You will see this pattern in every single Go project:&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;findUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Something went wrong, handle it here&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// If no error, continue&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It feels repetitive to write this 50 times a day, but it means your app almost never crashes in production because you are forced to handle every single edge case while you type.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Killer Feature: Goroutines
&lt;/h2&gt;

&lt;p&gt;This is the main reason to learn Go. In Ruby, if you want to do 10 things at once, you need Sidekiq and Redis. &lt;/p&gt;

&lt;p&gt;In Go, you have &lt;strong&gt;Goroutines&lt;/strong&gt;. You just put the word &lt;code&gt;go&lt;/code&gt; in front of a function call, and it runs in the background on a different CPU thread instantly.&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// slow logic&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// This runs in the background. No Redis needed!&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test@example.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 

    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Moving on..."&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;It is incredibly lightweight. You can run 100,000 goroutines on a cheap $5 laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Deployment: The Single Binary
&lt;/h2&gt;

&lt;p&gt;As someone who loves &lt;strong&gt;Kamal&lt;/strong&gt; and Docker, Go is a dream for deployment. &lt;/p&gt;

&lt;p&gt;When you build a Go app, it compiles into &lt;strong&gt;one single file&lt;/strong&gt; (a binary). This file contains your code and all your libraries. You don't need to install Ruby, Bundler, or Node on your server. You just move that one file to the server and run it. It makes Docker images tiny and deployments incredibly fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: Should you switch?
&lt;/h2&gt;

&lt;p&gt;I don't recommend replacing Rails with Go for your main web app. Rails is still much faster for building UIs and CRUD logic.&lt;/p&gt;

&lt;p&gt;Instead, &lt;strong&gt;use Go as a tool in your belt.&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Use Rails for the main app.&lt;/li&gt;
&lt;li&gt;  Use Go for that one specific microservice that needs to be blazing fast or handle massive amounts of data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Learning Go will make you a more disciplined developer. It teaches you to think about memory, types, and how the computer actually works.&lt;/p&gt;

</description>
      <category>go</category>
      <category>ruby</category>
      <category>learning</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Is Coding Over? 5 Career Pivots for Rails Devs in the AI Era</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Thu, 11 Jun 2026 18:05:29 +0000</pubDate>
      <link>https://dev.to/zilton7/is-coding-over-5-career-pivots-for-rails-devs-in-the-ai-era-289b</link>
      <guid>https://dev.to/zilton7/is-coding-over-5-career-pivots-for-rails-devs-in-the-ai-era-289b</guid>
      <description>&lt;p&gt;If you spend any time on Tech Twitter lately, you might feel like the sky is falling. People are saying that "coding is dead" and that AI will replace every developer by 2027. &lt;/p&gt;

&lt;p&gt;I don't think coding is dead, but I do think the &lt;strong&gt;Job of the Coder&lt;/strong&gt; is changing. &lt;/p&gt;

&lt;p&gt;The era of being paid $100k a year just to know the syntax of &lt;code&gt;has_many :through&lt;/code&gt; is coming to an end. AI can write syntax better and faster than you. But Rails developers have a unique advantage: we are usually &lt;strong&gt;Product Engineers&lt;/strong&gt;. We know how to build entire systems, not just small functions. &lt;/p&gt;

&lt;p&gt;If you are worried about AI taking over and want to pivot your career into something more "future-proof," here are the 5 best paths for a Rails developer in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The AI Orchestrator (AI Engineer)
&lt;/h2&gt;

&lt;p&gt;This is the most natural pivot. You aren't leaving Rails; you are just changing what you do with it. Instead of writing CRUD logic, you spend your time building &lt;strong&gt;Agentic Workflows&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You become the person who connects the Rails monolith to OpenAI, Anthropic, and local LLMs. You design the RAG (Retrieval-Augmented Generation) pipelines and the "verification loops" that ensure the AI doesn't hallucinate. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it’s safe:&lt;/strong&gt; AI models are powerful, but they are "brains in a jar." They need an engineer to give them hands and feet (APIs, Databases, and Logic) to actually do useful work.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Solution Architect
&lt;/h2&gt;

&lt;p&gt;In a world where AI can generate 1,000 lines of code in a second, the world is about to be flooded with &lt;strong&gt;terrible, messy code.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Companies will be desperate for "Architects" - people who can look at the big picture and say: &lt;em&gt;"Wait, we shouldn't use a microservice here just because the AI suggested it. We need a Majestic Monolith to keep our data consistent."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it’s safe:&lt;/strong&gt; AI is a great bricklayer, but it’s a terrible city planner. It doesn't understand long-term maintenance, technical debt, or business constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Technical Product Manager (PM)
&lt;/h2&gt;

&lt;p&gt;As a Rails dev, you already understand how a database works, how a checkout flow works, and how long a feature &lt;em&gt;actually&lt;/em&gt; takes to build. &lt;/p&gt;

&lt;p&gt;If you have good "people skills," moving into Product Management is a huge win. You become the person who defines &lt;strong&gt;what&lt;/strong&gt; to build, while the AI (managed by a junior dev) does the actual typing. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it’s safe:&lt;/strong&gt; AI doesn't have "Taste." It doesn't know what users find annoying or what feature will actually make the company money. Empathy and market intuition cannot be automated.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Solopreneur / Indie Hacker
&lt;/h2&gt;

&lt;p&gt;This is my favorite path. If AI makes development 10x faster and 10x cheaper, the biggest winner is the &lt;strong&gt;Solo Developer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the past, you needed a team of 5 to launch a serious SaaS. Today, you can use Rails 8 and Cursor to build, test, and deploy a profitable product by yourself in a month. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it’s safe:&lt;/strong&gt; You stop being an "expense" on someone else's balance sheet and start being the &lt;strong&gt;owner&lt;/strong&gt; of the assets. AI becomes your free labor force, allowing you to compete with big companies.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Cybersecurity and Compliance
&lt;/h2&gt;

&lt;p&gt;AI is going to make it easier for hackers to find bugs in software. As more code is generated autonomously, the "attack surface" of every company is going to explode.&lt;/p&gt;

&lt;p&gt;Moving into security - specifically specialized Rails security or GDPR/Data Privacy compliance - is a very high-paying niche. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it’s safe:&lt;/strong&gt; Trust is the one thing AI cannot generate. A company will always want a human being to be responsible for ensuring their customer data is safe and their servers are locked down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: Don't Panic, Adapt
&lt;/h2&gt;

&lt;p&gt;If you are a Rails developer today, you are in a great position. You already think in "Systems." &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Stop&lt;/strong&gt; focusing on being a faster typer. &lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Start&lt;/strong&gt; focusing on Architecture, Product Vision, and AI Orchestration.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Learn&lt;/strong&gt; how to manage AI agents like you would manage a team of junior developers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The "Junior Developer" role is the one most at risk. If you can move up the chain and become the &lt;strong&gt;Editor-in-Chief&lt;/strong&gt; or the &lt;strong&gt;Product Owner&lt;/strong&gt;, AI won't replace you - it will make you rich.&lt;/p&gt;

</description>
      <category>career</category>
      <category>ai</category>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Demystifying Rails Magic: How Autoloading Actually Works</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Wed, 10 Jun 2026 18:09:35 +0000</pubDate>
      <link>https://dev.to/zilton7/demystifying-rails-magic-how-autoloading-actually-works-85p</link>
      <guid>https://dev.to/zilton7/demystifying-rails-magic-how-autoloading-actually-works-85p</guid>
      <description>&lt;p&gt;I remember when I first started learning Ruby outside of Rails. I was building a small script, and my code kept crashing with &lt;code&gt;NameError: uninitialized constant&lt;/code&gt;. I was so confused because I knew the file existed. I realized that in plain Ruby, you have to manually add &lt;code&gt;require './user'&lt;/code&gt; at the top of every single file.&lt;/p&gt;

&lt;p&gt;In Rails, you never do this. You just type &lt;code&gt;User.first&lt;/code&gt; or &lt;code&gt;OrderService.new&lt;/code&gt; and it "just works." People often call this "Rails Magic," but it isn't magic at all. It is a very structured system called &lt;strong&gt;Zeitwerk&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Understanding how this works will help you stop fighting the file system and start using the framework properly. Here is the breakdown of how Rails manages to "auto-import" your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 1: The Convention (Snake Case to CamelCase)
&lt;/h2&gt;

&lt;p&gt;The core of the magic is a very strict naming rule. Rails assumes that your file names match your class names perfectly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  If your class is &lt;code&gt;User&lt;/code&gt;, the file must be &lt;code&gt;user.rb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  If your class is &lt;code&gt;UserCredential&lt;/code&gt;, the file must be &lt;code&gt;user_credential.rb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  If your class is inside a folder like &lt;code&gt;app/services/billing/payout_manager.rb&lt;/code&gt;, the class name must be &lt;code&gt;Billing::PayoutManager&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you break this rule - for example, naming your file &lt;code&gt;payouts.rb&lt;/code&gt; but naming the class &lt;code&gt;PayoutManager&lt;/code&gt; - Rails will crash. It uses these names to find the files on your hard drive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 2: The Engine (Zeitwerk)
&lt;/h2&gt;

&lt;p&gt;Since Rails 6, the engine that handles this is called &lt;strong&gt;Zeitwerk&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;When your Rails app starts up, Zeitwerk scans all the folders inside &lt;code&gt;app/&lt;/code&gt; (models, controllers, jobs, etc.). It builds a map of all the files.&lt;/p&gt;

&lt;p&gt;When you type &lt;code&gt;User&lt;/code&gt; in your code, Ruby looks at its own memory and says: &lt;em&gt;"I don't know what 'User' is."&lt;/em&gt; Normally, this would trigger an error. But Zeitwerk intercepts that error and says: &lt;em&gt;"Wait! I know where that is. Based on the name 'User', it should be in &lt;code&gt;app/models/user.rb&lt;/code&gt;."&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;Zeitwerk then runs a hidden &lt;code&gt;require&lt;/code&gt; for that file, loads the class into memory, and your code continues running as if the class was always there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 3: Why This is Different from Elixir
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Elixir&lt;/strong&gt;, you don't have "autoloading" in the same way. Elixir is a compiled language. When you start a Phoenix app, the compiler reads all your modules and links them together before the app even starts.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Rails&lt;/strong&gt;, everything is dynamic. Classes can be loaded, deleted, and reloaded while the app is running. This leads us to the most useful feature for solo developers: &lt;strong&gt;Reloading&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 4: Hot Reloading in Development
&lt;/h2&gt;

&lt;p&gt;It is very annoying to restart your server every time you change a line of code. Because Rails uses Zeitwerk for autoloading, it can also do "autounloading."&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;development&lt;/code&gt; mode, Zeitwerk watches your files. When you save &lt;code&gt;user.rb&lt;/code&gt;, Zeitwerk wipes the &lt;code&gt;User&lt;/code&gt; class from Ruby's memory. The next time a request comes in, Zeitwerk sees that &lt;code&gt;User&lt;/code&gt; is missing, finds the updated file, and loads it again. &lt;/p&gt;

&lt;p&gt;This is why you can edit a Rails app and see the changes instantly in the browser. &lt;/p&gt;

&lt;h2&gt;
  
  
  Level 5: When the Magic Fails
&lt;/h2&gt;

&lt;p&gt;Even the best magic has limits. You will usually run into "Autoloading Errors" in two cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The &lt;code&gt;lib/&lt;/code&gt; folder:&lt;/strong&gt; By default, Rails does NOT autoload files in the &lt;code&gt;lib/&lt;/code&gt; directory. If you put a class there, you still have to use &lt;code&gt;require&lt;/code&gt;. (Most devs fix this by adding &lt;code&gt;lib&lt;/code&gt; to the &lt;code&gt;config.autoload_paths&lt;/code&gt; in &lt;code&gt;application.rb&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Initializers:&lt;/strong&gt; Files in &lt;code&gt;config/initializers&lt;/code&gt; only run once when the server starts. If you change a file there, you &lt;strong&gt;must&lt;/strong&gt; restart the server.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Rails "magic" is just a very smart agreement between you and the framework. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Strict Naming:&lt;/strong&gt; Name your files &lt;code&gt;snake_case&lt;/code&gt; and your classes &lt;code&gt;CamelCase&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Zeitwerk:&lt;/strong&gt; The engine that handles the loading so you don't have to write &lt;code&gt;require&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Efficiency:&lt;/strong&gt; Only the code you actually use gets loaded into memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you respect the naming conventions, the magic stays out of your way and lets you focus on building features.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>zeitwerk</category>
      <category>learning</category>
    </item>
    <item>
      <title>Stripe-Style IDs in Rails: A Guide to the custom_id Gem</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Tue, 09 Jun 2026 18:16:16 +0000</pubDate>
      <link>https://dev.to/zilton7/stripe-style-ids-in-rails-a-guide-to-the-makeid-gem-40ec</link>
      <guid>https://dev.to/zilton7/stripe-style-ids-in-rails-a-guide-to-the-makeid-gem-40ec</guid>
      <description>&lt;p&gt;If you have ever used the Stripe API, you’ve noticed their IDs look awesome. Instead of a random number or a long, confusing UUID, they look like this: &lt;code&gt;cus_M1p7abc123&lt;/code&gt; or &lt;code&gt;ch_3Oixyz456&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These are called &lt;strong&gt;Prefixed IDs&lt;/strong&gt;. They are great for two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Context:&lt;/strong&gt; When you see &lt;code&gt;inv_...&lt;/code&gt; in your logs, you immediately know it’s an Invoice without looking at the table name.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Security:&lt;/strong&gt; It hides your business volume. Hackers can't guess that your next user is ID 501.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the past, setting this up in Rails required a lot of custom code in your models. But recently, I found a gem that makes this process incredibly simple: &lt;strong&gt;&lt;a href="https://github.com/pniemczyk/custom_id" rel="noopener noreferrer"&gt;custom_id&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is how to implement professional, Stripe-style IDs in your Rails 8 app in 4 steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: Install the Gem
&lt;/h2&gt;

&lt;p&gt;Add the gem to your Gemfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"custom_id"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run  in your terminal&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install
&lt;/span&gt;rails custom_id:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 2: The Migration
&lt;/h2&gt;

&lt;p&gt;Just like with ULIDs or Snowflake IDs, we need to tell the database that our primary key is a &lt;code&gt;string&lt;/code&gt; and not an &lt;code&gt;integer&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g model Project name:string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the migration file and disable the automatic ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# db/migrate/XXXXXXXXXXXXXX_create_projects.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateProjects&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:projects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="c1"&gt;# We use string for the prefixed ID&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 3: Configure the Model
&lt;/h2&gt;

&lt;p&gt;This is where the gem shines. You just use the &lt;code&gt;custom_id&lt;/code&gt; macro. You tell it what prefix you want to use, and it handles the rest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/project.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="c1"&gt;# This tells the gem to generate an ID starting with 'prj_'&lt;/span&gt;
  &lt;span class="n"&gt;cid&lt;/span&gt; &lt;span class="s2"&gt;"usr"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, the gem uses a collision-resistant loop with database uniqueness check.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 4: Seeing it in Action
&lt;/h2&gt;

&lt;p&gt;Open your Rails console (&lt;code&gt;bin/rails c&lt;/code&gt;) and create a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"Cloud Empire"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; "prj_7k2n9zp1m5r8"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks professional, it’s easy to read in your logs, and it’s perfectly unique.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced: Customizing your IDs
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;custom_id&lt;/code&gt; gem is very flexible. You can change the length of the random part if you have a massive amount of data and want to avoid collisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="c1"&gt;# Use a 3-letter prefix and a longer 20-character random string&lt;/span&gt;
  &lt;span class="n"&gt;cid&lt;/span&gt; &lt;span class="ss"&gt;:usr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;size: &lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why I prefer this over UUIDs?
&lt;/h2&gt;

&lt;p&gt;I love UUIDs for their uniqueness, but I hate how they look in URLs. &lt;br&gt;
&lt;code&gt;/users/550e8400-e29b-41d4-a716-446655440000&lt;/code&gt; is ugly and hard to double-click and copy.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/users/usr_7k2n9zp1m5r8&lt;/code&gt; is clean, tells me exactly what the object is, and fits perfectly in a mobile app UI or a support email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;custom_id&lt;/code&gt; gem is a "Quality of Life" improvement for Rails developers. It takes an enterprise-level feature (Prefixed IDs) and turns it into a one-line setup.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Install&lt;/strong&gt; the gem.&lt;/li&gt;
&lt;li&gt; Set your primary key to &lt;strong&gt;string&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Add &lt;strong&gt;&lt;code&gt;cid 'prefix'&lt;/code&gt;&lt;/strong&gt; to your model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It’s a tiny change that makes your Rails monolith feel significantly more polished and "pro."&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>security</category>
      <category>database</category>
    </item>
    <item>
      <title>Rapid UI Development: Top 4 Component Kits for Solo Rails Devs</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Mon, 08 Jun 2026 18:10:29 +0000</pubDate>
      <link>https://dev.to/zilton7/rapid-ui-development-top-4-component-kits-for-solo-rails-devs-3hfg</link>
      <guid>https://dev.to/zilton7/rapid-ui-development-top-4-component-kits-for-solo-rails-devs-3hfg</guid>
      <description>&lt;p&gt;I remember the days when building a professional-looking dashboard in Rails meant spending a whole week wrestling with CSS flexbox and custom SVG icons. You’d get the backend working in an hour, but the frontend would take forever.&lt;/p&gt;

&lt;p&gt;In 2026, the "One-Person Framework" philosophy has finally fixed this. We now have high-quality, pre-built component libraries that give us a "premium" look instantly. &lt;/p&gt;

&lt;p&gt;Instead of building a "Modal" or a "DataTable" from scratch, you just install a library and render a component. Here are the top pre-built libraries you should use to ship your next Rails 8 project in record time.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Shadcn Rails (The Solo Developer's Favorite)
&lt;/h2&gt;

&lt;p&gt;If you have been watching the React world, you know that &lt;strong&gt;Shadcn&lt;/strong&gt; is the biggest thing in design. It’s famous because you don't "install" it as a black-box library; you copy-paste the code into your project so you have 100% control.&lt;/p&gt;

&lt;p&gt;The Rails community now has &lt;strong&gt;&lt;a href="https://shadcn.rails-components.com/" rel="noopener noreferrer"&gt;Shadcn Rails&lt;/a&gt;&lt;/strong&gt;, and it is amazing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it works:&lt;/strong&gt;&lt;br&gt;
It uses &lt;strong&gt;ViewComponent&lt;/strong&gt; and &lt;strong&gt;Tailwind CSS&lt;/strong&gt;. When you add a component, it generates a Ruby class and an ERB file in your &lt;code&gt;app/components&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You find a component (like a Command Palette or a Calendar) on their site.&lt;/li&gt;
&lt;li&gt;You run a simple command to "add" it to your app.&lt;/li&gt;
&lt;li&gt;You use it in your view: &lt;code&gt;&amp;lt;%= render UI::Button.new { "Save" } %&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because the code is now &lt;em&gt;yours&lt;/em&gt;, you can easily tweak the Tailwind classes to match your brand without fighting a gem's internal CSS.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Polaris ViewComponents (The Enterprise Choice)
&lt;/h2&gt;

&lt;p&gt;If you want your app to feel as solid and reliable as &lt;strong&gt;Shopify&lt;/strong&gt;, you should use &lt;strong&gt;&lt;a href="https://polarisviewcomponents.org/" rel="noopener noreferrer"&gt;Polaris ViewComponents&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Polaris is the official design system for Shopify. Since Shopify is built on Rails, their component library is one of the most battle-tested in the world. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it works:&lt;/strong&gt;&lt;br&gt;
It is extremely high-quality. It handles accessibility (A11y), keyboard navigation, and complex states perfectly. If you are building a B2B SaaS or a complex internal tool, this is the "No-Brainer" choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Catch:&lt;/strong&gt;&lt;br&gt;
Your app will look a lot like the Shopify admin. If you want a unique, "funky" startup look, you might find Polaris a bit too "corporate."&lt;/p&gt;
&lt;h2&gt;
  
  
  3. PhlexUI (The Speed King)
&lt;/h2&gt;

&lt;p&gt;If you have moved away from ERB and are using &lt;strong&gt;Phlex&lt;/strong&gt; (the pure Ruby view engine), then &lt;strong&gt;&lt;a href="https://www.phlexui.com/" rel="noopener noreferrer"&gt;PhlexUI&lt;/a&gt;&lt;/strong&gt; is your best friend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it works:&lt;/strong&gt;&lt;br&gt;
It is built specifically for the Phlex ecosystem. Because there are no HTML templates to parse, it is incredibly fast. The components are beautifully designed using Tailwind and are very easy to customize because they are just plain Ruby classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Workflow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;PhlexUI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;PhlexUI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Card&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;PhlexUI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Typography&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;H3&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="s2"&gt;"Revenue"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;PhlexUI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Card&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;PhlexUI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Typography&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;P&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="s2"&gt;"$12,400"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It feels like writing a script rather than writing a webpage. For a solo dev, the "Flow State" you get from staying in pure Ruby is a huge productivity boost.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Flowbite / DaisyUI (The "Light" Alternative)
&lt;/h2&gt;

&lt;p&gt;Sometimes, a full ViewComponent library is overkill. You might just want some pre-styled Tailwind classes. In that case, &lt;strong&gt;Flowbite&lt;/strong&gt; or &lt;strong&gt;DaisyUI&lt;/strong&gt; are great.&lt;/p&gt;

&lt;p&gt;They don't give you Ruby classes. Instead, they give you a set of "standard" HTML structures and Tailwind classes that make things like tooltips and accordions work using the native &lt;code&gt;popover&lt;/code&gt; and &lt;code&gt;details&lt;/code&gt; HTML tags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Workflow:&lt;/strong&gt;&lt;br&gt;
You copy a snippet of HTML from their docs and paste it into your Rails view. It’s the fastest way to get a single component, but it’s harder to maintain than a structured library like Shadcn Rails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: Which one should you pick?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Choose Shadcn Rails&lt;/strong&gt; if you want a modern "Linear/Vercel" look and want total control over the code.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Choose Polaris&lt;/strong&gt; if you are building an enterprise tool where reliability and accessibility are more important than a unique brand.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Choose PhlexUI&lt;/strong&gt; if you are already using Phlex and want the fastest possible development experience.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's pretty much it. Stop spending your time on CSS gradients. Pick a library, snap the components together, and get back to building the features that actually make you money.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>frontend</category>
      <category>design</category>
    </item>
    <item>
      <title>ViewComponent vs. Phlex: Which Ruby UI Library Should You Choose?</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sun, 07 Jun 2026 18:13:28 +0000</pubDate>
      <link>https://dev.to/zilton7/viewcomponent-vs-phlex-which-ruby-ui-library-should-you-choose-4po4</link>
      <guid>https://dev.to/zilton7/viewcomponent-vs-phlex-which-ruby-ui-library-should-you-choose-4po4</guid>
      <description>&lt;p&gt;I remember when my &lt;code&gt;app/views&lt;/code&gt; folder was a complete disaster. I had deeply nested partials, instance variables floating everywhere, and logic hidden inside HTML tags. If I wanted to change the color of a button, I had to search through 50 different files.&lt;/p&gt;

&lt;p&gt;In 2026, we don't build Rails apps like that anymore. We use &lt;strong&gt;Components&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Components allow you to treat your UI like Lego blocks. You build a "Button" once, and you reuse it everywhere. It makes your code cleaner, easier to test, and much faster to write. &lt;/p&gt;

&lt;p&gt;Here are the best Rails component libraries you should be using today to keep your monolith organized.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. ViewComponent (The Gold Standard)
&lt;/h2&gt;

&lt;p&gt;Created by the team at GitHub, &lt;strong&gt;ViewComponent&lt;/strong&gt; is the most popular choice. It turns your views into Ruby objects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I like it:&lt;/strong&gt;&lt;br&gt;
Instead of a "dumb" partial, you get a Ruby class and an HTML file. You can pass data into the class, and Ruby will yell at you if you forget a required parameter. It makes your UI "Type Safe."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/components/button_component.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ButtonComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
    &lt;span class="vi"&gt;@url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/components/button_component.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="vi"&gt;@title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-500 text-white p-2 rounded"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The best part? You can &lt;strong&gt;Unit Test&lt;/strong&gt; your UI. You don't need a slow browser test just to see if a button has the right text. You can test it in milliseconds using standard Minitest.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Phlex (The Speed Demon)
&lt;/h2&gt;

&lt;p&gt;If you hate context-switching between Ruby and ERB, you will love &lt;strong&gt;Phlex&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Phlex is a new challenger that is taking the Rails world by storm. It doesn't use &lt;code&gt;.html.erb&lt;/code&gt; files at all. You write your HTML in &lt;strong&gt;Pure Ruby&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/components/card.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Card&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Phlex&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;template&lt;/span&gt;
    &lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"p-4 border rounded shadow"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Hello from Phlex"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"This is 10x faster than ERB."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why I like it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Performance:&lt;/strong&gt; It is significantly faster than rendering ERB templates.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;No context switching:&lt;/strong&gt; You stay in Ruby land. You can use standard Ruby loops, conditions, and methods without the messy &lt;code&gt;&amp;lt;% %&amp;gt;&lt;/code&gt; tags.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;High ROI:&lt;/strong&gt; For a solo developer, Phlex makes building complex UI kits feel like writing a normal script.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Lookbook (The Designer's Secret)
&lt;/h2&gt;

&lt;p&gt;If you are using ViewComponent or Phlex, &lt;strong&gt;Lookbook&lt;/strong&gt; is a mandatory tool. &lt;/p&gt;

&lt;p&gt;Think of it as a "Storybook" but for Rails. It creates a private dashboard in your development environment where you can see every component you've built. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to use it:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Install the gem.&lt;/li&gt;
&lt;li&gt; Mount the engine in your &lt;code&gt;routes.rb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Visit &lt;code&gt;/lookbook&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can now click through your buttons, modals, and navigation bars to see how they look with different data. It is the best way to maintain a consistent design system without getting lost in your own code.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Tailwind-Stimulus-Components
&lt;/h2&gt;

&lt;p&gt;Components aren't just about HTML; they are also about &lt;strong&gt;Behavior&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;If you use Tailwind CSS (which you should), you don't want to write custom JavaScript for every single dropdown or modal. This library gives you pre-written Stimulus controllers that handle the "boring" stuff.&lt;/p&gt;

&lt;p&gt;Instead of writing a "modal-open" script, you just add a data attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"modal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;modal#open"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-modal-target=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
     &lt;span class="c"&gt;&amp;lt;!-- Modal Content --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a perfect match for the "One-Person Framework" philosophy: use standard, battle-tested behavior so you can focus on the business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: Which one should you pick?
&lt;/h2&gt;

&lt;p&gt;If you are a solo developer trying to choose a stack today, here is my recommendation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Choose ViewComponent&lt;/strong&gt; if you want the safest, most documented option. It is used by GitHub and Shopify, so it isn't going anywhere.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Choose Phlex&lt;/strong&gt; if you are a Ruby purist who wants the absolute maximum speed and hates ERB syntax.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Always use Lookbook&lt;/strong&gt; to keep yourself organized. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Moving from partials to components is the single biggest "Level Up" you can take as a Rails developer. It turns your messy views into a professional, modular system.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The AI Support Agent: Connecting n8n, OpenAI, and Your Rails DB</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sat, 06 Jun 2026 18:14:28 +0000</pubDate>
      <link>https://dev.to/zilton7/the-ai-support-agent-connecting-n8n-openai-and-your-rails-db-54kd</link>
      <guid>https://dev.to/zilton7/the-ai-support-agent-connecting-n8n-openai-and-your-rails-db-54kd</guid>
      <description>&lt;p&gt;I used to wake up every morning to a wall of support emails. As a solo developer, this is the ultimate "flow-state" killer. &lt;/p&gt;

&lt;p&gt;Someone can't find their invoice. Someone else wants to know why their "Pro" features aren't active yet. To answer these, I had to open my Rails console, look up the user by email, check their &lt;code&gt;plan_id&lt;/code&gt; and &lt;code&gt;stripe_customer_id&lt;/code&gt;, and then type out a polite response. &lt;/p&gt;

&lt;p&gt;It takes 10 minutes per ticket. If you have 10 tickets, your first two hours of work are gone before you've even touched your code.&lt;/p&gt;

&lt;p&gt;In 2026, I’ve automated 90% of this work. I don't let AI talk directly to my customers (that’s dangerous), but I do let AI &lt;strong&gt;draft the replies&lt;/strong&gt; for me, with full context from my production database. &lt;/p&gt;

&lt;p&gt;Here is how I built a "Support Sidekick" using &lt;strong&gt;n8n&lt;/strong&gt;, &lt;strong&gt;OpenAI&lt;/strong&gt;, and a &lt;strong&gt;PostgreSQL&lt;/strong&gt; connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Strategy: The "Human-in-the-Loop"
&lt;/h2&gt;

&lt;p&gt;We aren't building a bot that auto-replies. We are building a system that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Catches an incoming support email.&lt;/li&gt;
&lt;li&gt;Looks up the user's data in our real Rails database.&lt;/li&gt;
&lt;li&gt;Asks OpenAI to write a draft based on that data.&lt;/li&gt;
&lt;li&gt;Sends the draft to a private Discord channel for us to "Approve &amp;amp; Send."&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  STEP 1: The Trigger (Email or Webhook)
&lt;/h2&gt;

&lt;p&gt;First, you need a way for n8n to see the support request. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The Easy Way:&lt;/strong&gt; Use the &lt;strong&gt;Gmail&lt;/strong&gt; or &lt;strong&gt;Outlook&lt;/strong&gt; node in n8n to watch for new emails with the subject "Support."&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The Pro Way:&lt;/strong&gt; Use a &lt;strong&gt;Webhook&lt;/strong&gt; node. If you use a tool like HelpScout or Crisp, they can send a webhook to n8n every time a new ticket is created.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  STEP 2: Connecting the Production DB (Security First)
&lt;/h2&gt;

&lt;p&gt;To give the AI context, it needs to know who the user is. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CRITICAL SECURITY RULE:&lt;/strong&gt; Never connect n8n to your production database using an admin account. &lt;br&gt;
Create a &lt;strong&gt;Read-Only&lt;/strong&gt; user in Postgres that can only see the &lt;code&gt;users&lt;/code&gt; and &lt;code&gt;subscriptions&lt;/code&gt; tables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Run this in your production DB once&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;n8n_read_only&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'your_password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;my_app_production&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;n8n_read_only&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;n8n_read_only&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscriptions&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;n8n_read_only&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In n8n, add a &lt;strong&gt;PostgreSQL node&lt;/strong&gt;. Use the email from the incoming ticket to find the user:&lt;br&gt;
&lt;code&gt;SELECT * FROM users WHERE email = '{{ $json.from_email }}' LIMIT 1;&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: The AI Brain (OpenAI)
&lt;/h2&gt;

&lt;p&gt;Now we add the &lt;strong&gt;OpenAI node&lt;/strong&gt;. We want to use a high-context model like GPT-4o. &lt;/p&gt;

&lt;p&gt;The secret here is the &lt;strong&gt;System Prompt&lt;/strong&gt;. You need to give the AI the data you just pulled from the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System Prompt Example:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You are a support assistant for [App Name]. Here is the user's data: &lt;br&gt;
Plan: {{ $node.postgres.json.plan_name }}&lt;br&gt;
Joined: {{ $node.postgres.json.created_at }}&lt;br&gt;
Last Payment: {{ $node.postgres.json.last_payment_date }}&lt;/p&gt;

&lt;p&gt;Use this data to draft a polite, helpful response to the user's question. If they are on a Free plan, suggest they upgrade for priority support. If they are a Pro user, be extra thankful."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  STEP 4: The Delivery (Discord or Slack)
&lt;/h2&gt;

&lt;p&gt;Finally, we send the AI's draft to where we spend our time: &lt;strong&gt;Discord&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;Discord node&lt;/strong&gt; (or Slack). Set the message content to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;New Support Ticket from {{ $json.from_email }}&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User Question:&lt;/strong&gt; {{ $json.subject }}&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Drafted Reply:&lt;/strong&gt;&lt;br&gt;
{{ $node.openai.json.content }}&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why this is a game changer
&lt;/h2&gt;

&lt;p&gt;When I see a notification on my phone now, I don't have to go digging through my database. I see the user's status and a perfectly written draft immediately. &lt;/p&gt;

&lt;p&gt;I just copy the AI draft, make a tiny tweak if needed, and hit "Send" in my email client. What used to take 10 minutes now takes &lt;strong&gt;30 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;As a solo developer, you should only do "High Value" work. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;High Value:&lt;/strong&gt; Fixing a critical bug, building a new feature.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Low Value:&lt;/strong&gt; Looking up a user's sign-up date for a support ticket.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By using n8n to bridge the gap between your &lt;strong&gt;Production Database&lt;/strong&gt; and &lt;strong&gt;OpenAI&lt;/strong&gt;, you offload the low-value research work to the machines. Your Rails monolith stays clean, and your brain stays focused on the code.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>ai</category>
      <category>rails</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
