<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://norvilis.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://norvilis.com/" rel="alternate" type="text/html" /><updated>2026-10-15T23:29:08+00:00</updated><id>https://norvilis.com/feed.xml</id><title type="html">DevBlog by Zil Norvilis</title><subtitle>My thoughts on Web Developing, Solopreneurship, Linux and other. Including notes to myself for easier and quicker developing.</subtitle><author><name>Zil Norvilis</name></author><entry><title type="html">The Shopify Goldmine: How to Build Your First App in Rails</title><link href="https://norvilis.com/the-shopify-goldmine-how-to-build-your-first-app-in-rails/" rel="alternate" type="text/html" title="The Shopify Goldmine: How to Build Your First App in Rails" /><published>2026-08-07T00:00:00+00:00</published><updated>2026-08-07T00:00:00+00:00</updated><id>https://norvilis.com/the-shopify-goldmine-how-to-build-your-first-app-in-rails</id><content type="html" xml:base="https://norvilis.com/the-shopify-goldmine-how-to-build-your-first-app-in-rails/"><![CDATA[<p>If you know Ruby on Rails, you are already sitting on a goldmine.</p>

<p>Shopify is one of the biggest e-commerce platforms in the world, and here is a secret: <strong>Shopify was built with Rails.</strong> Because of this, the Shopify ecosystem is incredibly friendly to Rails developers.</p>

<p>Building a Shopify app is basically building a standard Rails app that lives inside another website’s dashboard. It is a fantastic way for a solo developer to make passive income because Shopify handles the customers, the billing, and the marketing for you.</p>

<p>Here is the quickest way to build your first Shopify app as a complete beginner.</p>

<h2 id="step-1-the-partner-account">STEP 1: The Partner Account</h2>

<p>Before you write any code, you need to join the club.</p>
<ol>
  <li>Go to the <a href="https://partners.shopify.com/">Shopify Partners</a> website and create a free account.</li>
  <li>Create a “Development Store.” This is a fake online store where you can install your app for free to test it.</li>
</ol>

<h2 id="step-2-the-shopify-app-gem">STEP 2: The Shopify App Gem</h2>

<p>We don’t build the connection to Shopify from scratch. Shopify provides an official gem that handles all the complex “handshake” logic (OAuth) for us.</p>

<p>Add this to your <code class="language-plaintext highlighter-rouge">Gemfile</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s1">'shopify_app'</span>
</code></pre></div></div>
<p>Run <code class="language-plaintext highlighter-rouge">bundle install</code>.</p>

<p>Now, run the Shopify generator. This will turn your boring Rails app into a Shopify-ready engine:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails generate shopify_app
</code></pre></div></div>

<p>This command will ask you for your <strong>API Key</strong> and <strong>Secret Key</strong>. You can find these in your Shopify Partner Dashboard under your App settings.</p>

<h2 id="step-3-connecting-your-laptop-the-tunnel">STEP 3: Connecting Your Laptop (The Tunnel)</h2>

<p>This is where beginners usually get stuck. Shopify needs to talk to your Rails app, but your Rails app is currently hidden inside your laptop (<code class="language-plaintext highlighter-rouge">localhost</code>).</p>

<p>Shopify cannot see your laptop. You need to create a “Tunnel” to the internet.</p>

<p>The Shopify CLI comes with a built-in tunnel tool. Run this in your terminal:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>shopify app dev
</code></pre></div></div>

<p>This will give you a public URL like <code class="language-plaintext highlighter-rouge">https://random-words.cloudflare.com</code>. Copy this URL and paste it into the “App Setup” section of your Shopify Partner Dashboard. Now, Shopify can finally send data to your Rails code.</p>

<h2 id="step-4-building-the-ui-polaris">STEP 4: Building the UI (Polaris)</h2>

<p>When a shop owner opens your app, they expect it to look like Shopify. They don’t want to see a custom “bootstrap” design.</p>

<p>Shopify uses a design system called <strong>Polaris</strong>. In a previous article, I mentioned the <code class="language-plaintext highlighter-rouge">polaris_view_components</code> gem. This is the best way to make your app look professional instantly.</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/home/index.html.erb --&gt;</span>
<span class="cp">&lt;%=</span> <span class="n">render</span> <span class="no">Polaris</span><span class="o">::</span><span class="no">Page</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"My Discount Tool"</span><span class="p">)</span> <span class="k">do</span> <span class="cp">%&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">render</span> <span class="no">Polaris</span><span class="o">::</span><span class="no">Card</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">sectioned: </span><span class="kp">true</span><span class="p">)</span> <span class="k">do</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;p&gt;</span>Welcome! This app will help you manage your store better.<span class="nt">&lt;/p&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">render</span> <span class="no">Polaris</span><span class="o">::</span><span class="no">Button</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">primary: </span><span class="kp">true</span><span class="p">)</span> <span class="p">{</span> <span class="s2">"Create Discount"</span> <span class="p">}</span> <span class="cp">%&gt;</span>
  <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p>With just a few lines of Ruby, your app now looks like a multi-million dollar software product.</p>

<h2 id="step-5-webhooks-the-listening-ear">STEP 5: Webhooks (The “Listening” Ear)</h2>

<p>This is the most important concept in Shopify apps. A “Webhook” is Shopify calling your app to say: <em>“Hey, a customer just bought something!”</em> or <em>“Hey, the store owner just deleted a product!”</em></p>

<p>The <code class="language-plaintext highlighter-rouge">shopify_app</code> gem handles this for you. You just define which events you want to listen to in <code class="language-plaintext highlighter-rouge">config/initializers/shopify_app.rb</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span><span class="p">.</span><span class="nf">webhooks</span> <span class="o">=</span> <span class="p">[</span>
  <span class="p">{</span> <span class="ss">topic: </span><span class="s1">'orders/create'</span><span class="p">,</span> <span class="ss">address: </span><span class="s1">'https://your-tunnel.com/webhooks/orders_create'</span> <span class="p">}</span>
<span class="p">]</span>
</code></pre></div></div>

<p>Then, you create a simple background job (using <strong>Solid Queue</strong>) to process the data when the order comes in.</p>

<h2 id="summary">Summary</h2>

<p>Building a Shopify app is the ultimate “One-Person Framework” project.</p>
<ol>
  <li><strong>Rails</strong> handles the logic.</li>
  <li><strong>Shopify App Gem</strong> handles the security.</li>
  <li><strong>Polaris</strong> handles the design.</li>
  <li><strong>Shopify App Store</strong> handles the customers.</li>
</ol>

<p>If you have a weekend and a good idea for a small store utility, you can have an app live and potentially making money by Monday morning.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="shopify" /><category term="ruby" /><category term="webdev" /><summary type="html"><![CDATA[If you know Ruby on Rails, you are already sitting on a goldmine.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-08-07-The-Shopify-Goldmine-How-To-Build-Your-First-App-In-Rails/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-08-07-The-Shopify-Goldmine-How-To-Build-Your-First-App-In-Rails/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Real-Time Engagement: Web Push Notifications in Rails 8</title><link href="https://norvilis.com/real-time-engagement-web-push-notifications-in-rails-8/" rel="alternate" type="text/html" title="Real-Time Engagement: Web Push Notifications in Rails 8" /><published>2026-06-23T00:00:00+00:00</published><updated>2026-06-23T00:00:00+00:00</updated><id>https://norvilis.com/real-time-engagement-web-push-notifications-in-rails-8</id><content type="html" xml:base="https://norvilis.com/real-time-engagement-web-push-notifications-in-rails-8/"><![CDATA[<p>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.</p>

<p>In 2026, the <strong>Web Push API</strong> 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.</p>

<p>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.</p>

<h2 id="the-mental-model-how-web-push-works">The Mental Model: How Web Push Works</h2>

<ol>
  <li><strong>Subscription:</strong> The user clicks “Allow” in their browser. The browser gives us a “Subscription” object (a URL and some secret keys).</li>
  <li><strong>Storage:</strong> We save that object in our database, linked to the <code class="language-plaintext highlighter-rouge">User</code>.</li>
  <li><strong>The Push:</strong> When an event happens, our Rails server signs a message and sends it to the browser’s “Push Service” (Google, Apple, or Mozilla).</li>
  <li><strong>Delivery:</strong> The Service Worker on the user’s device wakes up and shows the notification.</li>
</ol>

<h2 id="step-1-the-vapid-keys">STEP 1: The VAPID Keys</h2>

<p>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.</p>

<p>Add the gem to your <code class="language-plaintext highlighter-rouge">Gemfile</code>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s2">"web-push"</span>
</code></pre></div></div>

<p>Run this in your terminal to generate your keys:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Generate keys</span>
web-push generate-vapid-keys
</code></pre></div></div>
<p>Copy these keys into your <code class="language-plaintext highlighter-rouge">.kamal/secrets</code> or your credentials file.</p>

<h2 id="step-2-the-database-setup">STEP 2: The Database Setup</h2>

<p>We need to store the subscription for each user. A user might have multiple subscriptions (one for their phone, one for their laptop).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g model WebPushSubscription user:references endpoint:string p256dh_key:string auth_key:string
rails db:migrate
</code></pre></div></div>

<h2 id="step-3-the-service-worker-javascript">STEP 3: The Service Worker (Javascript)</h2>

<p>Service Workers must live in the <code class="language-plaintext highlighter-rouge">public/</code> folder to have access to the whole site. Create a file at <code class="language-plaintext highlighter-rouge">public/service-worker.js</code>. This file listens for the “push” event even when your website is closed.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// public/service-worker.js</span>
<span class="nb">self</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">push</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
  
  <span class="nx">event</span><span class="p">.</span><span class="nx">waitUntil</span><span class="p">(</span>
    <span class="nb">self</span><span class="p">.</span><span class="nx">registration</span><span class="p">.</span><span class="nx">showNotification</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">body</span><span class="p">:</span> <span class="nx">data</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span>
      <span class="na">icon</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/logo.png</span><span class="dl">"</span>
    <span class="p">})</span>
  <span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Now, register this worker in your <code class="language-plaintext highlighter-rouge">app/javascript/application.js</code>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="dl">"</span><span class="s2">serviceWorker</span><span class="dl">"</span> <span class="k">in</span> <span class="nb">navigator</span><span class="p">)</span> <span class="p">{</span>
  <span class="nb">navigator</span><span class="p">.</span><span class="nx">serviceWorker</span><span class="p">.</span><span class="nx">register</span><span class="p">(</span><span class="dl">"</span><span class="s2">/service-worker.js</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="step-4-creating-the-subscription">STEP 4: Creating the Subscription</h2>

<p>We need a Stimulus controller to ask the user for permission and send the subscription to Rails.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/javascript/controllers/push_subscription_controller.js</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/stimulus</span><span class="dl">"</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="kd">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nx">subscribe</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">registration</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">navigator</span><span class="p">.</span><span class="nx">serviceWorker</span><span class="p">.</span><span class="nx">ready</span><span class="p">;</span>
    
    <span class="kd">const</span> <span class="nx">subscription</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">registration</span><span class="p">.</span><span class="nx">pushManager</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">({</span>
      <span class="na">userVisibleOnly</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">applicationServerKey</span><span class="p">:</span> <span class="dl">"</span><span class="s2">YOUR_PUBLIC_VAPID_KEY</span><span class="dl">"</span>
    <span class="p">});</span>

    <span class="c1">// Send this JSON to your Rails controller</span>
    <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/web_push_subscriptions</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">X-CSRF-Token</span><span class="dl">"</span><span class="p">:</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">[name='csrf-token']</span><span class="dl">"</span><span class="p">).</span><span class="nx">content</span> <span class="p">},</span>
      <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">subscription</span><span class="p">)</span>
    <span class="p">});</span>
    
    <span class="nx">alert</span><span class="p">(</span><span class="dl">"</span><span class="s2">Subscribed!</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="step-5-sending-the-notification-the-ruby-part">STEP 5: Sending the Notification (The Ruby Part)</h2>

<p>Finally, we send the notification from a background job so we don’t slow down the app.</p>

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

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

<h2 id="summary">Summary</h2>

<p>Web Push is the “Pro” way to bring users back to your app.</p>

<ol>
  <li><strong>VAPID keys</strong> handle the security.</li>
  <li><strong>Service Workers</strong> handle the delivery while the user is away.</li>
  <li><strong>Solid Queue</strong> handles the background sending.</li>
</ol>

<p>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.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="ruby" /><category term="webdev" /><category term="notifications" /><summary type="html"><![CDATA[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.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-06-23-Real-Time-Engagement-Web-Push-Notifications-In-Rails-8/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-06-23-Real-Time-Engagement-Web-Push-Notifications-In-Rails-8/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Budget-Friendly Monolith: Rails File Uploads with Cloudflare R2</title><link href="https://norvilis.com/the-budget-friendly-monolith-rails-file-uploads-with-cloudflare-r2/" rel="alternate" type="text/html" title="The Budget-Friendly Monolith: Rails File Uploads with Cloudflare R2" /><published>2026-06-21T00:00:00+00:00</published><updated>2026-06-21T00:00:00+00:00</updated><id>https://norvilis.com/the-budget-friendly-monolith-rails-file-uploads-with-cloudflare-r2</id><content type="html" xml:base="https://norvilis.com/the-budget-friendly-monolith-rails-file-uploads-with-cloudflare-r2/"><![CDATA[<p>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 <strong>Egress Fees</strong>. Every time a user downloads a file or views an image, AWS charges you for the data leaving their servers.</p>

<p>For a solo developer or a small startup, these “hidden” fees can grow very fast.</p>

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

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

<h2 id="step-1-the-credentials">STEP 1: The Credentials</h2>

<p>First, log into your Cloudflare dashboard and create a new <strong>R2 Bucket</strong>.</p>

<p>Once the bucket is created, you need to generate “API Tokens.”</p>
<ol>
  <li>Go to <strong>R2</strong> &gt; <strong>Manage R2 API Tokens</strong>.</li>
  <li>Create a new token with <strong>Edit</strong> permissions.</li>
  <li>Save your <strong>Access Key ID</strong>, <strong>Secret Access Key</strong>, and the <strong>Jurisdiction-specific endpoint</strong>.</li>
</ol>

<p>The endpoint will look something like this:
<code class="language-plaintext highlighter-rouge">https://&lt;account_id&gt;.r2.cloudflorage.com</code></p>

<h2 id="step-2-the-gems">STEP 2: The Gems</h2>

<p>Even though we are using Cloudflare, we still use the official AWS S3 gem because Cloudflare built R2 to be a “drop-in” replacement.</p>

<p>Add this to your <code class="language-plaintext highlighter-rouge">Gemfile</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s2">"aws-sdk-s3"</span><span class="p">,</span> <span class="ss">require: </span><span class="kp">false</span>
</code></pre></div></div>
<p>Run <code class="language-plaintext highlighter-rouge">bundle install</code>.</p>

<h2 id="step-3-configuration-storageyml">STEP 3: Configuration (<code class="language-plaintext highlighter-rouge">storage.yml</code>)</h2>

<p>Now we need to tell Rails how to talk to the R2 bucket. Open your <code class="language-plaintext highlighter-rouge">config/storage.yml</code> and add a new section for Cloudflare.</p>

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

<p><em>Note: Make sure to add those ENV variables to your <code class="language-plaintext highlighter-rouge">.kamal/secrets</code> or your production server!</em></p>

<h2 id="step-4-tell-rails-to-use-r2">STEP 4: Tell Rails to use R2</h2>

<p>Finally, update your production environment to use the new service.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/environments/production.rb</span>
<span class="n">config</span><span class="p">.</span><span class="nf">active_storage</span><span class="p">.</span><span class="nf">service</span> <span class="o">=</span> <span class="ss">:cloudflare</span>
</code></pre></div></div>

<h2 id="bonus-enabling-direct-uploads-cors">Bonus: Enabling Direct Uploads (CORS)</h2>

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

<p>Cloudflare makes this easy. Go to your Bucket settings in the dashboard and paste this JSON into the CORS section:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"AllowedOrigins"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"https://yourdomain.com"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"AllowedMethods"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"GET"</span><span class="p">,</span><span class="w"> </span><span class="s2">"PUT"</span><span class="p">,</span><span class="w"> </span><span class="s2">"POST"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"AllowedHeaders"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"*"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"ExposeHeaders"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ETag"</span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>This tells Cloudflare: <em>“It is safe to let users from my website upload files directly to this bucket.”</em></p>

<h2 id="summary">Summary</h2>

<p>Switching from AWS S3 to Cloudflare R2 is one of the smartest “business” moves a solo developer can make.</p>

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

<p>By combining <strong>Rails 8</strong>, <strong>Kamal 2</strong>, and <strong>Cloudflare R2</strong>, you are building a production stack that is fast, secure, and incredibly cheap to run.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="storage" /><category term="cloudflare" /><category term="devops" /><summary type="html"><![CDATA[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 Egress Fees. Every time a user downloads a file or views an image, AWS charges you for the data leaving their servers.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-06-21-The-Budget-Friendly-Monolith-Rails-File-Uploads-With-Cloudflare-R2/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-06-21-The-Budget-Friendly-Monolith-Rails-File-Uploads-With-Cloudflare-R2/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Pro File Uploads in Rails 8: Speed and Scalability with Direct Uploads</title><link href="https://norvilis.com/pro-file-uploads-in-rails-8-speed-and-scalability-with-direct-uploads/" rel="alternate" type="text/html" title="Pro File Uploads in Rails 8: Speed and Scalability with Direct Uploads" /><published>2026-06-19T00:00:00+00:00</published><updated>2026-06-19T00:00:00+00:00</updated><id>https://norvilis.com/pro-file-uploads-in-rails-8-speed-and-scalability-with-direct-uploads</id><content type="html" xml:base="https://norvilis.com/pro-file-uploads-in-rails-8-speed-and-scalability-with-direct-uploads/"><![CDATA[<p>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 <em>then</em> your server sends it to S3 or Google Cloud.</p>

<p>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.</p>

<p>In 2026, the professional way to handle this is <strong>Direct Uploads</strong>.</p>

<p>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.</p>

<h2 id="step-1-configure-your-storage">STEP 1: Configure Your Storage</h2>

<p>First, make sure you aren’t using the local disk for production. You need a cloud provider like AWS S3 or Cloudflare R2.</p>

<p>In your <code class="language-plaintext highlighter-rouge">config/storage.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">amazon</span><span class="pi">:</span>
  <span class="na">service</span><span class="pi">:</span> <span class="s">S3</span>
  <span class="na">access_key_id</span><span class="pi">:</span> <span class="s">&lt;%= ENV['AWS_ACCESS_KEY_ID'] %&gt;</span>
  <span class="na">secret_access_key</span><span class="pi">:</span> <span class="s">&lt;%= ENV['AWS_SECRET_ACCESS_KEY'] %&gt;</span>
  <span class="na">region</span><span class="pi">:</span> <span class="s">us-east-1</span>
  <span class="na">bucket</span><span class="pi">:</span> <span class="s">my-app-uploads</span>
  <span class="c1"># Crucial for Direct Uploads!</span>
  <span class="na">public</span><span class="pi">:</span> <span class="no">true</span> 
</code></pre></div></div>

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

<h2 id="step-2-the-rails-form">STEP 2: The Rails Form</h2>

<p>Rails makes the backend part incredibly easy. You just add one attribute to your file field: <code class="language-plaintext highlighter-rouge">direct_upload: true</code>.</p>

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

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

<p>When you add <code class="language-plaintext highlighter-rouge">direct_upload: true</code>, Rails automatically includes a JavaScript library that handles the “handshake” with S3.</p>

<h2 id="step-3-adding-a-progress-bar-the-ux-win">STEP 3: Adding a Progress Bar (The UX Win)</h2>

<p>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.</p>

<p>First, add a small piece of HTML to your form:</p>

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

<p>Now, we create a tiny <strong>Stimulus</strong> controller to watch the upload progress.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/javascript/controllers/upload_controller.js</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/stimulus</span><span class="dl">"</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="kd">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
  <span class="nx">connect</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">direct-upload:progress</span><span class="dl">"</span><span class="p">,</span> <span class="nx">event</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="p">{</span> <span class="nx">progress</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">detail</span>
      <span class="kd">const</span> <span class="nx">bar</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">progress-fill</span><span class="dl">"</span><span class="p">)</span>
      <span class="kd">const</span> <span class="nx">container</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">progress-bar</span><span class="dl">"</span><span class="p">)</span>
      
      <span class="nx">container</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="dl">"</span><span class="s2">hidden</span><span class="dl">"</span><span class="p">)</span>
      <span class="nx">bar</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">progress</span><span class="p">}</span><span class="s2">%`</span>
    <span class="p">})</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">direct-upload:error</span><span class="dl">"</span><span class="p">,</span> <span class="nx">event</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">alert</span><span class="p">(</span><span class="dl">"</span><span class="s2">Upload failed! Please check your connection.</span><span class="dl">"</span><span class="p">)</span>
    <span class="p">})</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Attach this to your form: <code class="language-plaintext highlighter-rouge">&lt;%= form_with(model: user, data: { controller: "upload" }) do |f| %&gt;</code>.</p>

<h2 id="step-4-why-this-is-the-one-person-superpower">STEP 4: Why this is the “One-Person” Superpower</h2>

<p>As a solo developer, you want to avoid “Scaling Issues” as long as possible.</p>

<p>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.</p>

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

<h2 id="summary">Summary</h2>

<p>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.</p>

<ol>
  <li>Use <strong>Cloud Storage</strong> (S3/R2).</li>
  <li>Enable <strong>CORS</strong> on your bucket.</li>
  <li>Add <strong><code class="language-plaintext highlighter-rouge">direct_upload: true</code></strong> to your form.</li>
  <li>Add a <strong>Stimulus</strong> progress bar for that “premium” feel.</li>
</ol>

<p>Your Puma threads will thank you, and your users will love the snappy experience.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="storage" /><category term="performance" /><category term="webdev" /><summary type="html"><![CDATA[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 then your server sends it to S3 or Google Cloud.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-06-19-Pro-File-Uploads-In-Rails-8-Speed-And-Scalability-With-Direct-Uploads/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-06-19-Pro-File-Uploads-In-Rails-8-Speed-And-Scalability-With-Direct-Uploads/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Zero-JS Search: Building a Live-Filter with Turbo 8 Page Morphing</title><link href="https://norvilis.com/zero-js-search-building-a-live-filter-with-turbo-8-page-morphing/" rel="alternate" type="text/html" title="Zero-JS Search: Building a Live-Filter with Turbo 8 Page Morphing" /><published>2026-06-18T00:00:00+00:00</published><updated>2026-06-18T00:00:00+00:00</updated><id>https://norvilis.com/zero-js-search-building-a-live-filter-with-turbo-8-page-morphing</id><content type="html" xml:base="https://norvilis.com/zero-js-search-building-a-live-filter-with-turbo-8-page-morphing/"><![CDATA[<p>I’ve lost count of the number of hours I’ve spent in the past building “Live Search” features.</p>

<p>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 <code class="language-plaintext highlighter-rouge">Turbo Frames</code>, but that meant wrapping everything in specific tags and losing the “state” of the rest of the page.</p>

<p>In 2026, the game has changed. With <strong>Turbo 8 Page Morphing</strong>, 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.</p>

<p>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.</p>

<h2 id="step-1-enable-morphing">STEP 1: Enable Morphing</h2>

<p>First, we need to tell our application that we want to use the new Morphing behavior instead of the old “Replace” behavior.</p>

<p>Open your main application layout and add these two tags to the <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code>.</p>

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

<h2 id="step-2-the-search-form-and-controller">STEP 2: The Search Form and Controller</h2>

<p>We are going to use a standard HTML form. No <code class="language-plaintext highlighter-rouge">remote: true</code>, no complex setups. Just a simple <code class="language-plaintext highlighter-rouge">GET</code> request.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/products_controller.rb</span>
<span class="k">class</span> <span class="nc">ProductsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">index</span>
    <span class="vi">@products</span> <span class="o">=</span> <span class="no">Product</span><span class="p">.</span><span class="nf">all</span>
    
    <span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="ss">:query</span><span class="p">].</span><span class="nf">present?</span>
      <span class="vi">@products</span> <span class="o">=</span> <span class="vi">@products</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"name ILIKE ?"</span><span class="p">,</span> <span class="s2">"%</span><span class="si">#{</span><span class="n">params</span><span class="p">[</span><span class="ss">:query</span><span class="p">]</span><span class="si">}</span><span class="s2">%"</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>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 <strong>Stimulus</strong> controller that I keep in every project as a “utility” tool.</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/products/index.html.erb --&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"p-8"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;div</span> <span class="na">data-controller=</span><span class="s">"autosave"</span><span class="nt">&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">form_with</span> <span class="ss">url: </span><span class="n">products_path</span><span class="p">,</span> <span class="ss">method: :get</span><span class="p">,</span> 
                  <span class="ss">data: </span><span class="p">{</span> <span class="ss">autosave_target: </span><span class="s2">"form"</span><span class="p">,</span> <span class="ss">turbo_frame: </span><span class="s2">"_top"</span> <span class="p">}</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="cp">%&gt;</span>
      
      <span class="cp">&lt;%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:query</span><span class="p">,</span> 
            <span class="ss">placeholder: </span><span class="s2">"Search products..."</span><span class="p">,</span> 
            <span class="ss">value: </span><span class="n">params</span><span class="p">[</span><span class="ss">:query</span><span class="p">],</span>
            <span class="ss">data: </span><span class="p">{</span> <span class="ss">action: </span><span class="s2">"input-&gt;autosave#save"</span> <span class="p">},</span>
            <span class="ss">class: </span><span class="s2">"border-2 border-black p-2 w-full"</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;/div&gt;</span>

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

<h2 id="step-3-the-utility-controller-the-only-js-you-need">STEP 3: The Utility Controller (The only JS you need)</h2>

<p>This is the only JavaScript we need. It is a reusable “autosave” controller that simply submits the form whenever you type.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/javascript/controllers/autosave_controller.js</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/stimulus</span><span class="dl">"</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="kd">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
  <span class="kd">static</span> <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="dl">"</span><span class="s2">form</span><span class="dl">"</span><span class="p">]</span>

  <span class="nx">save</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// This triggers the standard Rails form submission</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">formTarget</span><span class="p">.</span><span class="nx">requestSubmit</span><span class="p">()</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="how-it-works-the-magic">How it works (The Magic)</h2>

<p>In the old days, calling <code class="language-plaintext highlighter-rouge">requestSubmit()</code> 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.</p>

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

<p>The user perceives a lightning-fast, reactive live filter, but you are just writing standard, boring Rails code.</p>

<h2 id="summary">Summary</h2>

<p>As a solo developer, your goal is to stay out of “JavaScript Land” as much as possible. By leveraging Turbo 8 Page Morphing:</p>

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

<p>This is the “One-Person Framework” at its best: high-end features with zero-maintenance code.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="hotwire" /><category term="search" /><category term="webdev" /><summary type="html"><![CDATA[I’ve lost count of the number of hours I’ve spent in the past building “Live Search” features.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-06-18-Zero-Js-Search-Building-A-Live-Filter-With-Turbo-8-Page-Morphing/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-06-18-Zero-Js-Search-Building-A-Live-Filter-With-Turbo-8-Page-Morphing/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Datastar in Rails 8: Real-Time UI Without the WebSocket Headache</title><link href="https://norvilis.com/datastar-in-rails-8-real-time-ui-without-the-websocket-headache/" rel="alternate" type="text/html" title="Datastar in Rails 8: Real-Time UI Without the WebSocket Headache" /><published>2026-06-17T00:00:00+00:00</published><updated>2026-06-17T00:00:00+00:00</updated><id>https://norvilis.com/datastar-in-rails-8-real-time-ui-without-the-websocket-headache</id><content type="html" xml:base="https://norvilis.com/datastar-in-rails-8-real-time-ui-without-the-websocket-headache/"><![CDATA[<p>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: <strong>Datastar</strong>.</p>

<p>If you think of <strong>HTMX</strong> as the “venerable grandfather” and <strong>Hotwire</strong> as the “official Omakase choice,” then <strong>Datastar</strong> is the “high-performance specialist.”</p>

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

<p>Here is how to get Datastar running in your Rails 8 app in 4 easy steps.</p>

<h2 id="the-concept-what-makes-datastar-different">The Concept: What makes Datastar different?</h2>

<p>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.</p>

<p>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 <strong>“Signals”</strong> (like Solid.js or Preact) to update only the tiny parts of the page that changed.</p>

<h2 id="step-1-installation-the-no-build-way">STEP 1: Installation (The No-Build Way)</h2>

<p>Since we are on Rails 8, we don’t want <code class="language-plaintext highlighter-rouge">node_modules</code>. We just pin the library using Importmaps.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bin/importmap pin @starfederation/datastar
</code></pre></div></div>

<p>Then, in your <code class="language-plaintext highlighter-rouge">app/javascript/application.js</code>, just import it to initialize the global listeners:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="dl">"</span><span class="s2">@starfederation/datastar</span><span class="dl">"</span>
</code></pre></div></div>

<h2 id="step-2-the-frontend-signal">STEP 2: The Frontend “Signal”</h2>

<p>Datastar uses attributes that look a lot like Alpine.js or Vue. Let’s build a simple counter that updates on the server.</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/counters/show.html.erb --&gt;</span>
<span class="nt">&lt;div</span> <span class="na">data-signals=</span><span class="s">"{count: 0}"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h2</span> <span class="na">data-text=</span><span class="s">"$count"</span><span class="nt">&gt;</span>0<span class="nt">&lt;/h2&gt;</span>
  
  <span class="nt">&lt;button</span> <span class="na">data-on-click=</span><span class="s">"@post('/increment')"</span><span class="nt">&gt;</span>
    Increment on Server
  <span class="nt">&lt;/button&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p><strong>What is happening here?</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">data-signals</code>: This is a tiny piece of client-side memory.</li>
  <li><code class="language-plaintext highlighter-rouge">data-text</code>: This binds the header text to that memory.</li>
  <li><code class="language-plaintext highlighter-rouge">data-on-click</code>: This sends a POST request to our Rails controller.</li>
</ul>

<h2 id="step-3-the-rails-controller-streaming-sse">STEP 3: The Rails Controller (Streaming SSE)</h2>

<p>This is where the magic happens. We aren’t returning a regular HTML page or a Turbo Stream. We are returning a <strong>Datastar SSE stream</strong>.</p>

<p>You’ll want to add a tiny helper or a gem like <code class="language-plaintext highlighter-rouge">datastar-rails</code> to handle the formatting, but here is what the raw response looks like:</p>

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

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

    <span class="c1"># 2. Set the correct header for SSE</span>
    <span class="n">response</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s1">'Content-Type'</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'text/event-stream'</span>
    
    <span class="c1"># 3. Send the Datastar "Fragment"</span>
    <span class="c1"># This tells Datastar to update the 'count' signal on the client</span>
    <span class="n">response</span><span class="p">.</span><span class="nf">stream</span><span class="p">.</span><span class="nf">write</span> <span class="s2">"event: datastar-merge-signals</span><span class="se">\n</span><span class="s2">data: signals {count: </span><span class="si">#{</span><span class="n">new_count</span><span class="si">}</span><span class="s2">}</span><span class="se">\n\n</span><span class="s2">"</span>
  <span class="k">ensure</span>
    <span class="n">response</span><span class="p">.</span><span class="nf">stream</span><span class="p">.</span><span class="nf">close</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="step-4-the-real-time-win">STEP 4: The Real-Time Win</h2>

<p>Because Datastar is built on SSE, you can keep the connection open and “push” updates as many times as you want.</p>

<p>Imagine a background job is processing an image. You can keep the <code class="language-plaintext highlighter-rouge">increment</code> method open and send:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">data: signals {status: 'Processing...'}</code></li>
  <li>Wait 2 seconds…</li>
  <li><code class="language-plaintext highlighter-rouge">data: signals {status: 'Done!', progress: 100}</code></li>
</ul>

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

<h2 id="why-try-this-over-hotwire">Why try this over Hotwire?</h2>

<ol>
  <li><strong>Fine-grained control:</strong> You can update individual variables (Signals) inside an HTML element without re-rendering the whole element.</li>
  <li><strong>No WebSocket Overhead:</strong> 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 <code class="language-plaintext highlighter-rouge">Solid Cable</code> helps with that!).</li>
  <li><strong>Client-side logic:</strong> You get a little bit of client-side state (<code class="language-plaintext highlighter-rouge">data-signals</code>) for things like toggling menus or form validation without needing to write a full Stimulus controller.</li>
</ol>

<h2 id="summary">Summary</h2>

<p>Datastar is perfect for the <strong>“Pragmatic Solo Developer”</strong> who wants real-time features but finds ActionCable or heavy JS frameworks a bit too much.</p>

<p>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.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="datastar" /><category term="hotwire" /><category term="webdev" /><summary type="html"><![CDATA[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: Datastar.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-06-17-Datastar-In-Rails-8-Real-Time-Ui-Without-The-Websocket-Headache/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-06-17-Datastar-In-Rails-8-Real-Time-Ui-Without-The-Websocket-Headache/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Go for Rubyists: A Practical Guide to Learning Golang</title><link href="https://norvilis.com/go-for-rubyists-a-practical-guide-to-learning-golang/" rel="alternate" type="text/html" title="Go for Rubyists: A Practical Guide to Learning Golang" /><published>2026-06-16T00:00:00+00:00</published><updated>2026-06-16T00:00:00+00:00</updated><id>https://norvilis.com/go-for-rubyists-a-practical-guide-to-learning-golang</id><content type="html" xml:base="https://norvilis.com/go-for-rubyists-a-practical-guide-to-learning-golang/"><![CDATA[<p>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.</p>

<p>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 <strong>Go</strong> (Golang) for high-performance side-services.</p>

<p>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.</p>

<h2 id="1-the-big-shift-no-more-magic">1. The Big Shift: No More Magic</h2>

<p>In Rails, we are used to “magic.” You type <code class="language-plaintext highlighter-rouge">User.find(1)</code> and it just works. You don’t see the SQL, you don’t see the imports, and you don’t worry about types.</p>

<p><strong>Go is the opposite.</strong> There is zero magic.</p>
<ul>
  <li>If you use a variable, you must define its type.</li>
  <li>If you import a library and don’t use it, the code won’t even compile.</li>
  <li>Everything is explicit.</li>
</ul>

<p>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.</p>

<h2 id="2-syntax-translation">2. Syntax Translation</h2>

<p>Let’s look at a simple function comparison.</p>

<p><strong>Ruby:</strong></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">greet</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>
  <span class="s2">"Hello, </span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">!"</span>
<span class="k">end</span>

<span class="nb">puts</span> <span class="n">greet</span><span class="p">(</span><span class="s2">"Zil"</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Go:</strong></p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="s">"fmt"</span>

<span class="c">// We must say name is a string, and the function returns a string</span>
<span class="k">func</span> <span class="n">greet</span><span class="p">(</span><span class="n">name</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
	<span class="k">return</span> <span class="s">"Hello, "</span> <span class="o">+</span> <span class="n">name</span> <span class="o">+</span> <span class="s">"!"</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">greet</span><span class="p">(</span><span class="s">"Zil"</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Notice the <code class="language-plaintext highlighter-rouge">package main</code> and <code class="language-plaintext highlighter-rouge">import</code>. Every Go file needs these. Also, notice the curly braces <code class="language-plaintext highlighter-rouge">{}</code>. As a Rubyist, you’ll miss <code class="language-plaintext highlighter-rouge">do...end</code> at first, but you’ll get used to it.</p>

<h2 id="3-dealing-with-errors-the-if-err--nil-loop">3. Dealing with Errors (The “if err != nil” loop)</h2>

<p>In Ruby, we use <code class="language-plaintext highlighter-rouge">begin...rescue</code> to handle errors. Or we just let the app crash and check the logs.</p>

<p>In Go, errors are not “exceptions.” They are just values that functions return. You will see this pattern in every single Go project:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">user</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">findUser</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>

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

<span class="c">// If no error, continue</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">Name</span><span class="p">)</span>
</code></pre></div></div>

<p>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.</p>

<h2 id="4-the-killer-feature-goroutines">4. The Killer Feature: Goroutines</h2>

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

<p>In Go, you have <strong>Goroutines</strong>. You just put the word <code class="language-plaintext highlighter-rouge">go</code> in front of a function call, and it runs in the background on a different CPU thread instantly.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">sendEmail</span><span class="p">(</span><span class="n">email</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="c">// slow logic</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="c">// This runs in the background. No Redis needed!</span>
    <span class="k">go</span> <span class="n">sendEmail</span><span class="p">(</span><span class="s">"test@example.com"</span><span class="p">)</span> 
    
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Moving on..."</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It is incredibly lightweight. You can run 100,000 goroutines on a cheap $5 laptop.</p>

<h2 id="5-deployment-the-single-binary">5. Deployment: The Single Binary</h2>

<p>As someone who loves <strong>Kamal</strong> and Docker, Go is a dream for deployment.</p>

<p>When you build a Go app, it compiles into <strong>one single file</strong> (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.</p>

<h2 id="summary-should-you-switch">Summary: Should you switch?</h2>

<p>I don’t recommend replacing Rails with Go for your main web app. Rails is still much faster for building UIs and CRUD logic.</p>

<p>Instead, <strong>use Go as a tool in your belt.</strong></p>
<ul>
  <li>Use Rails for the main app.</li>
  <li>Use Go for that one specific microservice that needs to be blazing fast or handle massive amounts of data.</li>
</ul>

<p>Learning Go will make you a more disciplined developer. It teaches you to think about memory, types, and how the computer actually works.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="go" /><category term="ruby" /><category term="learning" /><category term="webdev" /><summary type="html"><![CDATA[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.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-06-16-Go-For-Rubyists-A-Practical-Guide-To-Learning-Golang/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-06-16-Go-For-Rubyists-A-Practical-Guide-To-Learning-Golang/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Is Coding Over? 5 Career Pivots for Rails Devs in the AI Era</title><link href="https://norvilis.com/is-coding-over-5-career-pivots-for-rails-devs-in-the-ai-era/" rel="alternate" type="text/html" title="Is Coding Over? 5 Career Pivots for Rails Devs in the AI Era" /><published>2026-06-11T00:00:00+00:00</published><updated>2026-06-11T00:00:00+00:00</updated><id>https://norvilis.com/is-coding-over-5-career-pivots-for-rails-devs-in-the-ai-era</id><content type="html" xml:base="https://norvilis.com/is-coding-over-5-career-pivots-for-rails-devs-in-the-ai-era/"><![CDATA[<p>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.</p>

<p>I don’t think coding is dead, but I do think the <strong>Job of the Coder</strong> is changing.</p>

<p>The era of being paid $100k a year just to know the syntax of <code class="language-plaintext highlighter-rouge">has_many :through</code> is coming to an end. AI can write syntax better and faster than you. But Rails developers have a unique advantage: we are usually <strong>Product Engineers</strong>. We know how to build entire systems, not just small functions.</p>

<p>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.</p>

<h2 id="1-the-ai-orchestrator-ai-engineer">1. The AI Orchestrator (AI Engineer)</h2>

<p>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 <strong>Agentic Workflows</strong>.</p>

<p>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.</p>

<p><strong>Why it’s safe:</strong> 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.</p>

<h2 id="2-the-solution-architect">2. The Solution Architect</h2>

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

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

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

<h2 id="3-the-technical-product-manager-pm">3. The Technical Product Manager (PM)</h2>

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

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

<p><strong>Why it’s safe:</strong> 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.</p>

<h2 id="4-the-solopreneur--indie-hacker">4. The Solopreneur / Indie Hacker</h2>

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

<p>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.</p>

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

<h2 id="5-cybersecurity-and-compliance">5. Cybersecurity and Compliance</h2>

<p>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.</p>

<p>Moving into security - specifically specialized Rails security or GDPR/Data Privacy compliance - is a very high-paying niche.</p>

<p><strong>Why it’s safe:</strong> 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.</p>

<h2 id="summary-dont-panic-adapt">Summary: Don’t Panic, Adapt</h2>

<p>If you are a Rails developer today, you are in a great position. You already think in “Systems.”</p>

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

<p>The “Junior Developer” role is the one most at risk. If you can move up the chain and become the <strong>Editor-in-Chief</strong> or the <strong>Product Owner</strong>, AI won’t replace you - it will make you rich.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="career" /><category term="ai" /><category term="rails" /><category term="ruby" /><summary type="html"><![CDATA[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.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-06-11-Is-Coding-Over-5-Career-Pivots-For-Rails-Devs-In-The-Ai-Era/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-06-11-Is-Coding-Over-5-Career-Pivots-For-Rails-Devs-In-The-Ai-Era/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Demystifying Rails Magic: How Autoloading Actually Works</title><link href="https://norvilis.com/demystifying-rails-magic-how-autoloading-actually-works/" rel="alternate" type="text/html" title="Demystifying Rails Magic: How Autoloading Actually Works" /><published>2026-06-10T00:00:00+00:00</published><updated>2026-06-10T00:00:00+00:00</updated><id>https://norvilis.com/demystifying-rails-magic-how-autoloading-actually-works</id><content type="html" xml:base="https://norvilis.com/demystifying-rails-magic-how-autoloading-actually-works/"><![CDATA[<p>I remember when I first started learning Ruby outside of Rails. I was building a small script, and my code kept crashing with <code class="language-plaintext highlighter-rouge">NameError: uninitialized constant</code>. I was so confused because I knew the file existed. I realized that in plain Ruby, you have to manually add <code class="language-plaintext highlighter-rouge">require './user'</code> at the top of every single file.</p>

<p>In Rails, you never do this. You just type <code class="language-plaintext highlighter-rouge">User.first</code> or <code class="language-plaintext highlighter-rouge">OrderService.new</code> and it “just works.” People often call this “Rails Magic,” but it isn’t magic at all. It is a very structured system called <strong>Zeitwerk</strong>.</p>

<p>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.</p>

<h2 id="level-1-the-convention-snake-case-to-camelcase">Level 1: The Convention (Snake Case to CamelCase)</h2>

<p>The core of the magic is a very strict naming rule. Rails assumes that your file names match your class names perfectly.</p>

<ul>
  <li>If your class is <code class="language-plaintext highlighter-rouge">User</code>, the file must be <code class="language-plaintext highlighter-rouge">user.rb</code>.</li>
  <li>If your class is <code class="language-plaintext highlighter-rouge">UserCredential</code>, the file must be <code class="language-plaintext highlighter-rouge">user_credential.rb</code>.</li>
  <li>If your class is inside a folder like <code class="language-plaintext highlighter-rouge">app/services/billing/payout_manager.rb</code>, the class name must be <code class="language-plaintext highlighter-rouge">Billing::PayoutManager</code>.</li>
</ul>

<p>If you break this rule - for example, naming your file <code class="language-plaintext highlighter-rouge">payouts.rb</code> but naming the class <code class="language-plaintext highlighter-rouge">PayoutManager</code> - Rails will crash. It uses these names to find the files on your hard drive.</p>

<h2 id="level-2-the-engine-zeitwerk">Level 2: The Engine (Zeitwerk)</h2>

<p>Since Rails 6, the engine that handles this is called <strong>Zeitwerk</strong>.</p>

<p>When your Rails app starts up, Zeitwerk scans all the folders inside <code class="language-plaintext highlighter-rouge">app/</code> (models, controllers, jobs, etc.). It builds a map of all the files.</p>

<p>When you type <code class="language-plaintext highlighter-rouge">User</code> in your code, Ruby looks at its own memory and says: <em>“I don’t know what ‘User’ is.”</em> Normally, this would trigger an error. But Zeitwerk intercepts that error and says: <em>“Wait! I know where that is. Based on the name ‘User’, it should be in <code class="language-plaintext highlighter-rouge">app/models/user.rb</code>.”</em></p>

<p>Zeitwerk then runs a hidden <code class="language-plaintext highlighter-rouge">require</code> for that file, loads the class into memory, and your code continues running as if the class was always there.</p>

<h2 id="level-3-why-this-is-different-from-elixir">Level 3: Why This is Different from Elixir</h2>

<p>In <strong>Elixir</strong>, 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.</p>

<p>In <strong>Rails</strong>, 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: <strong>Reloading</strong>.</p>

<h2 id="level-4-hot-reloading-in-development">Level 4: Hot Reloading in Development</h2>

<p>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.”</p>

<p>In <code class="language-plaintext highlighter-rouge">development</code> mode, Zeitwerk watches your files. When you save <code class="language-plaintext highlighter-rouge">user.rb</code>, Zeitwerk wipes the <code class="language-plaintext highlighter-rouge">User</code> class from Ruby’s memory. The next time a request comes in, Zeitwerk sees that <code class="language-plaintext highlighter-rouge">User</code> is missing, finds the updated file, and loads it again.</p>

<p>This is why you can edit a Rails app and see the changes instantly in the browser.</p>

<h2 id="level-5-when-the-magic-fails">Level 5: When the Magic Fails</h2>

<p>Even the best magic has limits. You will usually run into “Autoloading Errors” in two cases:</p>

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

<h2 id="summary">Summary</h2>

<p>Rails “magic” is just a very smart agreement between you and the framework.</p>

<ol>
  <li><strong>Strict Naming:</strong> Name your files <code class="language-plaintext highlighter-rouge">snake_case</code> and your classes <code class="language-plaintext highlighter-rouge">CamelCase</code>.</li>
  <li><strong>Zeitwerk:</strong> The engine that handles the loading so you don’t have to write <code class="language-plaintext highlighter-rouge">require</code>.</li>
  <li><strong>Efficiency:</strong> Only the code you actually use gets loaded into memory.</li>
</ol>

<p>Once you respect the naming conventions, the magic stays out of your way and lets you focus on building features.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="ruby" /><category term="zeitwerk" /><category term="learning" /><summary type="html"><![CDATA[I remember when I first started learning Ruby outside of Rails. I was building a small script, and my code kept crashing with NameError: uninitialized constant. I was so confused because I knew the file existed. I realized that in plain Ruby, you have to manually add require './user' at the top of every single file.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-06-10-Demystifying-Rails-Magic-How-Autoloading-Actually-Works/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-06-10-Demystifying-Rails-Magic-How-Autoloading-Actually-Works/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Stripe-Style IDs in Rails: A Guide to the custom_id Gem</title><link href="https://norvilis.com/stripe-style-ids-in-rails-a-guide-to-the-make-id-gem/" rel="alternate" type="text/html" title="Stripe-Style IDs in Rails: A Guide to the custom_id Gem" /><published>2026-06-09T00:00:00+00:00</published><updated>2026-06-09T00:00:00+00:00</updated><id>https://norvilis.com/stripe-style-ids-in-rails-a-guide-to-the-make-id-gem</id><content type="html" xml:base="https://norvilis.com/stripe-style-ids-in-rails-a-guide-to-the-make-id-gem/"><![CDATA[<p>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: <code class="language-plaintext highlighter-rouge">cus_M1p7abc123</code> or <code class="language-plaintext highlighter-rouge">ch_3Oixyz456</code>.</p>

<p>These are called <strong>Prefixed IDs</strong>. They are great for two reasons:</p>
<ol>
  <li><strong>Context:</strong> When you see <code class="language-plaintext highlighter-rouge">inv_...</code> in your logs, you immediately know it’s an Invoice without looking at the table name.</li>
  <li><strong>Security:</strong> It hides your business volume. Hackers can’t guess that your next user is ID 501.</li>
</ol>

<p>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: <strong><a href="https://github.com/pniemczyk/custom_id">custom_id</a></strong>.</p>

<p>Here is how to implement professional, Stripe-style IDs in your Rails 8 app in 4 steps.</p>

<h2 id="step-1-install-the-gem">STEP 1: Install the Gem</h2>

<p>Add the gem to your Gemfile:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s2">"custom_id"</span>
</code></pre></div></div>
<p>Run  in your terminal</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle install
rails custom_id:install
</code></pre></div></div>

<h2 id="step-2-the-migration">STEP 2: The Migration</h2>

<p>Just like with ULIDs or Snowflake IDs, we need to tell the database that our primary key is a <code class="language-plaintext highlighter-rouge">string</code> and not an <code class="language-plaintext highlighter-rouge">integer</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g model Project name:string
</code></pre></div></div>

<p>Open the migration file and disable the automatic ID:</p>

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

<h2 id="step-3-configure-the-model">STEP 3: Configure the Model</h2>

<p>This is where the gem shines. You just use the <code class="language-plaintext highlighter-rouge">custom_id</code> macro. You tell it what prefix you want to use, and it handles the rest.</p>

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

<p>By default, the gem uses a collision-resistant loop with database uniqueness check.</p>

<h2 id="step-4-seeing-it-in-action">STEP 4: Seeing it in Action</h2>

<p>Open your Rails console (<code class="language-plaintext highlighter-rouge">bin/rails c</code>) and create a project:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">project</span> <span class="o">=</span> <span class="no">Project</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Cloud Empire"</span><span class="p">)</span>

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

<p>It looks professional, it’s easy to read in your logs, and it’s perfectly unique.</p>

<h2 id="advanced-customizing-your-ids">Advanced: Customizing your IDs</h2>

<p>The <code class="language-plaintext highlighter-rouge">custom_id</code> 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.</p>

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

<h2 id="why-i-prefer-this-over-uuids">Why I prefer this over UUIDs?</h2>

<p>I love UUIDs for their uniqueness, but I hate how they look in URLs. 
<code class="language-plaintext highlighter-rouge">/users/550e8400-e29b-41d4-a716-446655440000</code> is ugly and hard to double-click and copy.</p>

<p><code class="language-plaintext highlighter-rouge">/users/usr_7k2n9zp1m5r8</code> is clean, tells me exactly what the object is, and fits perfectly in a mobile app UI or a support email.</p>

<h2 id="summary">Summary</h2>

<p>The <code class="language-plaintext highlighter-rouge">custom_id</code> 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.</p>

<ol>
  <li><strong>Install</strong> the gem.</li>
  <li>Set your primary key to <strong>string</strong>.</li>
  <li>Add <strong><code class="language-plaintext highlighter-rouge">cid 'prefix'</code></strong> to your model.</li>
</ol>

<p>It’s a tiny change that makes your Rails monolith feel significantly more polished and “pro.”</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="ruby" /><category term="security" /><category term="database" /><summary type="html"><![CDATA[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: cus_M1p7abc123 or ch_3Oixyz456.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-06-09-Stripe-Style-Ids-In-Rails-A-Guide-To-The-Make-Id-Gem/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-06-09-Stripe-Style-Ids-In-Rails-A-Guide-To-The-Make-Id-Gem/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>