<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Sulman Baig]]></title><description><![CDATA[Senior Software Engineer with 11 years of expertise in Ruby on Rails and Vue.js, specializing in health, e-commerce, staffing, and transport. Experienced in software development and version analysis.]]></description><link>https://sulmanweb.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1737208354560/3940cfdb-0818-4a1b-a146-801b33cbed9b.png</url><title>Sulman Baig</title><link>https://sulmanweb.com</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 19 Apr 2026 04:53:20 GMT</lastBuildDate><atom:link href="https://sulmanweb.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Master Shopify Embedded Apps with Rails 8]]></title><description><![CDATA[Motivation
After building several Shopify embedded apps, I've learned valuable lessons about what works (and what definitely doesn't) in the embedded app environment. Today, I'm sharing these insights to help you avoid common pitfalls and build bette...]]></description><link>https://sulmanweb.com/shopify-embedded-apps-rails-guide-best-practices</link><guid isPermaLink="true">https://sulmanweb.com/shopify-embedded-apps-rails-guide-best-practices</guid><category><![CDATA[shopify]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Thu, 06 Feb 2025 05:00:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738296849363/d6d35276-d206-49c8-9554-8e5a8eddc2ca.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-motivation">Motivation</h2>
<p>After building several Shopify embedded apps, I've learned valuable lessons about what works (and what definitely doesn't) in the embedded app environment. Today, I'm sharing these insights to help you avoid common pitfalls and build better Shopify apps.</p>
<h2 id="heading-the-challenge">The Challenge</h2>
<p>Building embedded apps for Shopify presents unique challenges - particularly around navigation, authentication, and user experience. One of the biggest pitfalls? Trying to use regular URL navigation in an embedded app context.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>First, let's look at the proper configuration for a Shopify embedded app:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># config/initializers/shopify_app.rb</span>
ShopifyApp.configure <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.embedded_app = <span class="hljs-literal">true</span>
  config.new_embedded_auth_strategy = <span class="hljs-literal">false</span>
  config.scope = <span class="hljs-string">"read_customers,write_customers"</span>
  config.api_version = <span class="hljs-string">"2024-10"</span>

  <span class="hljs-comment"># Webhook configuration</span>
  config.webhooks = [
    { <span class="hljs-symbol">topic:</span> <span class="hljs-string">"app/uninstalled"</span>, <span class="hljs-symbol">address:</span> <span class="hljs-string">"webhooks/app_uninstalled"</span> },
    { <span class="hljs-symbol">topic:</span> <span class="hljs-string">"customers/create"</span>, <span class="hljs-symbol">address:</span> <span class="hljs-string">"webhooks/customers_create"</span> }
  ]
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-dos-and-donts">Do's and Don'ts</h2>
<h3 id="heading-do-use-app-bridge-for-navigation">✅ DO: Use App Bridge for Navigation</h3>
<pre><code class="lang-javascript"><span class="hljs-comment">// app/javascript/shopify_app.js</span>
<span class="hljs-keyword">var</span> AppBridge = <span class="hljs-built_in">window</span>[<span class="hljs-string">'app-bridge'</span>];
<span class="hljs-keyword">var</span> createApp = AppBridge.default;
<span class="hljs-built_in">window</span>.app = createApp({
  <span class="hljs-attr">apiKey</span>: data.apiKey,
  <span class="hljs-attr">host</span>: data.host,
});

<span class="hljs-keyword">var</span> actions = AppBridge.actions;
<span class="hljs-keyword">var</span> TitleBar = actions.TitleBar;
TitleBar.create(app, {
  <span class="hljs-attr">title</span>: data.page,
});
</code></pre>
<h3 id="heading-dont-use-regular-rails-links">❌ DON'T: Use Regular Rails Links</h3>
<p>Avoid using regular Rails link_to helpers without proper App Bridge handling:</p>
<pre><code class="lang-erb"><span class="hljs-comment">&lt;%# Wrong way %&gt;</span><span class="xml">
<span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">"Settings"</span>, settings_path </span><span class="xml"><span class="hljs-tag">%&gt;</span>

</span><span class="hljs-comment">&lt;%# Right way %&gt;</span><span class="xml">
<span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">"Settings"</span>, settings_path(request.query_parameters) </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<h3 id="heading-do-handle-authentication-properly">✅ DO: Handle Authentication Properly</h3>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthenticatedController</span> &lt; ApplicationController</span>
  <span class="hljs-keyword">include</span> ShopifyApp::EnsureHasSession

  before_action <span class="hljs-symbol">:verify_embedded_app_request</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">verify_embedded_app_request</span></span>
    <span class="hljs-keyword">if</span> ShopifyAPI::Context.embedded? &amp;&amp; 
       (!params[<span class="hljs-symbol">:embedded</span>].present? <span class="hljs-params">||</span> params[<span class="hljs-symbol">:embedded</span>] != <span class="hljs-string">"1"</span>)
      redirect_to(ShopifyAPI::Auth.embedded_app_url(params[<span class="hljs-symbol">:host</span>]).to_s + 
                 request.path, 
                 <span class="hljs-symbol">allow_other_host:</span> <span class="hljs-literal">true</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-do-implement-proper-flash-messages">✅ DO: Implement Proper Flash Messages</h3>
<pre><code class="lang-ruby"><span class="hljs-comment"># app/helpers/application_helper.rb</span>
<span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">ApplicationHelper</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">flash_class</span><span class="hljs-params">(type)</span></span>
    <span class="hljs-keyword">case</span> type.to_sym
    <span class="hljs-keyword">when</span> <span class="hljs-symbol">:success</span>, <span class="hljs-symbol">:notice</span>
      <span class="hljs-string">"bg-green-50"</span>
    <span class="hljs-keyword">when</span> <span class="hljs-symbol">:error</span>, <span class="hljs-symbol">:alert</span>
      <span class="hljs-string">"bg-red-50"</span>
    <span class="hljs-keyword">else</span>
      <span class="hljs-string">"bg-blue-50"</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-do-use-proper-layout-for-embedded-apps">✅ DO: Use Proper Layout for Embedded Apps</h3>
<pre><code class="lang-erb"><span class="hljs-comment">&lt;%# app/views/layouts/embedded_app.html.erb %&gt;</span><span class="xml">
<span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://unpkg.com/@shopify/app-bridge@3.7.9"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> csrf_meta_tags </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> javascript_importmap_tags </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"app"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-string">'shared/navbar'</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> <span class="hljs-keyword">yield</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">if</span> flash[<span class="hljs-symbol">:notice</span>].present? <span class="hljs-params">||</span> flash[<span class="hljs-symbol">:error</span>].present? </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
        <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'DOMContentLoaded'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
          <span class="hljs-keyword">var</span> Toast = <span class="hljs-built_in">window</span>[<span class="hljs-string">'app-bridge'</span>].actions.Toast;

          &lt;%</span></span><span class="ruby"> <span class="hljs-keyword">if</span> flash[<span class="hljs-symbol">:notice</span>].present? </span><span class="xml"><span class="javascript">%&gt;
            Toast.create(<span class="hljs-built_in">window</span>.app, {
              <span class="hljs-attr">message</span>: <span class="hljs-string">"&lt;%=</span></span></span><span class="ruby"> j flash[<span class="hljs-symbol">:notice</span>] </span><span class="xml">%&gt;",
              duration: 5000
            }).dispatch(Toast.Action.SHOW);
          &lt;%</span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml">%&gt;
        });
      <span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></span>
</code></pre>
<h3 id="heading-dont-forget-query-parameters">❌ DON'T: Forget Query Parameters</h3>
<p>When creating links or forms, always include the necessary query parameters:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># app/controllers/application_controller.rb</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">maintain_query_parameters</span></span>
  @query_parameters = request.query_parameters
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-do-implement-proper-navigation">✅ DO: Implement Proper Navigation</h3>
<pre><code class="lang-erb"><span class="hljs-comment">&lt;%# app/views/shared/_navbar.html.erb %&gt;</span><span class="xml">
<span class="hljs-tag">&lt;<span class="hljs-name">nav</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"bg-white border-b border-gray-200 fixed w-full z-50"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex justify-between h-16"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hidden md:flex md:items-center md:space-x-6"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">"Home"</span>, 
            root_path(params.except(<span class="hljs-symbol">:action</span>, <span class="hljs-symbol">:controller</span>).permit!.to_h), 
            <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-600 <span class="hljs-title">hover</span>:<span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900 <span class="hljs-title">px</span>-3 <span class="hljs-title">py</span>-2" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span></span>
</code></pre>
<h2 id="heading-common-pitfalls-to-avoid">Common Pitfalls to Avoid</h2>
<ol>
<li><strong>Direct URL Manipulation</strong>
```ruby<h1 id="heading-wrong">Wrong</h1>
redirect_to settings_path</li>
</ol>
<h1 id="heading-right">Right</h1>
<p>redirect_to settings_path(request.query_parameters)</p>
<pre><code>
<span class="hljs-number">2.</span> **Missing CSRF Protection**
<span class="hljs-string">``</span><span class="hljs-string">`ruby
class ApplicationController &lt; ActionController::Base
  protect_from_forgery with: :exception
  skip_before_action :verify_authenticity_token, if: :valid_shopify_request?

  private

  def valid_shopify_request?
    return true if request.headers["HTTP_AUTHORIZATION"]&amp;.start_with?("Bearer ")
    return true if params[:session].present?
    false
  end
end</span>
</code></pre><ol start="3">
<li><p><strong>Improper Session Handling</strong></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Shop</span> &lt; ActiveRecord::Base</span>
<span class="hljs-keyword">include</span> ShopifyApp::ShopSessionStorageWithScopes

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">api_version</span></span>
 ShopifyApp.configuration.api_version
<span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
</li>
</ol>
<h2 id="heading-best-practices-for-success">Best Practices for Success</h2>
<ol>
<li><strong>Use App Bridge for All Navigation</strong></li>
<li><strong>Maintain Query Parameters</strong></li>
<li><strong>Implement Proper Error Handling</strong></li>
<li><strong>Use Appropriate Authentication Checks</strong></li>
<li><strong>Follow Shopify's Design Guidelines</strong></li>
</ol>
<h2 id="heading-learning-journey">Learning Journey</h2>
<p>Building embedded Shopify apps has taught me the importance of proper navigation handling and session management. The biggest lesson? Never assume standard web development practices will work in an embedded context.</p>
<h2 id="heading-results-and-impact">Results and Impact</h2>
<ul>
<li>Smoother user experience</li>
<li>Fewer authentication issues</li>
<li>Better app stability</li>
<li>Reduced support tickets</li>
</ul>
<h2 id="heading-next-steps">Next Steps</h2>
<ol>
<li><p><strong>Enhanced Error Handling</strong></p>
<ul>
<li>Implement better error boundaries</li>
<li>Add detailed logging</li>
</ul>
</li>
<li><p><strong>Performance Optimization</strong></p>
<ul>
<li>Optimize App Bridge usage</li>
<li>Implement proper caching</li>
</ul>
</li>
<li><p><strong>User Experience Improvements</strong></p>
<ul>
<li>Add loading states</li>
<li>Enhance feedback mechanisms</li>
</ul>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building embedded Shopify apps requires a different mindset from traditional web development. By following these best practices and avoiding common pitfalls, you can create robust, user-friendly applications that integrate seamlessly with the Shopify admin.</p>
<p>Have you encountered other challenges while building Shopify embedded apps? Share your experiences in the comments below!</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Build Secure Shopify Apps with App Bridge]]></title><description><![CDATA[As a developer working with Shopify's ecosystem, I recently built a multi-tenant SaaS application that synchronizes customer data between Shopify stores and external services. In this article, I'll share my experience and technical insights into crea...]]></description><link>https://sulmanweb.com/building-secure-shopify-apps-with-app-bridge</link><guid isPermaLink="true">https://sulmanweb.com/building-secure-shopify-apps-with-app-bridge</guid><category><![CDATA[shopify]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Mon, 03 Feb 2025 05:00:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738296962335/c0f70db0-28ef-4064-8f4f-66e6202f2403.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a developer working with Shopify's ecosystem, I recently built a multi-tenant SaaS application that synchronizes customer data between Shopify stores and external services. In this article, I'll share my experience and technical insights into creating a secure, scalable Shopify app using App Bridge.</p>
<h2 id="heading-project-overview">Project Overview</h2>
<p>Our application needed to:</p>
<ul>
<li>Handle multiple Shopify stores (multi-tenancy)</li>
<li>Process customer data securely</li>
<li>Provide a seamless embedded experience</li>
<li>Manage OAuth flows and webhooks</li>
<li>Handle billing subscriptions</li>
</ul>
<p>Let's dive into how we accomplished these requirements using Rails 8 and Shopify's App Bridge.</p>
<h2 id="heading-setting-up-the-foundation">Setting Up the Foundation</h2>
<p>First, we set up our Rails application with the necessary Shopify integrations. Here's how our initial configuration looked:</p>
<pre><code class="lang-ruby">ShopifyApp.configure <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.embedded_app = <span class="hljs-literal">true</span>
  config.scope = <span class="hljs-string">"read_customers,write_customers"</span>
  config.after_authenticate_job = <span class="hljs-literal">false</span>
  config.api_version = <span class="hljs-string">"2024-10"</span>
  config.shop_session_repository = <span class="hljs-string">"Shop"</span>
  config.webhooks = [
    { <span class="hljs-symbol">topic:</span> <span class="hljs-string">"app/uninstalled"</span>, <span class="hljs-symbol">address:</span> <span class="hljs-string">"webhooks/app_uninstalled"</span> },
    { <span class="hljs-symbol">topic:</span> <span class="hljs-string">"customers/create"</span>, <span class="hljs-symbol">address:</span> <span class="hljs-string">"webhooks/customers_create"</span> },
    { <span class="hljs-symbol">topic:</span> <span class="hljs-string">"customers/update"</span>, <span class="hljs-symbol">address:</span> <span class="hljs-string">"webhooks/customers_update"</span> }
  ]
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-multi-tenant-data-model">Multi-tenant Data Model</h2>
<p>Our core data model revolves around the <code>Shop</code> model, which handles multi-tenancy:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Shop</span> &lt; ActiveRecord::Base</span>
  <span class="hljs-keyword">include</span> ShopifyApp::ShopSessionStorageWithScopes

  has_many <span class="hljs-symbol">:customers</span>, <span class="hljs-symbol">dependent:</span> <span class="hljs-symbol">:destroy</span>

  encrypts <span class="hljs-symbol">:api_key</span>, <span class="hljs-symbol">deterministic:</span> <span class="hljs-literal">true</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-embedded-app-architecture">Embedded App Architecture</h2>
<p>One of the key aspects was creating a seamless embedded experience. We achieved this through our layout configuration:</p>
<pre><code class="lang-erb"><span class="hljs-comment">&lt;%# app/views/layouts/embedded_app.html.erb %&gt;</span><span class="xml">
<span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://unpkg.com/@shopify/app-bridge@3.7.9"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
      <span class="hljs-keyword">var</span> AppBridge = <span class="hljs-built_in">window</span>[<span class="hljs-string">'app-bridge'</span>];
      <span class="hljs-keyword">var</span> createApp = AppBridge.default;
      <span class="hljs-keyword">var</span> app = createApp({
        <span class="hljs-attr">apiKey</span>: <span class="hljs-string">"&lt;%=</span></span></span><span class="ruby"> ShopifyApp.configuration.api_key </span><span class="xml">%&gt;",
        host: "&lt;%=</span><span class="ruby"> @host </span><span class="xml">%&gt;",
        forceRedirect: true
      });
    <span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> <span class="hljs-keyword">yield</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></span>
</code></pre>
<h2 id="heading-secure-authentication-flow">Secure Authentication Flow</h2>
<p>We implemented authenticated controllers to ensure secure access:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthenticatedController</span> &lt; ApplicationController</span>
  <span class="hljs-keyword">include</span> ShopifyApp::EnsureHasSession

  before_action <span class="hljs-symbol">:ensure_store_settings</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">ensure_store_settings</span></span>
    redirect_to settings_path <span class="hljs-keyword">unless</span> current_shop.setup_completed?
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-webhook-processing">Webhook Processing</h2>
<p>Handling webhooks securely was crucial for our app. Here's our webhook processing implementation:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebhooksController</span> &lt; ApplicationController</span>
  <span class="hljs-keyword">include</span> ShopifyApp::WebhookVerification

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">customers_create</span></span>
    shop = Shop.find_by(<span class="hljs-symbol">shopify_domain:</span> params[<span class="hljs-symbol">:shop_domain</span>])

    <span class="hljs-keyword">if</span> shop
      shop.with_shopify_session <span class="hljs-keyword">do</span>
        process_customer_data(params)
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>

    head <span class="hljs-symbol">:ok</span>
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process_customer_data</span><span class="hljs-params">(data)</span></span>
    CustomerProcessingJob.perform_later(
      <span class="hljs-symbol">shop_id:</span> shop.id,
      <span class="hljs-symbol">customer_data:</span> data
    )
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-background-job-processing">Background Job Processing</h2>
<p>We used Solid Queue for reliable background processing:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomerProcessingJob</span> &lt; ApplicationJob</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(<span class="hljs-symbol">shop_id:</span>, <span class="hljs-symbol">customer_data:</span>)</span></span>
    shop = Shop.find(shop_id)

    shop.with_shopify_session <span class="hljs-keyword">do</span>
      customer = shop.customers.find_or_initialize_by(
        <span class="hljs-symbol">shopify_customer_id:</span> customer_data[<span class="hljs-string">"id"</span>]
      )

      customer.update!(
        <span class="hljs-symbol">email:</span> customer_data[<span class="hljs-string">"email"</span>],
        <span class="hljs-symbol">first_name:</span> customer_data[<span class="hljs-string">"first_name"</span>],
        <span class="hljs-symbol">last_name:</span> customer_data[<span class="hljs-string">"last_name"</span>]
      )
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-billing-integration">Billing Integration</h2>
<p>We implemented Shopify's billing API to handle subscriptions:</p>
<pre><code class="lang-ruby">config.billing = ShopifyApp::BillingConfiguration.new(
  <span class="hljs-symbol">charge_name:</span> <span class="hljs-string">"App Subscription"</span>,
  <span class="hljs-symbol">amount:</span> <span class="hljs-number">4.99</span>,
  <span class="hljs-symbol">interval:</span> ShopifyApp::BillingConfiguration::INTERVAL_EVERY_30_DAYS,
  <span class="hljs-symbol">trial_days:</span> <span class="hljs-number">7</span>
)
</code></pre>
<h2 id="heading-user-interface-with-tailwind-css">User Interface with Tailwind CSS</h2>
<p>We created a clean, responsive interface using Tailwind CSS:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-12 pt-20"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sm:flex sm:items-center"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sm:flex-auto"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-base font-semibold leading-6 text-gray-900"</span>&gt;</span>
        Customers
      <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-2 text-sm text-gray-700"</span>&gt;</span>
        Manage your synchronized customers
      <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-string">"customer_list"</span>, <span class="hljs-symbol">customers:</span> @customers </span><span class="xml"><span class="hljs-tag">%&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
<h2 id="heading-lessons-learned">Lessons Learned</h2>
<p>Throughout this project, I learned several valuable lessons:</p>
<ol>
<li><p><strong>Session Management</strong>: Always use Shopify's session tokens for authentication rather than storing raw access tokens.</p>
</li>
<li><p><strong>Webhook Reliability</strong>: Implement idempotency in webhook processing to handle potential duplicate events.</p>
</li>
<li><p><strong>Background Jobs</strong>: Use background jobs for any operations that might take more than a few seconds to complete.</p>
</li>
<li><p><strong>Error Handling</strong>: Implement comprehensive error handling and logging, especially for webhook processing and API calls.</p>
</li>
<li><p><strong>Security</strong>: Always encrypt sensitive data and never expose API keys in the frontend.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building a multi-tenant Shopify app requires careful consideration of security, scalability, and user experience. By leveraging Rails, App Bridge, and modern development practices, we created a robust application that securely handles multiple stores and their data.</p>
<p>The combination of Shopify's App Bridge, Rails 8, and modern tools like Solid Queue and Tailwind CSS provided a solid foundation for building a scalable SaaS application.</p>
<h2 id="heading-next-steps">Next Steps</h2>
<p>If you're building a Shopify app, consider these recommendations:</p>
<ol>
<li>Start with a solid authentication and authorization system</li>
<li>Implement webhook handling early in the development process</li>
<li>Use background jobs for long-running tasks</li>
<li>Plan for scalability from the beginning</li>
<li>Follow Shopify's security best practices</li>
</ol>
<p>Remember that building a multi-tenant application requires careful consideration of data isolation and security at every level of your application.</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Rails CI/CD with Dokku & GitHub Actions]]></title><description><![CDATA[As developers, we're always looking for ways to streamline our deployment process while maintaining security and reliability. Today, I'm excited to share my experience setting up an automated deployment pipeline for a Rails application using Dokku an...]]></description><link>https://sulmanweb.com/rails-dokku-github-actions-deployment</link><guid isPermaLink="true">https://sulmanweb.com/rails-dokku-github-actions-deployment</guid><category><![CDATA[Rails]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[Dokku]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Thu, 30 Jan 2025 05:00:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737040565228/ac4bdef7-e7f1-41ae-8fae-21ad1a5f40db.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As developers, we're always looking for ways to streamline our deployment process while maintaining security and reliability. Today, I'm excited to share my experience setting up an automated deployment pipeline for a Rails application using Dokku and GitHub Actions. This setup has transformed our deployment process from a manual, error-prone task into a smooth, automated workflow.</p>
<h2 id="heading-the-challenge">The Challenge</h2>
<p>Managing production deployments can be tricky. You want something robust like Heroku but with more control over your infrastructure. Enter Dokku - a lightweight, open-source Platform as a Service (PaaS) that gives you Heroku-like features on your own server.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we dive in, make sure you have:</p>
<ul>
<li><p>A Dokku server set up and running</p>
<p>  %[https://sulmanweb.com/rails-8-production-dokku-deployment-guide] </p>
</li>
<li><p>An existing Rails application deployed to the server</p>
</li>
<li><p>Administrative access to both the server and your GitHub repository</p>
</li>
</ul>
<h2 id="heading-setting-up-secure-deployment-keys">Setting Up Secure Deployment Keys</h2>
<p>The first step in our automation journey is setting up secure SSH keys for deployment. This is crucial for allowing GitHub Actions to securely communicate with our Dokku server.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># On your production server</span>
ssh-keygen -t ed25519 -f dokku
</code></pre>
<p>This command generates two files:</p>
<ul>
<li><p><code>dokku</code> - Your private key (keep this secret!)</p>
</li>
<li><p><code>dokku.pub</code> - Your public key (safe to share)</p>
</li>
</ul>
<p>Next, we need to configure Dokku and the server to trust these keys:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Add the key to Dokku</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"YOUR_PUBLIC_KEY_CONTENT"</span> | dokku ssh-keys:add github

<span class="hljs-comment"># Add to authorized keys for additional security</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"YOUR_PUBLIC_KEY_CONTENT"</span> &gt;&gt; ~/.ssh/authorized_keys
</code></pre>
<h2 id="heading-github-repository-configuration">GitHub Repository Configuration</h2>
<p>With our keys in place, we need to store the private key securely in GitHub. Here's how:</p>
<ol>
<li><p>Navigate to your repository's Settings → Secrets and variables → Actions</p>
</li>
<li><p>Create a new secret named <code>PRODUCTION_DOKKU_SSH_PRIVATE_KEY</code></p>
</li>
<li><p>Paste your private key content</p>
</li>
</ol>
<p>This secret will be securely encrypted and only accessible during the deployment process.</p>
<h2 id="heading-the-magic-github-actions-workflow">The Magic: GitHub Actions Workflow</h2>
<p>Now for the exciting part - setting up our automated deployment pipeline. Create or modify <code>.github/workflows/ci.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">deploy_production:</span>
    <span class="hljs-attr">needs:</span> [ <span class="hljs-string">test</span> ]
    <span class="hljs-attr">if:</span> <span class="hljs-string">github.event_name</span> <span class="hljs-string">==</span> <span class="hljs-string">'push'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">github.ref</span> <span class="hljs-string">==</span> <span class="hljs-string">'refs/heads/main'</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-24.04</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Cloning</span> <span class="hljs-string">repo</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v3</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">fetch-depth:</span> <span class="hljs-number">0</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Push</span> <span class="hljs-string">to</span> <span class="hljs-string">dokku</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">dokku/github-action@master</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">branch:</span> <span class="hljs-string">'main'</span>
          <span class="hljs-attr">trace:</span> <span class="hljs-string">'1'</span>
          <span class="hljs-attr">git_push_flags:</span> <span class="hljs-string">'--force'</span>
          <span class="hljs-attr">git_remote_url:</span> <span class="hljs-string">'ssh://dokku@SERVER_IP:22/APP_NAME_IN_DOKKU'</span>
          <span class="hljs-attr">ssh_private_key:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.PRODUCTION_DOKKU_SSH_PRIVATE_KEY</span> <span class="hljs-string">}}</span>
</code></pre>
<p>Let's break down what's happening here:</p>
<h3 id="heading-workflow-triggers">Workflow Triggers</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">if:</span> <span class="hljs-string">github.event_name</span> <span class="hljs-string">==</span> <span class="hljs-string">'push'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">github.ref</span> <span class="hljs-string">==</span> <span class="hljs-string">'refs/heads/main'</span>
</code></pre>
<p>This condition ensures deployments only happen when code is pushed to the main branch - perfect for a trunk-based development workflow.</p>
<h3 id="heading-job-dependencies">Job Dependencies</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">needs:</span> [ <span class="hljs-string">test</span> ]
</code></pre>
<p>By specifying <code>needs: [ test ]</code>, we ensure our tests pass before any deployment occurs. This is crucial for maintaining code quality!</p>
<h3 id="heading-deployment-configuration">Deployment Configuration</h3>
<p>The <code>dokku/github-action@master</code> action handles the heavy lifting:</p>
<ul>
<li><p><code>branch: 'main'</code> - Specifies which branch to deploy</p>
</li>
<li><p><code>trace: '1'</code> - Enables detailed logging for troubleshooting</p>
</li>
<li><p><code>git_push_flags: '--force'</code> - Ensures clean deployments</p>
</li>
<li><p><code>git_remote_url</code> - Connects to your Dokku server</p>
</li>
<li><p><code>ssh_private_key</code> - Securely authenticates using our previously configured key</p>
</li>
</ul>
<h2 id="heading-the-deployment-process">The Deployment Process</h2>
<p>With everything set up, deploying is as simple as:</p>
<pre><code class="lang-bash">git push origin main
</code></pre>
<p>This triggers the following sequence:</p>
<ol>
<li><p>Your code is pushed to GitHub</p>
</li>
<li><p>GitHub Actions detects the push to main</p>
</li>
<li><p>The test suite runs</p>
</li>
<li><p>If tests pass, the deployment job begins</p>
</li>
<li><p>Your code is securely deployed to the Dokku server</p>
</li>
</ol>
<h2 id="heading-personal-insights-and-tips">Personal Insights and Tips</h2>
<p>Through implementing this setup, I've learned some valuable lessons:</p>
<ol>
<li><p><strong>Security First</strong>: Always use dedicated deployment keys and never store sensitive information in your repository.</p>
</li>
<li><p><strong>Test Dependencies</strong>: Make deployment dependent on test success to prevent broken code from reaching production.</p>
</li>
<li><p><strong>Logging Matters</strong>: The <code>trace: '1'</code> option has saved hours of debugging by providing detailed deployment logs.</p>
</li>
</ol>
<h2 id="heading-looking-forward">Looking Forward</h2>
<p>This setup has dramatically improved our deployment workflow, but there's always room for enhancement. Future improvements might include:</p>
<ul>
<li><p>Adding staging environment deployments</p>
</li>
<li><p>Implementing automatic database backups pre-deployment</p>
</li>
<li><p>Setting up deployment notifications in Slack</p>
</li>
</ul>
<p>Remember, the goal of automation isn't just to save time - it's to build confidence in your deployment process and free up mental energy for solving more interesting problems.</p>
<p>Have you implemented a similar setup? I'd love to hear about your experiences and any improvements you've made to this workflow!</p>
<p>Happy deployments!</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Rails 8 Production Setup with Dokku Guide]]></title><description><![CDATA[The Challenge: Modernizing Rails Deployment
When I recently needed to deploy my Rails 8 application with multiple databases, I faced a common dilemma: choosing between expensive managed solutions and complex self-managed servers. My requirements were...]]></description><link>https://sulmanweb.com/rails-8-production-dokku-deployment-guide</link><guid isPermaLink="true">https://sulmanweb.com/rails-8-production-dokku-deployment-guide</guid><category><![CDATA[Dokku]]></category><category><![CDATA[Rails]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Mon, 27 Jan 2025 05:00:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737039484198/ae3b9736-1624-406b-887b-648143656ed0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-challenge-modernizing-rails-deployment">The Challenge: Modernizing Rails Deployment</h2>
<p>When I recently needed to deploy my Rails 8 application with multiple databases, I faced a common dilemma: choosing between expensive managed solutions and complex self-managed servers. My requirements were specific:</p>
<ul>
<li><p>Multiple PostgreSQL databases for different concerns</p>
</li>
<li><p>SSL certificate management</p>
</li>
<li><p>Automated deployment pipeline</p>
</li>
<li><p>Cost-effective hosting</p>
</li>
<li><p>Easy database management</p>
</li>
<li><p>Background job processing</p>
</li>
</ul>
<p>Let me walk you through how I solved this using Dokku.</p>
<h2 id="heading-setting-up-our-foundation">Setting Up Our Foundation</h2>
<p>First, let's prepare our server. I chose a 2GB RAM VPS - here's why:</p>
<ul>
<li><p>Dokku needs about 1GB RAM to operate smoothly</p>
</li>
<li><p>Rails 8 with Puma and background jobs needs the other 1GB</p>
</li>
<li><p>Anything less resulted in occasional memory issues during asset compilation</p>
</li>
</ul>
<h3 id="heading-initial-server-configuration">Initial Server Configuration</h3>
<p>SSH into your server and install Dokku:</p>
<pre><code class="lang-bash">wget -NP . https://dokku.com/install/v0.35.13/bootstrap.sh
sudo DOKKU_TAG=v0.35.13 bash bootstrap.sh
</code></pre>
<p>After countless deployments, I've found this version to be particularly stable with Rails 8.</p>
<h2 id="heading-the-database-architecture">The Database Architecture</h2>
<p>One of Rails 8's strengths is its multi-database support. In my setup, I separated concerns into four databases:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create our databases</span>
dokku postgres:create rails_primary_db
dokku postgres:create rails_cache_db
dokku postgres:create rails_queue_db
dokku postgres:create rails_cable_db
</code></pre>
<p>Here's why I chose this architecture:</p>
<ul>
<li><p>Primary DB: Core application data</p>
</li>
<li><p>Cache DB: Session and cache storage</p>
</li>
<li><p>Queue DB: Background job management</p>
</li>
<li><p>Cable DB: Action Cable subscriptions</p>
</li>
</ul>
<h3 id="heading-the-magic-of-database-linking">The Magic of Database Linking</h3>
<p>This is where things get interesting. When linking databases, Dokku generates unique environment variables:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Link each database and capture the environment variable names</span>
dokku postgres:link rails_primary_db your-rails-app
<span class="hljs-comment"># Returns: DATABASE_URL=postgres://...</span>

dokku postgres:link rails_cache_db your-rails-app
<span class="hljs-comment"># Returns: DOKKU_POSTGRES_AQUA_URL=postgres://...</span>

dokku postgres:link rails_queue_db your-rails-app
<span class="hljs-comment"># Returns: DOKKU_POSTGRES_BLACK_URL=postgres://...</span>

dokku postgres:link rails_cable_db your-rails-app
<span class="hljs-comment"># Returns: DOKKU_POSTGRES_BLUE_URL=postgres://...</span>
</code></pre>
<h2 id="heading-rails-8-configuration-magic">Rails 8 Configuration Magic</h2>
<p>Here's how I configured my Rails 8 application to use these databases. In <code>config/database.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">production:</span>
  <span class="hljs-attr">primary:</span> <span class="hljs-meta">&amp;primary_production</span>
    <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-meta">*default</span>
    <span class="hljs-attr">database:</span> &lt;%=<span class="ruby"> ENV[<span class="hljs-string">'DATABASE_URL'</span>] <span class="hljs-params">||</span> <span class="hljs-string">'rails_primary_db'</span> </span>%&gt;
    <span class="hljs-attr">url:</span> &lt;%=<span class="ruby"> ENV[<span class="hljs-string">'DATABASE_URL'</span>] </span>%&gt;

  <span class="hljs-attr">cache:</span>
    <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-meta">*primary_production</span>
    <span class="hljs-attr">database:</span> &lt;%=<span class="ruby"> ENV[<span class="hljs-string">'DOKKU_POSTGRES_AQUA_URL'</span>] <span class="hljs-params">||</span> <span class="hljs-string">'rails_cache_db'</span> </span>%&gt;
    <span class="hljs-attr">migrations_paths:</span> <span class="hljs-string">db/cache_migrate</span>
    <span class="hljs-attr">url:</span> &lt;%=<span class="ruby"> ENV[<span class="hljs-string">'DOKKU_POSTGRES_AQUA_URL'</span>] </span>%&gt;

  <span class="hljs-attr">queue:</span>
    <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-meta">*primary_production</span>
    <span class="hljs-attr">database:</span> &lt;%=<span class="ruby"> ENV[<span class="hljs-string">'DOKKU_POSTGRES_BLACK_URL'</span>] <span class="hljs-params">||</span> <span class="hljs-string">'rails_queue_db'</span> </span>%&gt;
    <span class="hljs-attr">migrations_paths:</span> <span class="hljs-string">db/queue_migrate</span>
    <span class="hljs-attr">url:</span> &lt;%=<span class="ruby"> ENV[<span class="hljs-string">'DOKKU_POSTGRES_BLACK_URL'</span>] </span>%&gt;

  <span class="hljs-attr">cable:</span>
    <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-meta">*primary_production</span>
    <span class="hljs-attr">database:</span> &lt;%=<span class="ruby"> ENV[<span class="hljs-string">'DOKKU_POSTGRES_BLUE_URL'</span>] <span class="hljs-params">||</span> <span class="hljs-string">'rails_cable_db'</span> </span>%&gt;
    <span class="hljs-attr">migrations_paths:</span> <span class="hljs-string">db/cable_migrate</span>
    <span class="hljs-attr">url:</span> &lt;%=<span class="ruby"> ENV[<span class="hljs-string">'DOKKU_POSTGRES_BLUE_URL'</span>] </span>%&gt;
</code></pre>
<h2 id="heading-the-deployment-pipeline">The Deployment Pipeline</h2>
<p>Create a <code>Procfile</code> in your Rails root:</p>
<pre><code class="lang-plaintext">web: bin/rails server -p $PORT -e $RAILS_ENV
worker: bin/jobs
release: bundle exec rails db:migrate 2&gt;/dev/null || bundle exec rails db:setup
</code></pre>
<p>I learned the hard way that the <code>release</code> command needs error handling for first deployments, hence the <code>2&gt;/dev/null || bundle exec rails db:setup</code> pattern.</p>
<h2 id="heading-environment-configuration">Environment Configuration</h2>
<p>Rails 8 needs specific environment variables. Here's my production setup:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Rails essentials</span>
dokku config:<span class="hljs-built_in">set</span> your-rails-app RAILS_ENV=production
dokku config:<span class="hljs-built_in">set</span> your-rails-app RACK_ENV=production
dokku config:<span class="hljs-built_in">set</span> your-rails-app RAILS_MASTER_KEY=$(cat config/credentials/production.key)

<span class="hljs-comment"># Rails 8 specifics</span>
dokku config:<span class="hljs-built_in">set</span> your-rails-app RAILS_SERVE_STATIC_FILES=<span class="hljs-literal">true</span>
dokku config:<span class="hljs-built_in">set</span> your-rails-app RAILS_LOG_TO_STDOUT=<span class="hljs-literal">true</span>
</code></pre>
<h2 id="heading-git-deployment-setup">Git Deployment Setup</h2>
<p>Configure your local repository:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Add Dokku remote</span>
git remote add production dokku@your-server-ip:your-rails-app

<span class="hljs-comment"># Verify remote addition</span>
git remote show production

<span class="hljs-comment"># Set deployment branch</span>
dokku git:<span class="hljs-built_in">set</span> your-rails-app deploy-branch main
</code></pre>
<h2 id="heading-ssl-configuration">SSL Configuration</h2>
<p>Rails 8 expects HTTPS in production. Here's how to set it up with Let's Encrypt:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install the plugin</span>
sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git

<span class="hljs-comment"># Configure your domain</span>
dokku domains:clear-global
dokku domains:<span class="hljs-built_in">set</span> your-rails-app yourdomain.com

<span class="hljs-comment"># Setup SSL</span>
dokku letsencrypt:<span class="hljs-built_in">set</span> your-rails-app email your@email.com
dokku letsencrypt:<span class="hljs-built_in">enable</span> your-rails-app
dokku letsencrypt:cron-job --add
</code></pre>
<h2 id="heading-deployment-and-monitoring">Deployment and Monitoring</h2>
<p>Deploy your application:</p>
<pre><code class="lang-bash">git push production main
</code></pre>
<p>Monitor your deployment:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Real-time logs</span>
dokku logs your-rails-app -t

<span class="hljs-comment"># Process status</span>
dokku ps:report your-rails-app

<span class="hljs-comment"># Database status</span>
dokku postgres:info rails_primary_db
</code></pre>
<h2 id="heading-troubleshooting-common-rails-8-issues">Troubleshooting Common Rails 8 Issues</h2>
<p>During my deployments, I encountered several Rails 8-specific issues:</p>
<ol>
<li><p><strong>Asset Compilation Failures</strong></p>
<pre><code class="lang-bash"> <span class="hljs-comment"># Force a clean asset compilation</span>
 dokku run your-rails-app bundle <span class="hljs-built_in">exec</span> rails assets:clobber assets:precompile
</code></pre>
</li>
<li><p><strong>Database Migration Issues</strong></p>
<pre><code class="lang-bash"> <span class="hljs-comment"># Run migrations manually if needed</span>
 dokku run your-rails-app bundle <span class="hljs-built_in">exec</span> rails db:migrate
</code></pre>
</li>
<li><p><strong>Worker Process Management</strong></p>
<pre><code class="lang-bash"> <span class="hljs-comment"># Restart workers after config changes</span>
 dokku ps:restart your-rails-app worker
</code></pre>
</li>
</ol>
<h2 id="heading-maintenance-tips">Maintenance Tips</h2>
<p>Based on my experience, here are some crucial maintenance practices:</p>
<h3 id="heading-database-backups">Database Backups</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Backup all databases</span>
<span class="hljs-keyword">for</span> db <span class="hljs-keyword">in</span> primary cache queue cable; <span class="hljs-keyword">do</span>
  dokku postgres:<span class="hljs-built_in">export</span> rails_<span class="hljs-variable">${db}</span>_db &gt; <span class="hljs-variable">${db}</span>_backup.dump
<span class="hljs-keyword">done</span>
</code></pre>
<h3 id="heading-performance-monitoring">Performance Monitoring</h3>
<p>Monitor your Rails 8 application's health:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check memory usage</span>
dokku ps:report your-rails-app

<span class="hljs-comment"># View error logs</span>
dokku logs your-rails-app -t | grep ERROR
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Deploying Rails 8 with Dokku has been a game-changer for my development workflow. The combination of Rails 8's robust multi-database support and Dokku's Heroku-like deployment experience has made managing production applications both cost-effective and developer-friendly.</p>
<p>Remember to:</p>
<ul>
<li><p>Keep your Rails master key secure</p>
</li>
<li><p>Regularly backup all databases</p>
</li>
<li><p>Monitor worker processes</p>
</li>
<li><p>Keep an eye on memory usage</p>
</li>
</ul>
<p>The setup might seem extensive, but once configured, deployments become as simple as <code>git push production main</code>. Feel free to reach out if you encounter any Rails 8-specific deployment challenges!</p>
<p>This guide reflects my real-world experience deploying Rails 8 applications with Dokku. As you work with this setup, you'll likely discover additional optimizations specific to your use case.</p>
<p>Happy deploying!</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Heroku to Dokku: A Rails Dev's Migration Tale]]></title><description><![CDATA[The Cost-Benefit Realization
As a Rails developer who recently migrated from Heroku to Dokku, I want to share my journey and the surprising benefits I discovered. This transition wasn't just about cost savings—it opened up new possibilities for my de...]]></description><link>https://sulmanweb.com/heroku-to-dokku-rails-migration-guide</link><guid isPermaLink="true">https://sulmanweb.com/heroku-to-dokku-rails-migration-guide</guid><category><![CDATA[Dokku]]></category><category><![CDATA[Rails]]></category><category><![CDATA[Rails 8]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Thu, 23 Jan 2025 05:00:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736906301182/2ae17cd5-0839-4267-abd7-102e4653a5d8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-cost-benefit-realization">The Cost-Benefit Realization</h2>
<p>As a Rails developer who recently migrated from Heroku to Dokku, I want to share my journey and the surprising benefits I discovered. This transition wasn't just about cost savings—it opened up new possibilities for my deployment workflow.</p>
<h2 id="heading-the-heroku-challenge">The Heroku Challenge</h2>
<p>Before diving into Dokku's benefits, let me share why I started looking for alternatives. Heroku had been my go-to platform for years, offering:</p>
<ul>
<li><p>Zero-configuration deployments</p>
</li>
<li><p>Excellent developer experience</p>
</li>
<li><p>Reliable infrastructure</p>
</li>
<li><p>Built-in add-ons</p>
</li>
</ul>
<p>However, recent changes created some challenges:</p>
<ul>
<li><p>Removal of free tier</p>
</li>
<li><p>Significant cost increases for basic dynos</p>
</li>
<li><p>Sleep states affecting development apps</p>
</li>
<li><p>Add-on costs adding up quickly</p>
</li>
</ul>
<h2 id="heading-why-dokku-won-me-over">Why Dokku Won Me Over</h2>
<h3 id="heading-1-cost-efficiency">1. Cost Efficiency</h3>
<pre><code class="lang-ruby"><span class="hljs-comment"># Monthly Cost Comparison for a Basic Rails Setup</span>
heroku_cost = {
  <span class="hljs-symbol">basic_dyno:</span> <span class="hljs-number">7</span>,
  <span class="hljs-symbol">postgres:</span> <span class="hljs-number">9</span>,
  <span class="hljs-symbol">redis:</span> <span class="hljs-number">15</span>,
  <span class="hljs-symbol">ssl:</span> <span class="hljs-number">20</span>,
  <span class="hljs-symbol">total:</span> <span class="hljs-number">51</span>
}

dokku_cost = {
  <span class="hljs-symbol">server_2gb:</span> <span class="hljs-number">10</span>,
  <span class="hljs-symbol">managed_databases:</span> <span class="hljs-number">0</span>,  <span class="hljs-comment"># Included!</span>
  <span class="hljs-symbol">ssl:</span> <span class="hljs-number">0</span>,               <span class="hljs-comment"># Free via Let's Encrypt</span>
  <span class="hljs-symbol">total:</span> <span class="hljs-number">10</span>
}
</code></pre>
<h3 id="heading-2-infrastructure-control">2. Infrastructure Control</h3>
<p>Unlike Heroku's black box approach, Dokku lets me:</p>
<ul>
<li><p>Choose my server provider (Digital Ocean, Linode, AWS)</p>
</li>
<li><p>Customize server resources</p>
</li>
<li><p>Configure nginx directly</p>
</li>
<li><p>Manage database backups my way</p>
</li>
</ul>
<h3 id="heading-3-multi-app-efficiency">3. Multi-App Efficiency</h3>
<p>One of my favorite discoveries:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Deploy multiple apps on one server</span>
dokku apps:create rails-app-1
dokku apps:create rails-app-2
dokku apps:create rails-app-3

<span class="hljs-comment"># Each with its own domains and SSL</span>
dokku domains:<span class="hljs-built_in">set</span> rails-app-1 app1.domain.com
dokku domains:<span class="hljs-built_in">set</span> rails-app-2 app2.domain.com
</code></pre>
<h3 id="heading-4-familiar-workflow">4. Familiar Workflow</h3>
<p>Dokku maintains Heroku's git-based deployment:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Just like Heroku</span>
git push dokku main

<span class="hljs-comment"># Even supports release phase</span>
dokku config:<span class="hljs-built_in">set</span> rails-app-1 DOKKU_RELEASE_COMMAND=<span class="hljs-string">"rails db:migrate"</span>
</code></pre>
<h3 id="heading-5-database-flexibility">5. Database Flexibility</h3>
<p>My Rails apps often need multiple databases:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create separate databases for different concerns</span>
dokku postgres:create main_db
dokku postgres:create analytics_db
dokku postgres:create cache_db

<span class="hljs-comment"># Link them easily</span>
dokku postgres:link main_db rails-app
</code></pre>
<h3 id="heading-6-custom-plugin-ecosystem">6. Custom Plugin Ecosystem</h3>
<p>Beyond basic features:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install useful plugins</span>
sudo dokku plugin:install https://github.com/dokku/dokku-postgres.git
sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
sudo dokku plugin:install https://github.com/dokku/dokku-redis.git
</code></pre>
<h2 id="heading-real-world-benefits-ive-experienced">Real-World Benefits I've Experienced</h2>
<h3 id="heading-development-environment">Development Environment</h3>
<ul>
<li><p>Faster deployments (no queue waiting)</p>
</li>
<li><p>Direct log access</p>
</li>
<li><p>Quick database operations</p>
</li>
<li><p>Custom domain management</p>
</li>
</ul>
<h3 id="heading-production-advantages">Production Advantages</h3>
<ul>
<li><p>Significant cost savings</p>
</li>
<li><p>Better resource utilization</p>
</li>
<li><p>Full control over backups</p>
</li>
<li><p>Custom monitoring solutions</p>
</li>
</ul>
<h3 id="heading-scaling-capabilities">Scaling Capabilities</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Scale processes individually</span>
dokku ps:scale web=2
dokku ps:scale worker=1

<span class="hljs-comment"># Monitor resource usage</span>
dokku ps:report
</code></pre>
<h2 id="heading-making-the-transition-easier">Making the Transition Easier</h2>
<p>Here's what helped me during migration:</p>
<ol>
<li><strong>Environment Variable Management</strong></li>
</ol>
<pre><code class="lang-bash"><span class="hljs-comment"># Export from Heroku</span>
heroku config -a your-app &gt; config.txt

<span class="hljs-comment"># Import to Dokku</span>
<span class="hljs-keyword">while</span> <span class="hljs-built_in">read</span> line; <span class="hljs-keyword">do</span>
  dokku config:<span class="hljs-built_in">set</span> your-app <span class="hljs-variable">$line</span>
<span class="hljs-keyword">done</span> &lt; config.txt
</code></pre>
<ol start="2">
<li><strong>Database Migration</strong></li>
</ol>
<pre><code class="lang-bash"><span class="hljs-comment"># Export from Heroku</span>
heroku pg:backups:capture
heroku pg:backups:download

<span class="hljs-comment"># Import to Dokku</span>
dokku postgres:import database &lt; latest.dump
</code></pre>
<h2 id="heading-when-to-choose-dokku-over-heroku">When to Choose Dokku Over Heroku</h2>
<p>Consider Dokku when you:</p>
<ul>
<li><p>Need cost-effective hosting</p>
</li>
<li><p>Want multiple apps on one server</p>
</li>
<li><p>Require custom infrastructure</p>
</li>
<li><p>Have basic sysadmin knowledge</p>
</li>
<li><p>Value deployment flexibility</p>
</li>
</ul>
<p>Stay with Heroku if you:</p>
<ul>
<li><p>Need enterprise-level support</p>
</li>
<li><p>Require zero server management</p>
</li>
<li><p>Want managed add-on services</p>
</li>
<li><p>Have a larger team needing access controls</p>
</li>
</ul>
<h2 id="heading-learning-curve-and-roi">Learning Curve and ROI</h2>
<p>The initial setup took me about a day, but the benefits were immediate:</p>
<ul>
<li><p>Monthly costs dropped by 80%</p>
</li>
<li><p>Deployment times improved</p>
</li>
<li><p>More control over infrastructure</p>
</li>
<li><p>Better understanding of my stack</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>My journey from Heroku to Dokku has been transformative. While Heroku remains an excellent platform, Dokku provides a powerful, cost-effective alternative that doesn't compromise on developer experience. The learning curve is worth the benefits, especially for Rails applications where cost and control matter.</p>
<p>Remember: Dokku isn't just a Heroku alternative—it's a different way of thinking about deployments that empowers developers to take control of their infrastructure while maintaining the simplicity we love about Platform as a Service solutions.</p>
<p>If you're considering the switch, feel free to reach out. The Rails community around Dokku is growing, and sharing experiences helps everyone succeed in their deployment journey!</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Rails 8 CI/CD: GitHub Actions & Kamal 2025]]></title><description><![CDATA[In my previous article, we explored deploying Rails 8 applications using Docker and Kamal.
https://sulmanweb.com/deploy-rails-8-docker-kamal-production-guide
 
Today, I'm excited to share how we can take this deployment process to the next level by a...]]></description><link>https://sulmanweb.com/rails-8-github-actions-kamal-automated-deployment-guide</link><guid isPermaLink="true">https://sulmanweb.com/rails-8-github-actions-kamal-automated-deployment-guide</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Rails]]></category><category><![CDATA[deployment]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Mon, 20 Jan 2025 05:00:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736906124277/5a4c5d6a-f14c-40b4-a2dc-99e5f2164396.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my previous article, we explored deploying Rails 8 applications using Docker and Kamal.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://sulmanweb.com/deploy-rails-8-docker-kamal-production-guide">https://sulmanweb.com/deploy-rails-8-docker-kamal-production-guide</a></div>
<p> </p>
<p>Today, I'm excited to share how we can take this deployment process to the next level by automating it with GitHub Actions. This automation will make our deployments more consistent, reduce human error, and save valuable development time.</p>
<h2 id="heading-why-add-github-actions-to-our-deployment-stack">Why Add GitHub Actions to Our Deployment Stack?</h2>
<p>When I first started using Kamal for deployments, I found myself repeatedly running the same commands. While Kamal made the process straightforward, I knew we could make it even better. GitHub Actions provides the perfect solution by:</p>
<ul>
<li><p>Automating deployments on code pushes to main</p>
</li>
<li><p>Ensuring consistent deployment environments</p>
</li>
<li><p>Managing secrets securely</p>
</li>
<li><p>Providing detailed deployment logs and history</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we dive in, make sure you have:</p>
<ul>
<li><p>A Rails 8 application configured with Kamal (<a target="_blank" href="https://sulmanweb.com/deploy-rails-8-docker-kamal-production-guide">https://sulmanweb.com/deploy-rails-8-docker-kamal-production-guide</a>)</p>
</li>
<li><p>A GitHub repository for your application</p>
</li>
<li><p>Access to your repository's settings to configure secrets</p>
</li>
</ul>
<h2 id="heading-setting-up-github-actions">Setting Up GitHub Actions</h2>
<p>Let's break down the process into manageable steps.</p>
<h3 id="heading-1-creating-the-workflow-file">1. Creating the Workflow File</h3>
<p>First, create a new file at <code>.github/workflows/deploy.yml</code>. This is where our deployment magic will happen.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">Production</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>

<span class="hljs-attr">permissions:</span>
  <span class="hljs-attr">contents:</span> <span class="hljs-string">read</span>
  <span class="hljs-attr">packages:</span> <span class="hljs-string">write</span>
  <span class="hljs-attr">id-token:</span> <span class="hljs-string">write</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">deploy:</span>
    <span class="hljs-attr">if:</span> <span class="hljs-string">github.event_name</span> <span class="hljs-string">==</span> <span class="hljs-string">'push'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">github.ref</span> <span class="hljs-string">==</span> <span class="hljs-string">'refs/heads/main'</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-24.04</span>
    <span class="hljs-attr">env:</span>
      <span class="hljs-attr">DOCKER_BUILDKIT:</span> <span class="hljs-number">1</span>
      <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">production</span>
      <span class="hljs-attr">BUNDLE_WITHOUT:</span> <span class="hljs-string">"development test"</span>
      <span class="hljs-attr">BUNDLE_WITH:</span> <span class="hljs-string">tools</span>
</code></pre>
<h3 id="heading-2-setting-up-deployment-steps">2. Setting Up Deployment Steps</h3>
<p>Let's examine each step of our deployment process:</p>
<pre><code class="lang-yaml">    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">code</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">Ruby</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">ruby/setup-ruby@v1</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">ruby-version:</span> <span class="hljs-string">.ruby-version</span>
          <span class="hljs-attr">bundler-cache:</span> <span class="hljs-literal">true</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">Docker</span> <span class="hljs-string">Buildx</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">docker/setup-buildx-action@v3</span>
</code></pre>
<p>These initial steps prepare our deployment environment:</p>
<ul>
<li><p><code>checkout</code> clones our repository</p>
</li>
<li><p><code>setup-ruby</code> configures Ruby using our project's <code>.ruby-version</code></p>
</li>
<li><p><code>setup-buildx</code> enables Docker's advanced build capabilities</p>
</li>
</ul>
<h3 id="heading-3-configuring-ssh-access">3. Configuring SSH Access</h3>
<p>The SSH setup is crucial for secure server access:</p>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">SSH</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" &gt; ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          eval $(ssh-agent -s)
          ssh-add ~/.ssh/id_rsa
          ssh-keyscan xx.xx.xx.xx &gt;&gt; ~/.ssh/known_hosts
</span>        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">SSH_PRIVATE_KEY:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.SSH_PRIVATE_KEY</span> <span class="hljs-string">}}</span>
</code></pre>
<h3 id="heading-4-running-the-deployment">4. Running the Deployment</h3>
<p>Finally, we execute Kamal:</p>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">with</span> <span class="hljs-string">Kamal</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">bin/kamal</span> <span class="hljs-string">deploy</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">RAILS_MASTER_KEY:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.RAILS_MASTER_KEY</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">KAMAL_REGISTRY_PASSWORD:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.KAMAL_REGISTRY_PASSWORD</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.POSTGRES_PASSWORD</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">POSTGRES_PASS:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.POSTGRES_PASSWORD</span> <span class="hljs-string">}}</span>
</code></pre>
<h2 id="heading-setting-up-github-secrets">Setting Up GitHub Secrets</h2>
<p>Security is paramount in automated deployments. Here's how to set up your secrets in GitHub:</p>
<ol>
<li><p>Navigate to your repository's Settings</p>
</li>
<li><p>Select "Secrets and variables" → "Actions"</p>
</li>
<li><p>Click "New repository secret"</p>
</li>
<li><p>Add the following secrets:</p>
<ul>
<li><p><code>SSH_PRIVATE_KEY</code>: Your server's SSH private key</p>
</li>
<li><p><code>RAILS_MASTER_KEY</code>: Your Rails master key</p>
</li>
<li><p><code>KAMAL_REGISTRY_PASSWORD</code>: Docker registry password</p>
</li>
<li><p><code>POSTGRES_PASSWORD</code>: Database password</p>
</li>
</ul>
</li>
</ol>
<p>Pro tip: Generate strong passwords for these secrets using a password manager!</p>
<h2 id="heading-understanding-environment-variables">Understanding Environment Variables</h2>
<p>Our workflow uses several environment variables:</p>
<ul>
<li><p><code>DOCKER_BUILDKIT: 1</code>: Enables Docker's modern build system</p>
</li>
<li><p><code>RAILS_ENV: production</code>: Sets Rails environment</p>
</li>
<li><p><code>BUNDLE_WITHOUT</code>: Optimizes gem installation</p>
</li>
<li><p><code>BUNDLE_WITH: tools</code>: Ensures deployment tools are available</p>
</li>
</ul>
<h2 id="heading-deployment-flow">Deployment Flow</h2>
<p>When you push to main, here's what happens:</p>
<ol>
<li><p>GitHub Actions triggers the workflow</p>
</li>
<li><p>The environment is prepared with Ruby and Docker</p>
</li>
<li><p>SSH access is configured securely</p>
</li>
<li><p>Kamal executes the deployment</p>
</li>
<li><p>Your application is updated with zero downtime</p>
</li>
</ol>
<h2 id="heading-best-practices-and-tips">Best Practices and Tips</h2>
<p>Through my experience with this setup, I've learned some valuable lessons:</p>
<ol>
<li><p><strong>Test Your Workflow</strong>: Use GitHub Actions' "workflow_dispatch" trigger to test manually before automating.</p>
</li>
<li><p><strong>Monitor Deployments</strong>: Watch your Actions tab for deployment logs.</p>
</li>
<li><p><strong>Secure Your Secrets</strong>: Regularly rotate your credentials and use strong passwords.</p>
</li>
<li><p><strong>Keep Dependencies Updated</strong>: Regularly update your GitHub Actions versions.</p>
</li>
</ol>
<h2 id="heading-troubleshooting-common-issues">Troubleshooting Common Issues</h2>
<p>If you encounter issues, check these common points:</p>
<ol>
<li><p><strong>SSH Connection Failures</strong></p>
<ul>
<li><p>Verify your SSH private key format</p>
</li>
<li><p>Ensure the server IP in <code>known_hosts</code> is correct</p>
</li>
</ul>
</li>
<li><p><strong>Docker Registry Issues</strong></p>
<ul>
<li><p>Confirm your registry credentials</p>
</li>
<li><p>Check Docker login status</p>
</li>
</ul>
</li>
<li><p><strong>Environment Variables</strong></p>
<ul>
<li><p>Verify all secrets are properly set</p>
</li>
<li><p>Check for typos in secret names</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Automating deployments with GitHub Actions and Kamal has transformed our deployment process from a manual task to a streamlined, automated workflow. Not only does this save time, but it also provides consistency and reliability in our deployments.</p>
<p>Remember, automation is about finding the right balance between convenience and control. With this setup, we maintain full control over our deployment process while enjoying the benefits of automation.</p>
<p>What's your experience with automated deployments? I'd love to hear your thoughts and experiences in the comments below! 🚀</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Rails Self-Join Tables: Parent-Child Magic]]></title><description><![CDATA[In modern web applications, we often need to represent hierarchical data structures where records can have parent-child relationships within the same table. Think of organizational charts, nested categories, or multi-level attributes. Today, I'll wal...]]></description><link>https://sulmanweb.com/rails-self-referential-table-inheritance-tutorial</link><guid isPermaLink="true">https://sulmanweb.com/rails-self-referential-table-inheritance-tutorial</guid><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Ruby]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Thu, 16 Jan 2025 05:00:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736905921165/e6a9042f-89ad-4806-b47f-0fa9b66adcb5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In modern web applications, we often need to represent hierarchical data structures where records can have parent-child relationships within the same table. Think of organizational charts, nested categories, or multi-level attributes. Today, I'll walk you through implementing self-referential associations in Ruby on Rails, using a practical example from a laboratory management system.</p>
<h2 id="heading-the-challenge">The Challenge</h2>
<p>Recently, while building a laboratory information system, I needed to implement a flexible attribute system where each lab attribute could have multiple child attributes, creating a tree-like structure. For example, a "Blood Test" attribute might have child attributes like "Hemoglobin," "White Blood Cell Count," and "Platelet Count."</p>
<h2 id="heading-technical-implementation">Technical Implementation</h2>
<p>Let's break down the implementation into manageable steps and understand the underlying concepts.</p>
<h3 id="heading-step-1-database-migration">Step 1: Database Migration</h3>
<p>First, we need to set up our database structure. In Rails 8, we can generate a migration to add a self-referential foreign key:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># Terminal command</span>
rails generate migration AddParentToLabAttribute <span class="hljs-symbol">parent:</span>references

<span class="hljs-comment"># db/migrate/YYYYMMDDHHMMSS_add_parent_to_lab_attribute.rb</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AddParentToLabAttribute</span> &lt; ActiveRecord::Migration[8.0]</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change</span></span>
    add_reference <span class="hljs-symbol">:lab_attributes</span>, <span class="hljs-symbol">:parent</span>, 
                  <span class="hljs-symbol">foreign_key:</span> { <span class="hljs-symbol">to_table:</span> <span class="hljs-symbol">:lab_attributes</span> }
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>This migration adds a <code>parent_id</code> column to our <code>lab_attributes</code> table, which will reference another record in the same table. The <code>foreign_key</code> option explicitly tells Rails that this reference points back to the same table.</p>
<h3 id="heading-step-2-model-definition">Step 2: Model Definition</h3>
<p>The magic happens in our model definition. Here's how we set up the self-referential association:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># app/models/lab_attribute.rb</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LabAttribute</span> &lt; ApplicationRecord</span>
  <span class="hljs-comment"># Parent association</span>
  belongs_to <span class="hljs-symbol">:parent</span>, 
             <span class="hljs-symbol">class_name:</span> <span class="hljs-string">'LabAttribute'</span>, 
             <span class="hljs-symbol">optional:</span> <span class="hljs-literal">true</span>

  <span class="hljs-comment"># Children association</span>
  has_many <span class="hljs-symbol">:children</span>, 
           <span class="hljs-symbol">class_name:</span> <span class="hljs-string">'LabAttribute'</span>,
           <span class="hljs-symbol">foreign_key:</span> <span class="hljs-string">'parent_id'</span>,
           <span class="hljs-symbol">dependent:</span> <span class="hljs-symbol">:destroy</span>,
           <span class="hljs-symbol">inverse_of:</span> <span class="hljs-symbol">:parent</span>

  <span class="hljs-comment"># Validation to prevent circular references</span>
  validate <span class="hljs-symbol">:prevent_circular_reference</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">prevent_circular_reference</span></span>
    <span class="hljs-keyword">if</span> parent_id == id <span class="hljs-params">||</span> 
       (parent.present? &amp;&amp; parent.ancestor_ids.<span class="hljs-keyword">include</span>?(id))
      errors.add(<span class="hljs-symbol">:parent_id</span>, <span class="hljs-string">"cannot create circular reference"</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Let's break down the key components:</p>
<ol>
<li><p><code>belongs_to :parent</code> - Establishes the relationship to the parent attribute</p>
</li>
<li><p><code>optional: true</code> - Makes the parent association optional (root attributes don't have parents)</p>
</li>
<li><p><code>has_many :children</code> - Sets up the inverse relationship</p>
</li>
<li><p><code>dependent: :destroy</code> - Automatically deletes child attributes when the parent is deleted</p>
</li>
<li><p><code>inverse_of: :parent</code> - Helps Rails optimize memory usage and maintain consistency</p>
</li>
</ol>
<h3 id="heading-step-3-enhanced-functionality">Step 3: Enhanced Functionality</h3>
<p>Let's add some useful methods to work with our hierarchical structure:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># app/models/lab_attribute.rb</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LabAttribute</span> &lt; ApplicationRecord</span>
  <span class="hljs-comment"># Previous code...</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">root?</span></span>
    parent_id.<span class="hljs-literal">nil</span>?
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">leaf?</span></span>
    children.empty?
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">depth</span></span>
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> <span class="hljs-keyword">if</span> root?
    <span class="hljs-number">1</span> + parent.depth
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">ancestor_ids</span></span>
    <span class="hljs-keyword">return</span> [] <span class="hljs-keyword">if</span> root?
    [parent_id] + parent.ancestor_ids
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">ancestors</span></span>
    LabAttribute.where(<span class="hljs-symbol">id:</span> ancestor_ids)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">descendants</span></span>
    children.flat_map { <span class="hljs-params">|child|</span> [child] + child.descendants }
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-usage-examples">Usage Examples</h2>
<p>Here's how you can use this implementation in practice:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># Creating a hierarchy</span>
blood_test = LabAttribute.create!(<span class="hljs-symbol">name:</span> <span class="hljs-string">'Blood Test'</span>)
hemoglobin = blood_test.children.create!(<span class="hljs-symbol">name:</span> <span class="hljs-string">'Hemoglobin'</span>)
wbc = blood_test.children.create!(<span class="hljs-symbol">name:</span> <span class="hljs-string">'White Blood Cell Count'</span>)

<span class="hljs-comment"># Querying relationships</span>
puts blood_test.children.pluck(<span class="hljs-symbol">:name</span>)
<span class="hljs-comment"># =&gt; ["Hemoglobin", "White Blood Cell Count"]</span>

puts wbc.parent.name
<span class="hljs-comment"># =&gt; "Blood Test"</span>

puts hemoglobin.root?
<span class="hljs-comment"># =&gt; false</span>

puts blood_test.leaf?
<span class="hljs-comment"># =&gt; false</span>

puts wbc.depth
<span class="hljs-comment"># =&gt; 1</span>
</code></pre>
<h2 id="heading-performance-considerations">Performance Considerations</h2>
<p>When working with self-referential associations, keep these performance tips in mind:</p>
<ol>
<li>Use eager loading to avoid N+1 queries:</li>
</ol>
<pre><code class="lang-ruby">LabAttribute.includes(<span class="hljs-symbol">:children</span>, <span class="hljs-symbol">:parent</span>).where(<span class="hljs-symbol">parent_id:</span> <span class="hljs-literal">nil</span>)
</code></pre>
<ol start="2">
<li>Consider using counter caches for large hierarchies:</li>
</ol>
<pre><code class="lang-ruby">add_column <span class="hljs-symbol">:lab_attributes</span>, <span class="hljs-symbol">:children_count</span>, <span class="hljs-symbol">:integer</span>, <span class="hljs-symbol">default:</span> <span class="hljs-number">0</span>
</code></pre>
<ol start="3">
<li>For deep hierarchies, consider using closure tables or nested sets if you frequently need to query entire trees.</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Self-referential table inheritance is a powerful pattern for modeling hierarchical data in Rails applications. While this implementation focuses on lab attributes, the same pattern can be applied to any domain requiring hierarchical data structures.</p>
<p>Remember to:</p>
<ul>
<li><p>Validate against circular references</p>
</li>
<li><p>Consider the depth of your hierarchies</p>
</li>
<li><p>Use eager loading appropriately</p>
</li>
<li><p>Add indexes to foreign keys for better performance</p>
</li>
</ul>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Rails 8 CRUD: Modern Development Guide 2025]]></title><description><![CDATA[When our team decided to upgrade to Rails 8, we chose a more Rails-native approach using importmap for JavaScript management. This decision aligned perfectly with Rails' philosophy of convention over configuration, and I'm excited to share how this c...]]></description><link>https://sulmanweb.com/rails-8-modern-crud-development-guide</link><guid isPermaLink="true">https://sulmanweb.com/rails-8-modern-crud-development-guide</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[technology]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Mon, 13 Jan 2025 05:00:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736322525804/6aac2e5c-d24a-4dcd-9bf4-79df04815daf.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When our team decided to upgrade to Rails 8, we chose a more Rails-native approach using importmap for JavaScript management. This decision aligned perfectly with Rails' philosophy of convention over configuration, and I'm excited to share how this choice shaped our development experience.</p>
<h2 id="heading-initial-setup-and-modern-stack-choices">Initial Setup and Modern Stack Choices</h2>
<p>Let's start with setting up our Rails 8 project:</p>
<pre><code class="lang-bash">rails new modern_platform \
  --css tailwind \
  --database postgresql \
  --skip-test \
  --skip-system-test
</code></pre>
<p>Why no <code>--javascript</code> flag? Rails 8 comes with importmap by default, which I've found to be a game-changer for managing JavaScript dependencies. Here's how we configured our importmap:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># config/importmap.rb</span>
pin <span class="hljs-string">"@hotwired/turbo-rails"</span>, <span class="hljs-symbol">to:</span> <span class="hljs-string">"turbo.min.js"</span>
pin <span class="hljs-string">"@hotwired/stimulus"</span>, <span class="hljs-symbol">to:</span> <span class="hljs-string">"stimulus.min.js"</span>
pin <span class="hljs-string">"@hotwired/stimulus-loading"</span>, <span class="hljs-symbol">to:</span> <span class="hljs-string">"stimulus-loading.js"</span>

<span class="hljs-comment"># Third-party packages we're using</span>
pin <span class="hljs-string">"chart.js"</span>, <span class="hljs-symbol">to:</span> <span class="hljs-string">"https://ga.jspm.io/npm:chart.js@4.4.1/dist/chart.js"</span>
pin <span class="hljs-string">"@rails/request.js"</span>, <span class="hljs-symbol">to:</span> <span class="hljs-string">"https://ga.jspm.io/npm:@rails/request.js@0.0.9/src/index.js"</span>
pin <span class="hljs-string">"trix"</span>
pin <span class="hljs-string">"@rails/actiontext"</span>, <span class="hljs-symbol">to:</span> <span class="hljs-string">"actiontext.js"</span>

<span class="hljs-comment"># Local JavaScript modules</span>
pin_all_from <span class="hljs-string">"app/javascript/controllers"</span>, <span class="hljs-symbol">under:</span> <span class="hljs-string">"controllers"</span>
pin_all_from <span class="hljs-string">"app/javascript/components"</span>, <span class="hljs-symbol">under:</span> <span class="hljs-string">"components"</span>
</code></pre>
<h2 id="heading-modern-javascript-organization">Modern JavaScript Organization</h2>
<p>One of the benefits of importmap is how naturally it fits with module-based JavaScript. Here's how we structure our JavaScript:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// app/javascript/controllers/post_form_controller.js</span>
<span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>
<span class="hljs-keyword">import</span> { post } <span class="hljs-keyword">from</span> <span class="hljs-string">"@rails/request.js"</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  <span class="hljs-keyword">static</span> targets = [<span class="hljs-string">"form"</span>, <span class="hljs-string">"preview"</span>]
  <span class="hljs-keyword">static</span> values = {
    <span class="hljs-attr">previewUrl</span>: <span class="hljs-built_in">String</span>
  }

  <span class="hljs-keyword">async</span> preview() {
    <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData(<span class="hljs-built_in">this</span>.formTarget)

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> post(<span class="hljs-built_in">this</span>.previewUrlValue, {
        <span class="hljs-attr">body</span>: formData
      })

      <span class="hljs-keyword">if</span> (response.ok) {
        <span class="hljs-keyword">const</span> html = <span class="hljs-keyword">await</span> response.text
        <span class="hljs-built_in">this</span>.previewTarget.innerHTML = html
      }
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Preview failed:"</span>, error)
    }
  }
}
</code></pre>
<h2 id="heading-component-based-architecture">Component-Based Architecture</h2>
<p>We've embraced ViewComponent with Stimulus, creating a powerful combination for reusable UI components:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># app/components/rich_text_editor_component.rb</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RichTextEditorComponent</span> &lt; ViewComponent::Base</span>
  <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:form</span>, <span class="hljs-symbol">:field</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(<span class="hljs-symbol">form:</span>, <span class="hljs-symbol">field:</span>)</span></span>
    @form = form
    @field = field
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">stimulus_controller_options</span></span>
    {
      <span class="hljs-symbol">data:</span> {
        <span class="hljs-symbol">controller:</span> <span class="hljs-string">"rich-text-editor"</span>,
        <span class="hljs-symbol">rich_text_editor_toolbar_value:</span> toolbar_options.to_json
      }
    }
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">toolbar_options</span></span>
    {
      <span class="hljs-symbol">items:</span> [
        <span class="hljs-string">%w[bold italic underline strike]</span>,
        <span class="hljs-string">%w[heading-1 heading-2]</span>,
        <span class="hljs-string">%w[link code]</span>,
        <span class="hljs-string">%w[unordered-list ordered-list]</span>
      ]
    }
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-comment">&lt;!-- app/components/rich_text_editor_component.html.erb --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"rich-text-editor"</span> &lt;%=</span></span><span class="ruby"> stimulus_controller_options </span><span class="xml"><span class="hljs-tag">%&gt;</span>&gt;
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form.rich_text_area field,
    <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">prose</span> <span class="hljs-title">max</span>-<span class="hljs-title">w</span>-<span class="hljs-title">none</span>",</span>
    <span class="hljs-symbol">data:</span> { 
      <span class="hljs-symbol">rich_text_editor_target:</span> <span class="hljs-string">"editor"</span>,
      <span class="hljs-symbol">action:</span> <span class="hljs-string">"trix-change-&gt;rich-text-editor#onChange"</span>
    } </span><span class="xml"><span class="hljs-tag">%&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-2 text-sm text-gray-500"</span> 
       <span class="hljs-attr">data-rich-text-editor-target</span>=<span class="hljs-string">"counter"</span>&gt;</span>
    0 characters
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">// app/javascript/controllers/rich_text_editor_controller.js</span>
<span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>
<span class="hljs-keyword">import</span> Trix <span class="hljs-keyword">from</span> <span class="hljs-string">"trix"</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  <span class="hljs-keyword">static</span> targets = [<span class="hljs-string">"editor"</span>, <span class="hljs-string">"counter"</span>]
  <span class="hljs-keyword">static</span> values = { 
    <span class="hljs-attr">toolbar</span>: <span class="hljs-built_in">Object</span>,
    <span class="hljs-attr">maxLength</span>: <span class="hljs-built_in">Number</span> 
  }

  connect() {
    <span class="hljs-built_in">this</span>.setupToolbar()
    <span class="hljs-built_in">this</span>.updateCounter()
  }

  onChange() {
    <span class="hljs-built_in">this</span>.updateCounter()
  }

  updateCounter() {
    <span class="hljs-keyword">const</span> text = <span class="hljs-built_in">this</span>.editorTarget.value
    <span class="hljs-built_in">this</span>.counterTarget.textContent = 
      <span class="hljs-string">`<span class="hljs-subst">${text.length}</span> characters`</span>
  }

  setupToolbar() {
    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.hasToolbarValue) <span class="hljs-keyword">return</span>

    <span class="hljs-keyword">const</span> toolbar = <span class="hljs-built_in">this</span>.editorTarget
      .querySelector(<span class="hljs-string">"trix-toolbar"</span>)

    <span class="hljs-comment">// Customize toolbar based on configuration</span>
    <span class="hljs-built_in">this</span>.toolbarValue.items.forEach(<span class="hljs-function"><span class="hljs-params">group</span> =&gt;</span> {
      <span class="hljs-comment">// Toolbar customization logic</span>
    })
  }
}
</code></pre>
<h2 id="heading-chartjs-integration-with-importmap">Chart.js Integration with Importmap</h2>
<p>Here's how we handle data visualization using Chart.js through importmap:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// app/javascript/controllers/analytics_chart_controller.js</span>
<span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>
<span class="hljs-keyword">import</span> { Chart } <span class="hljs-keyword">from</span> <span class="hljs-string">"chart.js"</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  <span class="hljs-keyword">static</span> values = {
    <span class="hljs-attr">data</span>: <span class="hljs-built_in">Object</span>,
    <span class="hljs-attr">options</span>: <span class="hljs-built_in">Object</span>
  }

  connect() {
    <span class="hljs-built_in">this</span>.initializeChart()
  }

  initializeChart() {
    <span class="hljs-keyword">const</span> ctx = <span class="hljs-built_in">this</span>.element.getContext(<span class="hljs-string">"2d"</span>)

    <span class="hljs-keyword">new</span> Chart(ctx, {
      <span class="hljs-attr">type</span>: <span class="hljs-string">"line"</span>,
      <span class="hljs-attr">data</span>: <span class="hljs-built_in">this</span>.dataValue,
      <span class="hljs-attr">options</span>: {
        ...this.defaultOptions,
        ...this.optionsValue
      }
    })
  }

  <span class="hljs-keyword">get</span> <span class="hljs-title">defaultOptions</span>() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">responsive</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">maintainAspectRatio</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">plugins</span>: {
        <span class="hljs-attr">legend</span>: {
          <span class="hljs-attr">position</span>: <span class="hljs-string">"bottom"</span>
        }
      }
    }
  }
}
</code></pre>
<h2 id="heading-performance-optimizations-with-http2">Performance Optimizations with HTTP/2</h2>
<p>One advantage of importmap is its excellent performance with HTTP/2. Here's how we optimize our asset delivery:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># config/environments/production.rb</span>
Rails.application.configure <span class="hljs-keyword">do</span>
  <span class="hljs-comment"># Use CDN for importmapped JavaScript</span>
  config.action_controller.asset_host = ENV[<span class="hljs-string">"ASSET_HOST"</span>]

  <span class="hljs-comment"># Enable HTTP/2 Early Hints</span>
  config.action_dispatch.early_hints = <span class="hljs-literal">true</span>

  <span class="hljs-comment"># Configure importmap hosts</span>
  config.importmap.cache_sweepers &lt;&lt; Rails.root.join(<span class="hljs-string">"app/javascript"</span>)

  <span class="hljs-comment"># Preload critical JavaScript</span>
  config.action_view.preload_links_header = <span class="hljs-literal">true</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-testing-javascript-components">Testing JavaScript Components</h2>
<p>We use Capybara with Cuprite for JavaScript testing:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># spec/system/posts_spec.rb</span>
RSpec.describe <span class="hljs-string">"Posts"</span>, <span class="hljs-symbol">type:</span> <span class="hljs-symbol">:system</span> <span class="hljs-keyword">do</span>
  before <span class="hljs-keyword">do</span>
    driven_by(<span class="hljs-symbol">:cuprite</span>)
  <span class="hljs-keyword">end</span>

  it <span class="hljs-string">"previews post content"</span>, <span class="hljs-symbol">js:</span> <span class="hljs-literal">true</span> <span class="hljs-keyword">do</span>
    visit new_post_path

    find(<span class="hljs-string">"[data-controller='post-form']"</span>).tap <span class="hljs-keyword">do</span> <span class="hljs-params">|form|</span>
      form.fill_in <span class="hljs-string">"Content"</span>, <span class="hljs-symbol">with:</span> <span class="hljs-string">"**Bold text**"</span>
      form.click_button <span class="hljs-string">"Preview"</span>

      expect(form).to have_css(
        <span class="hljs-string">".preview strong"</span>, 
        <span class="hljs-symbol">text:</span> <span class="hljs-string">"Bold text"</span>
      )
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-deployment-considerations">Deployment Considerations</h2>
<p>Our production setup leverages HTTP/2 and CDN caching:</p>
<pre><code class="lang-nginx"><span class="hljs-comment"># nginx.conf</span>
<span class="hljs-section">server</span> {
  <span class="hljs-attribute">listen</span> <span class="hljs-number">443</span> ssl http2;

  <span class="hljs-comment"># Enable asset caching</span>
  <span class="hljs-attribute">location</span> /assets/ {
    <span class="hljs-attribute">expires</span> max;
    <span class="hljs-attribute">add_header</span> Cache-Control public;
  }

  <span class="hljs-comment"># Early hints for importmapped JavaScript</span>
  <span class="hljs-attribute">location</span> / {
    <span class="hljs-attribute">proxy_pass</span> http://backend;
    <span class="hljs-attribute">http2_push_preload</span> <span class="hljs-literal">on</span>;
  }
}
</code></pre>
<h2 id="heading-parallel-query-execution-a-game-changer">Parallel Query Execution: A Game-Changer</h2>
<p>One of the most exciting features we discovered in Rails 8 was parallel query execution. During our performance optimization sprint, this became a crucial tool for handling complex dashboard pages:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># app/controllers/dashboards_controller.rb</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DashboardsController</span> &lt; ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">show</span></span>
    <span class="hljs-comment"># Execute multiple queries asynchronously</span>
    posts_future = fetch_recent_posts.load_async
    comments_future = fetch_pending_comments.load_async
    analytics_future = ActiveRecord::Base.async_exec { fetch_analytics_data }
    notifications_future = fetch_user_notifications.load_async

    <span class="hljs-comment"># Resolve the futures</span>
    posts = posts_future.value
    comments = comments_future.value
    analytics = analytics_future.value
    notifications = notifications_future.value

    respond_to <span class="hljs-keyword">do</span> <span class="hljs-params">|format|</span>
      format.html <span class="hljs-keyword">do</span>
        render <span class="hljs-symbol">locals:</span> {
          <span class="hljs-symbol">posts:</span> posts,
          <span class="hljs-symbol">comments:</span> comments,
          <span class="hljs-symbol">analytics:</span> analytics,
          <span class="hljs-symbol">notifications:</span> notifications
        }
      <span class="hljs-keyword">end</span>

      format.turbo_stream <span class="hljs-keyword">do</span>
        render <span class="hljs-symbol">turbo_stream:</span> [
          turbo_stream.update(<span class="hljs-string">"dashboard-posts"</span>, <span class="hljs-symbol">partial:</span> <span class="hljs-string">"posts/list"</span>, <span class="hljs-symbol">locals:</span> { <span class="hljs-symbol">posts:</span> posts }),
          turbo_stream.update(<span class="hljs-string">"dashboard-analytics"</span>, <span class="hljs-symbol">partial:</span> <span class="hljs-string">"analytics/summary"</span>, <span class="hljs-symbol">locals:</span> { <span class="hljs-symbol">data:</span> analytics })
        ]
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_recent_posts</span></span>
    Post.visible_to(current_user)
        .includes(<span class="hljs-symbol">:author</span>, <span class="hljs-symbol">:categories</span>)
        .order(<span class="hljs-symbol">published_at:</span> <span class="hljs-symbol">:desc</span>)
        .limit(<span class="hljs-number">10</span>)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_pending_comments</span></span>
    Comment.pending_review
           .includes(<span class="hljs-symbol">:post</span>, <span class="hljs-symbol">:author</span>)
           .where(<span class="hljs-symbol">post:</span> { <span class="hljs-symbol">author_id:</span> current_user.id })
           .limit(<span class="hljs-number">15</span>)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_analytics_data</span></span>
    AnalyticsService.fetch_dashboard_metrics(
      <span class="hljs-symbol">user:</span> current_user,
      <span class="hljs-symbol">range:</span> <span class="hljs-number">30</span>.days.ago..Time.current
    )
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_user_notifications</span></span>
    current_user.notifications
               .unread
               .includes(<span class="hljs-symbol">:notifiable</span>)
               .limit(<span class="hljs-number">5</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>To make this even more powerful, we integrated it with Stimulus for real-time updates:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// app/javascript/controllers/dashboard_controller.js</span>
<span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>
<span class="hljs-keyword">import</span> { Chart } <span class="hljs-keyword">from</span> <span class="hljs-string">"chart.js"</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  <span class="hljs-keyword">static</span> targets = [<span class="hljs-string">"analytics"</span>, <span class="hljs-string">"posts"</span>]

  connect() {
    <span class="hljs-built_in">this</span>.initializeCharts()
    <span class="hljs-built_in">this</span>.startRefreshTimer()
  }

  disconnect() {
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.refreshTimer) {
      <span class="hljs-built_in">clearInterval</span>(<span class="hljs-built_in">this</span>.refreshTimer)
    }
  }

  <span class="hljs-keyword">async</span> refresh() {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-built_in">this</span>.element.dataset.refreshUrl, {
        <span class="hljs-attr">headers</span>: {
          <span class="hljs-attr">Accept</span>: <span class="hljs-string">"text/vnd.turbo-stream.html"</span>
        }
      })

      <span class="hljs-keyword">if</span> (response.ok) {
        Turbo.renderStreamMessage(<span class="hljs-keyword">await</span> response.text())
      }
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Dashboard refresh failed:"</span>, error)
    }
  }

  startRefreshTimer() {
    <span class="hljs-built_in">this</span>.refreshTimer = <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-built_in">this</span>.refresh()
    }, <span class="hljs-number">30000</span>) <span class="hljs-comment">// Refresh every 30 seconds</span>
  }

  initializeCharts() {
    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.hasAnalyticsTarget) <span class="hljs-keyword">return</span>

    <span class="hljs-keyword">const</span> data = <span class="hljs-built_in">JSON</span>.parse(<span class="hljs-built_in">this</span>.analyticsTarget.dataset.metrics)
    <span class="hljs-built_in">this</span>.createAnalyticsChart(data)
  }

  createAnalyticsChart(data) {
    <span class="hljs-keyword">const</span> ctx = <span class="hljs-built_in">this</span>.analyticsTarget.getContext(<span class="hljs-string">"2d"</span>)

    <span class="hljs-keyword">new</span> Chart(ctx, {
      <span class="hljs-attr">type</span>: <span class="hljs-string">"line"</span>,
      <span class="hljs-attr">data</span>: data,
      <span class="hljs-attr">options</span>: {
        <span class="hljs-attr">responsive</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">maintainAspectRatio</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">animations</span>: {
          <span class="hljs-attr">tension</span>: {
            <span class="hljs-attr">duration</span>: <span class="hljs-number">1000</span>,
            <span class="hljs-attr">easing</span>: <span class="hljs-string">'linear'</span>
          }
        }
      }
    })
  }
}
</code></pre>
<p>The combination of parallel queries and Turbo Streams gave us impressive performance improvements:</p>
<ol>
<li><p>Dashboard load times dropped by 47%</p>
</li>
<li><p>Database connection usage became more efficient</p>
</li>
<li><p>Real-time updates felt smoother with optimistic UI updates</p>
</li>
</ol>
<h2 id="heading-learning-journey-and-trade-offs">Learning Journey and Trade-offs</h2>
<p>Moving to importmap wasn't without challenges. Here's what we learned:</p>
<ol>
<li><p><strong>Simplified Dependency Management</strong>: No more yarn/npm complexity</p>
</li>
<li><p><strong>Better Caching</strong>: HTTP/2 multiplexing improved load times</p>
</li>
<li><p><strong>Module Patterns</strong>: Encouraged cleaner JavaScript organization</p>
</li>
<li><p><strong>Development Experience</strong>: Faster feedback loop without build steps</p>
</li>
</ol>
<h2 id="heading-looking-forward">Looking Forward</h2>
<p>Rails 8 with importmap has transformed our development workflow. The native integration with Hotwire and Stimulus, combined with HTTP/2 optimizations, has given us a modern, maintainable, and performant application stack.</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Deploying Rails 8 Applications: A Complete Guide with Docker, Kamal, and Cloudflare]]></title><description><![CDATA[Introduction
When I first started deploying Rails applications, the process felt overwhelming. Today, I'm excited to share a complete guide that transforms this complexity into a straightforward process. Let's dive into deploying a Rails 8 app using ...]]></description><link>https://sulmanweb.com/deploy-rails-8-docker-kamal-production-guide</link><guid isPermaLink="true">https://sulmanweb.com/deploy-rails-8-docker-kamal-production-guide</guid><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Thu, 09 Jan 2025 05:00:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735712782466/ef374c6d-72ed-44fa-9bdd-de6df7a92edb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>When I first started deploying Rails applications, the process felt overwhelming. Today, I'm excited to share a complete guide that transforms this complexity into a straightforward process. Let's dive into deploying a Rails 8 app using Docker, with PostgreSQL containerization, all orchestrated by Kamal on a Hetzner server.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we begin our deployment journey, you'll need:</p>
<ul>
<li><p>A Rails 8 application ready for production</p>
</li>
<li><p>Docker installed locally</p>
</li>
<li><p>A Hetzner account</p>
</li>
<li><p>Domain name and Cloudflare account</p>
</li>
<li><p>1Password account (for secrets management)</p>
</li>
<li><p>Basic SSH knowledge</p>
</li>
</ul>
<h2 id="heading-step-1-server-and-dns-configuration">Step 1: Server and DNS Configuration</h2>
<h3 id="heading-hetzner-server-setup">Hetzner Server Setup</h3>
<p>Let's start by creating our production environment:</p>
<ol>
<li><p>Create a Hetzner server with these specifications:</p>
<pre><code class="lang-plaintext"> - Ubuntu 24.04
 - ARM64 (Ampere) CPU
 - CAX11 size (or choose based on your needs)
 - IPv4 only configuration
 - Your SSH keys added
</code></pre>
</li>
</ol>
<h3 id="heading-cloudflare-configuration">Cloudflare Configuration</h3>
<p>Set up your domain with proper DNS and security settings:</p>
<ol>
<li><p>DNS Configuration:</p>
<pre><code class="lang-plaintext"> - Add A record: @ → your-server-ip (Proxy enabled)
 - Add CNAME: www → @ (Proxy enabled)
</code></pre>
</li>
<li><p>SSL/TLS Settings:</p>
<pre><code class="lang-plaintext"> - Edge Certificates: Enable "Always use HTTPS"
 - Overview: Set SSL/TLS mode to "Full"
</code></pre>
</li>
<li><p>Page Rules for www to root redirect:</p>
<pre><code class="lang-plaintext"> URL: https://www.yourdomain.com/*
 Setting: Forwarding URL
 Status Code: 301 - Permanent Redirect
 Destination URL: https://yourdomain.com/$1
</code></pre>
</li>
</ol>
<h2 id="heading-step-2-secrets-management">Step 2: Secrets Management</h2>
<p>I've learned that proper secrets management is crucial. Let's set it up with 1Password:</p>
<ol>
<li><p>Create a secure note in 1Password with these credentials:</p>
<pre><code class="lang-plaintext"> KAMAL_REGISTRY_PASSWORD: your-docker-registry-password
 RAILS_MASTER_KEY: your-rails-master-key
 POSTGRES_PASSWORD: your-postgres-password
</code></pre>
</li>
<li><p>Update <code>.kamal/secrets</code> to handle 1Password integration:</p>
<pre><code class="lang-bash"> SECRETS=$(kamal secrets fetch --adapter 1password --account YOUR_1PASSWORD_ACCOUNT_ID --from YOUR_VAULT_NAME/YOU_SECURE_NOTE KAMAL_REGISTRY_PASSWORD RAILS_PRODUCTION_KEY POSTGRES_PASSWORD)

 KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD <span class="hljs-variable">$SECRETS</span>)
 RAILS_MASTER_KEY=$(kamal secrets extract RAILS_PRODUCTION_KEY <span class="hljs-variable">$SECRETS</span>)
 POSTGRES_PASSWORD=$(kamal secrets extract POSTGRES_PASSWORD <span class="hljs-variable">$SECRETS</span>)
</code></pre>
</li>
</ol>
<p>For more info visit <a target="_blank" href="https://kamal-deploy.org/docs/commands/secrets/">Kamal Secrets</a>.</p>
<h2 id="heading-step-3-database-configuration">Step 3: Database Configuration</h2>
<h3 id="heading-postgresql-setup">PostgreSQL Setup</h3>
<ol>
<li><p>Create <code>config/init.sql</code> for database initialization:</p>
<pre><code class="lang-sql"> <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> <span class="hljs-string">`your_app_production`</span>;
 <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> <span class="hljs-string">`your_app_production_cache`</span>;
 <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> <span class="hljs-string">`your_app_production_queue`</span>;
 <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> <span class="hljs-string">`your_app_production_cable`</span>;
</code></pre>
</li>
<li><p>Configure PostgreSQL accessory in <code>config/deploy.yml</code>:</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">accessories:</span>
   <span class="hljs-attr">postgres:</span>
     <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:16-alpine</span>
     <span class="hljs-attr">host:</span> <span class="hljs-string">your-server-ip</span>
     <span class="hljs-attr">port:</span> <span class="hljs-number">5432</span>
     <span class="hljs-attr">options:</span>
       <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
     <span class="hljs-attr">env:</span>
       <span class="hljs-attr">clear:</span>
         <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">postgres</span>
         <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">your_app_production</span>
       <span class="hljs-attr">secret:</span>
         <span class="hljs-bullet">-</span> <span class="hljs-string">POSTGRES_PASSWORD</span>
     <span class="hljs-attr">files:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">config/init.sql:/docker-entrypoint-initdb.d/init.sql</span>
     <span class="hljs-attr">volumes:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">/var/lib/postgresql/your_app_production:/var/lib/postgresql/data</span>
</code></pre>
</li>
<li><p>Update <code>config/database.yml</code> for production:</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">production:</span>
     <span class="hljs-attr">primary:</span> <span class="hljs-meta">&amp;primary_production</span>
         <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-meta">*default</span>
         <span class="hljs-attr">host:</span> &lt;%=<span class="ruby"> ENV[<span class="hljs-string">"DB_HOST"</span>] </span>%&gt;
         <span class="hljs-attr">database:</span> <span class="hljs-string">your_app_production</span>
         <span class="hljs-attr">username:</span> <span class="hljs-string">postgres</span>
         <span class="hljs-attr">password:</span> &lt;%=<span class="ruby"> ENV[<span class="hljs-string">"POSTGRES_PASSWORD"</span>] </span>%&gt;
     <span class="hljs-attr">cache:</span>
         <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-meta">*primary_production</span>
         <span class="hljs-attr">database:</span> <span class="hljs-string">your_app_production_cache</span>
         <span class="hljs-attr">migrations_paths:</span> <span class="hljs-string">db/cache_migrate</span>
     <span class="hljs-attr">queue:</span>
         <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-string">*primary_production</span> 
         <span class="hljs-attr">database:</span> <span class="hljs-string">your_app_production_queue</span>
         <span class="hljs-attr">migrations_paths:</span> <span class="hljs-string">db/queue_migrate</span>
     <span class="hljs-attr">cable:</span> 
         <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-meta">*primary_production</span>
         <span class="hljs-attr">database:</span> <span class="hljs-string">your_app_production_cable</span>
         <span class="hljs-attr">migrations_paths:</span> <span class="hljs-string">db/cable_migrate</span>
</code></pre>
</li>
</ol>
<h2 id="heading-step-4-production-environment-configuration">Step 4: Production Environment Configuration</h2>
<p>Update <code>config/environments/production.rb</code> with these essential settings:</p>
<pre><code class="lang-ruby">Rails.application.configure <span class="hljs-keyword">do</span>
  <span class="hljs-comment"># Asset handling</span>
  config.require_master_key = <span class="hljs-literal">false</span>
  config.assets.css_compressor = <span class="hljs-literal">nil</span>
  config.assets.compile = <span class="hljs-literal">false</span>
  config.public_file_server.enabled = <span class="hljs-literal">true</span>
  config.asset_host = <span class="hljs-string">"https://yourdomain.com"</span>
  config.assets.enabled = <span class="hljs-literal">true</span>
  config.assets.version = <span class="hljs-string">"1.0"</span>

  <span class="hljs-comment"># SSL and security</span>
  config.ssl_options = { 
    <span class="hljs-symbol">redirect:</span> { 
      <span class="hljs-symbol">exclude:</span> -&gt;(request) { request.path == <span class="hljs-string">"/up"</span> } 
    } 
  }

  <span class="hljs-comment"># Health checks</span>
  config.silence_healthcheck_path = <span class="hljs-string">"/up"</span>

  <span class="hljs-comment"># Domain configuration</span>
  config.hosts = [
    <span class="hljs-string">"yourdomain.com"</span>,
    <span class="hljs-regexp">/.*\.yourdomain\.com/</span>
  ]

  config.host_authorization = { 
    <span class="hljs-symbol">exclude:</span> -&gt;(request) { request.path == <span class="hljs-string">"/up"</span> } 
  }

  <span class="hljs-comment"># URL options</span>
  config.action_mailer.default_url_options = { 
    <span class="hljs-symbol">host:</span> <span class="hljs-string">"yourdomain.com"</span>, 
    <span class="hljs-symbol">protocol:</span> <span class="hljs-string">"https"</span> 
  }

  routes.default_url_options = {
    <span class="hljs-symbol">host:</span> <span class="hljs-string">"yourdomain.com"</span>,
    <span class="hljs-symbol">protocol:</span> <span class="hljs-string">"https"</span>
  }
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-step-5-kamal-deployment-setup">Step 5: Kamal Deployment Setup</h2>
<p>Kamal, Rails 8's built-in deployment tool, makes containerized deployment straightforward:</p>
<ol>
<li>Update rest of <code>config/deploy.yml</code>:</li>
</ol>
<pre><code class="lang-yaml"><span class="hljs-attr">service:</span> <span class="hljs-string">your_app_name</span>

<span class="hljs-attr">registry:</span>
  <span class="hljs-attr">username:</span> <span class="hljs-string">your_dockerhub_username</span>
  <span class="hljs-attr">password:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">KAMAL_REGISTRY_PASSWORD</span>

<span class="hljs-attr">aliases:</span>
    <span class="hljs-attr">console:</span> <span class="hljs-string">app</span> <span class="hljs-string">exec</span> <span class="hljs-string">--interactive</span> <span class="hljs-string">--reuse</span> <span class="hljs-string">"bin/rails console"</span>
    <span class="hljs-attr">shell:</span> <span class="hljs-string">app</span> <span class="hljs-string">exec</span> <span class="hljs-string">--interactive</span> <span class="hljs-string">--reuse</span> <span class="hljs-string">"bash"</span>
    <span class="hljs-attr">logs:</span> <span class="hljs-string">app</span> <span class="hljs-string">logs</span> <span class="hljs-string">-f</span>
    <span class="hljs-attr">dbc:</span> <span class="hljs-string">app</span> <span class="hljs-string">exec</span> <span class="hljs-string">--interactive</span> <span class="hljs-string">--reuse</span> <span class="hljs-string">"bin/rails dbconsole"</span>

<span class="hljs-attr">image:</span> <span class="hljs-string">your_dockerhub_username/your_app_name</span>

<span class="hljs-attr">builder:</span>
    <span class="hljs-attr">arch:</span> <span class="hljs-string">arm64</span>

<span class="hljs-attr">proxy:</span>
  <span class="hljs-attr">ssl:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">host:</span> <span class="hljs-string">yourdomain.com</span>

<span class="hljs-attr">servers:</span>
  <span class="hljs-attr">web:</span>
    <span class="hljs-attr">hosts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">your_server_ip</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">your_app_storage:/rails/storage:rw</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">/tmp/storage:/rails/tmp/storage:rw</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">your_app_data:/data</span>

<span class="hljs-attr">env:</span>
  <span class="hljs-attr">secret:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">RAILS_MASTER_KEY</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">POSTGRES_PASSWORD</span>
  <span class="hljs-attr">clear:</span>
    <span class="hljs-attr">HOST:</span> <span class="hljs-string">yourdomain.com</span>
    <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">production</span>
    <span class="hljs-attr">DB_HOST:</span> <span class="hljs-string">your_app-postgres</span>
    <span class="hljs-attr">SOLID_QUEUE_IN_PUMA:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">RAILS_SERVE_STATIC_FILES:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">RAILS_LOG_TO_STDOUT:</span> <span class="hljs-literal">true</span>
</code></pre>
<h2 id="heading-step-6-deployment">Step 6: Deployment</h2>
<p>Now for the exciting part - deploying our application:</p>
<ol>
<li><p>Initial setup:</p>
<pre><code class="lang-bash"> bin/kamal setup
</code></pre>
</li>
<li><p>Watch the logs to ensure everything starts correctly:</p>
<pre><code class="lang-bash"> bin/kamal logs
</code></pre>
</li>
<li><p>For subsequent deployments:</p>
<pre><code class="lang-bash"> bin/kamal deploy
</code></pre>
</li>
</ol>
<h2 id="heading-deployment-verification-checklist">Deployment Verification Checklist</h2>
<p>After deployment, I always verify these key points:</p>
<ul>
<li><p>[ ] Health check endpoint (<code>/up</code>) responds</p>
</li>
<li><p>[ ] Database migrations completed successfully</p>
</li>
<li><p>[ ] Assets are serving correctly</p>
</li>
<li><p>[ ] SSL certificate is valid</p>
</li>
<li><p>[ ] www to root domain redirect works</p>
</li>
<li><p>[ ] PostgreSQL container is running and accessible</p>
</li>
</ul>
<h2 id="heading-troubleshooting-tips">Troubleshooting Tips</h2>
<p>From my experience, here are some common issues and solutions:</p>
<ol>
<li><p>Database Connection Issues:</p>
<pre><code class="lang-bash"> bin/kamal dbc
</code></pre>
</li>
<li><p>Container Inspection:</p>
<pre><code class="lang-bash"> bin/kamal shell
</code></pre>
</li>
<li><p>PostgreSQL Logs:</p>
<pre><code class="lang-bash"> bin/kamal accessory logs postgres
</code></pre>
</li>
</ol>
<p>For more info go to <a target="_blank" href="https://kamal-deploy.org/">kamal-deploy</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Deploying a Rails 8 application might seem daunting at first, but with this structured approach, it becomes a manageable and repeatable process. I've learned that proper configuration of each component - from DNS to secrets management to database setup - is crucial for a robust production deployment.</p>
<p>Remember to always test your deployment in a staging environment first, and keep your secrets secure using proper management tools like 1Password.</p>
<p>Have you deployed a Rails application using this approach? I'd love to hear about your experience in the comments below! 🚀</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Rails GraphQL Auth - JWT, Email & Security]]></title><description><![CDATA[Building a secure and scalable authentication system is crucial for modern web applications. Today, I'll share insights from implementing a robust GraphQL authentication system in Rails, with detailed explanations of each component.
🏗️ Architecture ...]]></description><link>https://sulmanweb.com/rails-graphql-authentication-jwt-email-security</link><guid isPermaLink="true">https://sulmanweb.com/rails-graphql-authentication-jwt-email-security</guid><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[JWT]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Mon, 06 Jan 2025 05:00:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735612400925/7a51df9f-3a13-4262-baa0-52cd1444cfec.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building a secure and scalable authentication system is crucial for modern web applications. Today, I'll share insights from implementing a robust GraphQL authentication system in Rails, with detailed explanations of each component.</p>
<h2 id="heading-architecture-overview">🏗️ Architecture Overview</h2>
<p>The authentication system consists of three main components:</p>
<ol>
<li>JWT-based token authentication</li>
<li>Email verification workflow</li>
<li>Secure password management</li>
</ol>
<p>Let's examine each component in detail.</p>
<h2 id="heading-jwt-authentication-implementation">🔑 JWT Authentication Implementation</h2>
<h3 id="heading-token-generation-and-management">Token Generation and Management</h3>
<p>The core of our authentication relies on JWT tokens. Let's break down the key components:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> &lt; ApplicationRecord</span>
  <span class="hljs-comment"># Token generation for different purposes with specific lifetimes</span>
  generates_token_for <span class="hljs-symbol">:auth_token</span>, <span class="hljs-symbol">expires_in:</span> <span class="hljs-number">1</span>.week
  generates_token_for <span class="hljs-symbol">:email_confirmation</span>, <span class="hljs-symbol">expires_in:</span> <span class="hljs-number">8</span>.hours
  generates_token_for <span class="hljs-symbol">:password_reset</span>, <span class="hljs-symbol">expires_in:</span> <span class="hljs-number">1</span>.hour
<span class="hljs-keyword">end</span>
</code></pre>
<p>This code uses a custom token generation system where:</p>
<ul>
<li><code>generates_token_for</code> is a macro that sets up token generation for specific purposes</li>
<li>Each token type has its own expiration time</li>
<li>Tokens are bound to specific user data for verification</li>
</ul>
<p>For example, when we call <code>user.generate_token_for(:auth_token)</code>, it:</p>
<ol>
<li>Creates a JWT token with user-specific claims</li>
<li>Sets an expiration time (1 week for auth tokens)</li>
<li>Signs the token with the application's secret key</li>
<li>Returns the encoded token for client use</li>
</ol>
<h3 id="heading-authentication-service">Authentication Service</h3>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Users</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SignInService</span> &lt; ApplicationService</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span>
      <span class="hljs-keyword">return</span> failure([USER_NOT_FOUND_MESSAGE]) <span class="hljs-keyword">unless</span> user

      <span class="hljs-comment"># Generate authentication token</span>
      token = user.generate_token_for(<span class="hljs-symbol">:auth_token</span>)

      <span class="hljs-comment"># Log the authentication event</span>
      log_event(<span class="hljs-symbol">user:</span>, <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">username:</span> user.username })

      <span class="hljs-comment"># Return success response with token and user data</span>
      success({ <span class="hljs-symbol">token:</span>, <span class="hljs-symbol">user:</span> })
    <span class="hljs-keyword">end</span>

    private

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">user</span></span>
      <span class="hljs-comment"># Authenticate user using credentials</span>
      @user <span class="hljs-params">||</span>= User.authenticate_by(permitted_params)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Key aspects of the service:</p>
<ol>
<li>Validates user credentials</li>
<li>Generates an authentication token</li>
<li>Logs the authentication event</li>
<li>Returns a structured response</li>
</ol>
<h3 id="heading-graphql-authentication-mutation">GraphQL Authentication Mutation</h3>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Mutations</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserSignIn</span> &lt; BaseMutationWithErrors</span>
    <span class="hljs-comment"># Define required input arguments</span>
    argument <span class="hljs-symbol">:password</span>, String, <span class="hljs-symbol">required:</span> <span class="hljs-literal">true</span>
    argument <span class="hljs-symbol">:username</span>, String, <span class="hljs-symbol">required:</span> <span class="hljs-literal">true</span>

    <span class="hljs-comment"># Define return fields</span>
    field <span class="hljs-symbol">:token</span>, String, <span class="hljs-symbol">null:</span> <span class="hljs-literal">true</span>
    field <span class="hljs-symbol">:user</span>, Types::UserType, <span class="hljs-symbol">null:</span> <span class="hljs-literal">true</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">resolve</span><span class="hljs-params">(**args)</span></span>
      result = Users::SignInService.call(args)
      {
        <span class="hljs-symbol">errors:</span> result.errors,
        <span class="hljs-symbol">success:</span> result.success?,
        <span class="hljs-symbol">token:</span> result.success? ? result.data[<span class="hljs-symbol">:token</span>] : <span class="hljs-literal">nil</span>,
        <span class="hljs-symbol">user:</span> result.success? ? result.data[<span class="hljs-symbol">:user</span>] : <span class="hljs-literal">nil</span>
      }
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>This mutation:</p>
<ol>
<li>Accepts username and password</li>
<li>Calls the authentication service</li>
<li>Returns token and user data on success</li>
<li>Handles errors gracefully</li>
</ol>
<h2 id="heading-email-verification-system">📧 Email Verification System</h2>
<h3 id="heading-confirmation-service-implementation">Confirmation Service Implementation</h3>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Users</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SendConfirmationEmailService</span> &lt; ApplicationService</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span>
      <span class="hljs-keyword">return</span> failure([USER_NOT_FOUND_ERROR]) <span class="hljs-keyword">unless</span> user
      <span class="hljs-keyword">return</span> failure([USER_ALREADY_CONFIRMED_ERROR]) <span class="hljs-keyword">if</span> user.confirmed?

      send_confirmation_email
      log_event(<span class="hljs-symbol">user:</span>, <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">confirmation_sent:</span> <span class="hljs-literal">true</span> })
      success(CONFIRMATION_SENT_MSG)
    <span class="hljs-keyword">end</span>

    private

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_confirmation_email</span></span>
      <span class="hljs-comment"># Generate confirmation email with secure token</span>
      email = Email.create_confirmation_email!(<span class="hljs-symbol">user:</span>)
      send_email(email)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The confirmation flow:</p>
<ol>
<li>Checks user existence and confirmation status</li>
<li>Generates a secure confirmation token</li>
<li>Creates and sends confirmation email</li>
<li>Logs the confirmation attempt</li>
</ol>
<h3 id="heading-email-token-generation">Email Token Generation</h3>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Email</span> &lt; ApplicationRecord</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">self</span>.<span class="hljs-title">create_confirmation_email!</span><span class="hljs-params">(<span class="hljs-symbol">user:</span>)</span></span>
    token = user.generate_token_for(<span class="hljs-symbol">:email_confirmation</span>)
    create!(
      <span class="hljs-symbol">to_emails:</span> [user.email],
      <span class="hljs-symbol">template_id:</span> Rails.application.credentials.dig(<span class="hljs-symbol">:sendgrid</span>, <span class="hljs-symbol">:confirm_template_id</span>),
      <span class="hljs-symbol">substitutions:</span> [{ 
        <span class="hljs-string">"confirmation_url"</span>: <span class="hljs-string">"<span class="hljs-subst">#{Settings.emails.confirm_url}</span>?token=<span class="hljs-subst">#{token}</span>"</span>,
        <span class="hljs-symbol">name:</span> user.name 
      }]
    )
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>This creates a confirmation email with:</p>
<ol>
<li>A secure, time-limited token</li>
<li>A personalized confirmation URL</li>
<li>User-specific template data</li>
</ol>
<h2 id="heading-security-implementation">🔒 Security Implementation</h2>
<h3 id="heading-authentication-middleware">Authentication Middleware</h3>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Queries</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseQuery</span> &lt; GraphQL::Schema::<span class="hljs-title">Resolver</span></span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">authenticate_user!</span></span>
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> current_user

      raise GraphQL::ExecutionError.new(
        I18n.t(<span class="hljs-string">'gql.errors.not_authenticated'</span>),
        <span class="hljs-symbol">extensions:</span> { <span class="hljs-symbol">code:</span> <span class="hljs-string">'AUTHENTICATION_ERROR'</span> }
      )
    <span class="hljs-keyword">end</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">current_user</span></span>
      context[<span class="hljs-symbol">:current_user</span>] <span class="hljs-params">||</span> Current.user
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>This middleware:</p>
<ol>
<li>Verifies token presence and validity</li>
<li>Maintains user context throughout requests</li>
<li>Handles authentication errors consistently</li>
<li>Provides access to current user data</li>
</ol>
<h3 id="heading-password-security">Password Security</h3>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> &lt; ApplicationRecord</span>
  <span class="hljs-comment"># Regular expression for password validation</span>
  PASSWORD_FORMAT = <span class="hljs-regexp">/\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&amp;*(),.?":{}|&lt;&gt;])[A-Za-z\d!@#$%^&amp;*(),.?":{}|&lt;&gt;]{8,72}\z/</span>

  validates <span class="hljs-symbol">:password</span>, 
    <span class="hljs-symbol">presence:</span> <span class="hljs-literal">true</span>,
    <span class="hljs-symbol">length:</span> { <span class="hljs-symbol">minimum:</span> <span class="hljs-number">8</span>, <span class="hljs-symbol">maximum:</span> <span class="hljs-number">72</span> },
    <span class="hljs-symbol">format:</span> { <span class="hljs-symbol">with:</span> PASSWORD_FORMAT },
    <span class="hljs-symbol">if:</span> <span class="hljs-symbol">:password_required?</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">password_required?</span></span>
    password_digest.<span class="hljs-literal">nil</span>? <span class="hljs-params">||</span> password.present?
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Password requirements:</p>
<ul>
<li>Minimum 8 characters</li>
<li>Maximum 72 characters (bcrypt limitation)</li>
<li>Must include lowercase and uppercase letters</li>
<li>Must include numbers and special characters</li>
<li>Validated only when necessary</li>
</ul>
<h2 id="heading-testing-strategy">🧪 Testing Strategy</h2>
<pre><code class="lang-ruby">RSpec.describe Users::SignInService <span class="hljs-keyword">do</span>
  describe <span class="hljs-string">'#call'</span> <span class="hljs-keyword">do</span>
    context <span class="hljs-string">'when credentials are valid'</span> <span class="hljs-keyword">do</span>
      it <span class="hljs-string">'generates an authentication token'</span> <span class="hljs-keyword">do</span>
        result = service.call
        expect(result.data[<span class="hljs-symbol">:token</span>]).to be_present
        expect(User.find_by_token_for(<span class="hljs-symbol">:auth_token</span>, result.data[<span class="hljs-symbol">:token</span>])).to eq(user)
      <span class="hljs-keyword">end</span>

      it <span class="hljs-string">'logs the authentication event'</span> <span class="hljs-keyword">do</span>
        expect { service.call }.to change(AuditLog, <span class="hljs-symbol">:count</span>).by(<span class="hljs-number">1</span>)
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>

    context <span class="hljs-string">'when credentials are invalid'</span> <span class="hljs-keyword">do</span>
      it <span class="hljs-string">'returns appropriate error messages'</span> <span class="hljs-keyword">do</span>
        result = described_class.new(invalid_params).call
        expect(result.errors).to <span class="hljs-keyword">include</span>(I18n.t(<span class="hljs-string">'services.users.sign_in.user_not_found'</span>))
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Our testing approach:</p>
<ol>
<li>Verifies token generation and validation</li>
<li>Ensures proper error handling</li>
<li>Checks audit logging</li>
<li>Validates security constraints</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>By implementing these patterns, we've created a secure, maintainable authentication system that:</p>
<ul>
<li>Provides secure token-based authentication</li>
<li>Handles email verification properly</li>
<li>Maintains high security standards</li>
<li>Scales well with application growth</li>
</ul>
<p>The complete implementation demonstrates how these components work together in a production environment while maintaining security and user experience.</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Rails Soft Delete & Audit Logging Guide]]></title><description><![CDATA[As financial applications grow in complexity, data integrity becomes paramount. Today, I'll share insights from implementing a robust soft deletion system with comprehensive audit logging in a Rails financial application. Let's explore how we can mai...]]></description><link>https://sulmanweb.com/rails-soft-delete-audit-logging-implementation</link><guid isPermaLink="true">https://sulmanweb.com/rails-soft-delete-audit-logging-implementation</guid><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Thu, 02 Jan 2025 05:00:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735006116219/51f38814-512d-4f17-819d-9868bbb01090.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As financial applications grow in complexity, data integrity becomes paramount. Today, I'll share insights from implementing a robust soft deletion system with comprehensive audit logging in a Rails financial application. Let's explore how we can maintain data traceability while ensuring nothing is permanently lost.</p>
<h2 id="heading-the-challenge">The Challenge</h2>
<p>In financial applications, simply deleting records isn't an option. We need to:</p>
<ul>
<li>Track all changes to financial records</li>
<li>Maintain data relationships</li>
<li>Enable record restoration</li>
<li>Ensure audit compliance</li>
</ul>
<h2 id="heading-implementing-soft-delete-with-actsasparanoid">Implementing Soft Delete with acts_as_paranoid</h2>
<p>The <code>acts_as_paranoid</code> gem provides a solid foundation for soft deletion. Here's how we've implemented it in our Account model:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Account</span> &lt; ApplicationRecord</span>
  acts_as_paranoid

  belongs_to <span class="hljs-symbol">:currency</span>, <span class="hljs-symbol">optional:</span> <span class="hljs-literal">true</span>
  belongs_to <span class="hljs-symbol">:user</span>
  has_many <span class="hljs-symbol">:transactions</span>, <span class="hljs-symbol">dependent:</span> <span class="hljs-symbol">:destroy</span>

  validates <span class="hljs-symbol">:name</span>, <span class="hljs-symbol">presence:</span> <span class="hljs-literal">true</span>
  validates <span class="hljs-symbol">:balance</span>, <span class="hljs-symbol">presence:</span> <span class="hljs-literal">true</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>We added a <code>deleted_at</code> timestamp column which, when set, effectively "hides" the record from normal queries while preserving it in the database.</p>
<h2 id="heading-advanced-audit-logging-system">Advanced Audit Logging System</h2>
<p>We've built a comprehensive audit logging system that tracks every change to our financial records:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuditLog</span> &lt; ApplicationRecord</span>
  acts_as_paranoid

  belongs_to <span class="hljs-symbol">:user</span>, <span class="hljs-symbol">optional:</span> <span class="hljs-literal">true</span>

  validates <span class="hljs-symbol">:class_name</span>, <span class="hljs-symbol">presence:</span> <span class="hljs-literal">true</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The audit logging is integrated into our service layer using a base service class:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationService</span></span>
  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">log_event</span><span class="hljs-params">(<span class="hljs-symbol">user:</span>, <span class="hljs-symbol">data:</span> {})</span></span>
    event_data = { 
      <span class="hljs-symbol">user:</span> user, 
      <span class="hljs-symbol">data:</span> data, 
      <span class="hljs-symbol">class_name:</span> <span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.to_s 
    }.compact
    AuditLog.create(event_data)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-transaction-tracking-across-deletions">Transaction Tracking Across Deletions</h2>
<p>For financial transactions, we maintain a complete audit trail even after deletion. Here's how we handle transaction deletion:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Transactions::DestroyService</span> &lt; ApplicationService</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span>
    <span class="hljs-keyword">return</span> failure([TRANSACTION_NOT_FOUND_MESSAGE]) <span class="hljs-keyword">unless</span> transaction
    <span class="hljs-keyword">return</span> failure([USER_NOT_FOUND_MESSAGE]) <span class="hljs-keyword">unless</span> user

    ActiveRecord::Base.transaction <span class="hljs-keyword">do</span>
      transaction.destroy
      update_account_balance
      log_event(<span class="hljs-symbol">user:</span> user, <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">transaction:</span> transaction })
      success(TRANSACTION_DELETED_MESSAGE)
    <span class="hljs-keyword">rescue</span> ActiveRecord::RecordInvalid =&gt; e
      failure(e.record.errors.full_messages)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_account_balance</span></span>
    factor = transaction.transaction_type == <span class="hljs-string">'expense'</span> ? <span class="hljs-number">1</span> : -<span class="hljs-number">1</span>
    transaction.account.update!(
      <span class="hljs-symbol">balance:</span> transaction.account.balance + (transaction.amount * factor)
    )
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-maintaining-data-integrity">Maintaining Data Integrity</h2>
<p>To ensure data integrity, we've implemented several key features:</p>
<ol>
<li><p><strong>Atomic Transactions</strong>: All related operations are wrapped in database transactions:</p>
<pre><code class="lang-ruby">ActiveRecord::Base.transaction <span class="hljs-keyword">do</span>
transaction.destroy
update_account_balance
log_event(<span class="hljs-symbol">user:</span> user, <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">transaction:</span> transaction })
<span class="hljs-keyword">end</span>
</code></pre>
</li>
<li><p><strong>Relationship Preservation</strong>: We maintain relationships between records even after deletion:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> &lt; ApplicationRecord</span>
acts_as_paranoid
has_many <span class="hljs-symbol">:audit_logs</span>, <span class="hljs-symbol">dependent:</span> <span class="hljs-symbol">:nullify</span>
has_many <span class="hljs-symbol">:accounts</span>, <span class="hljs-symbol">dependent:</span> <span class="hljs-symbol">:destroy</span>
has_many <span class="hljs-symbol">:transactions</span>, <span class="hljs-symbol">dependent:</span> <span class="hljs-symbol">:destroy</span>
<span class="hljs-keyword">end</span>
</code></pre>
</li>
<li><p><strong>Automated Cleanup</strong>: We handle old soft-deleted records with a background job:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RemoveSoftDeletedUsersJob</span> &lt; ApplicationJob</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span></span>
 <span class="hljs-keyword">return</span> <span class="hljs-keyword">unless</span> Settings.jobs.remove_soft_deleted_users.enabled

 User.deleted_before_time(eval(Settings.jobs.remove_soft_deleted_users.time).ago)
     .each(&amp;<span class="hljs-symbol">:destroy_fully!</span>)
<span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
</li>
</ol>
<h2 id="heading-real-world-benefits">Real-world Benefits</h2>
<p>This implementation has provided several advantages:</p>
<ol>
<li><strong>Regulatory Compliance</strong>: Complete audit trails for financial transactions</li>
<li><strong>Data Recovery</strong>: Easy restoration of accidentally deleted records</li>
<li><strong>Performance</strong>: Minimal impact on database performance while maintaining data integrity</li>
<li><strong>Maintenance</strong>: Automated cleanup of old soft-deleted records</li>
</ol>
<h2 id="heading-learning-outcomes">Learning Outcomes</h2>
<p>Building this system taught me several valuable lessons:</p>
<ol>
<li>Always consider the broader implications of data deletion in financial systems</li>
<li>Design for auditability from the ground up</li>
<li>Balance between data retention and system performance</li>
<li>Importance of atomic operations in maintaining data consistency</li>
</ol>
<h2 id="heading-best-practices">Best Practices</h2>
<p>When implementing a similar system, consider these recommendations:</p>
<ol>
<li>Always use database transactions for related operations</li>
<li>Log both the action and the state of the data</li>
<li>Implement cleanup strategies for old soft-deleted records</li>
<li>Maintain relationships between related records even after deletion</li>
<li>Design the audit system to be queryable and maintainable</li>
</ol>
<p>This implementation has proven robust in production, handling millions of financial transactions while maintaining complete traceability and data integrity. The combination of soft deletion and comprehensive audit logging provides the security and transparency essential for financial applications.</p>
<p>Remember, in financial applications, it's not just about storing data—it's about maintaining a verifiable history of every change while ensuring data integrity at every step.</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Rails Testing for Financial Operations]]></title><description><![CDATA[Testing financial applications requires exceptional attention to detail and robust test coverage. In this article, we'll explore advanced testing patterns for financial operations using a real-world Rails application. We'll cover transaction testing,...]]></description><link>https://sulmanweb.com/rails-testing-for-financial-operations</link><guid isPermaLink="true">https://sulmanweb.com/rails-testing-for-financial-operations</guid><category><![CDATA[#rspec]]></category><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Mon, 30 Dec 2024 05:00:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735006343030/fd14a811-0c10-4eb8-b4b7-16aeace99525.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Testing financial applications requires exceptional attention to detail and robust test coverage. In this article, we'll explore advanced testing patterns for financial operations using a real-world Rails application. We'll cover transaction testing, balance validations, and audit logging verification.</p>
<h2 id="heading-the-foundation-service-objects-and-rspec">The Foundation: Service Objects and RSpec</h2>
<p>Our financial application uses a service-object pattern to encapsulate business logic. Here's how we structure our tests:</p>
<pre><code class="lang-ruby">RSpec.describe Transactions::CreateService <span class="hljs-keyword">do</span>
  subject(<span class="hljs-symbol">:service</span>) { described_class.new(params) }

  let(<span class="hljs-symbol">:user</span>) { create(<span class="hljs-symbol">:user</span>) }
  let(<span class="hljs-symbol">:account</span>) { create(<span class="hljs-symbol">:account</span>, <span class="hljs-symbol">balance:</span> <span class="hljs-number">100</span>, <span class="hljs-symbol">user:</span> user) }
  let(<span class="hljs-symbol">:params</span>) <span class="hljs-keyword">do</span>
    {
      <span class="hljs-symbol">user_id:</span> user.id,
      <span class="hljs-symbol">account_id:</span> account.id,
      <span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>,
      <span class="hljs-symbol">transaction_type:</span> <span class="hljs-string">'expense'</span>
    }
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-testing-financial-transactions">Testing Financial Transactions</h2>
<p>When testing financial transactions, we need to verify several aspects:</p>
<ol>
<li><p>Balance updates</p>
</li>
<li><p>Transaction records</p>
</li>
<li><p>Audit logs</p>
</li>
<li><p>Error handling</p>
</li>
</ol>
<p>Here's a comprehensive test example:</p>
<pre><code class="lang-ruby">describe <span class="hljs-string">'#call'</span> <span class="hljs-keyword">do</span>
  context <span class="hljs-string">'when creating an expense transaction'</span> <span class="hljs-keyword">do</span>
    it <span class="hljs-string">'updates the account balance correctly'</span> <span class="hljs-keyword">do</span>
      result = service.call
      expect(result).to be_success
      expect(account.reload.balance).to eq(<span class="hljs-number">50</span>) <span class="hljs-comment"># 100 - 50</span>
    <span class="hljs-keyword">end</span>

    it <span class="hljs-string">'creates an audit log'</span> <span class="hljs-keyword">do</span>
      expect { service.call }
        .to change(AuditLog, <span class="hljs-symbol">:count</span>).by(<span class="hljs-number">1</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  context <span class="hljs-string">'when creating an income transaction'</span> <span class="hljs-keyword">do</span>
    let(<span class="hljs-symbol">:params</span>) <span class="hljs-keyword">do</span>
      {
        <span class="hljs-symbol">user_id:</span> user.id,
        <span class="hljs-symbol">account_id:</span> account.id,
        <span class="hljs-symbol">amount:</span> <span class="hljs-number">50</span>,
        <span class="hljs-symbol">transaction_type:</span> <span class="hljs-string">'income'</span>
      }
    <span class="hljs-keyword">end</span>

    it <span class="hljs-string">'increases the account balance'</span> <span class="hljs-keyword">do</span>
      result = service.call
      expect(result).to be_success
      expect(account.reload.balance).to eq(<span class="hljs-number">150</span>) <span class="hljs-comment"># 100 + 50</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-testing-money-transfers">Testing Money Transfers</h2>
<p>Transfer operations are particularly critical as they involve multiple accounts. Here's how we test them:</p>
<pre><code class="lang-ruby">RSpec.describe Transactions::TransferService <span class="hljs-keyword">do</span>
  let(<span class="hljs-symbol">:user</span>) { create(<span class="hljs-symbol">:user</span>) }
  let(<span class="hljs-symbol">:account_from</span>) { create(<span class="hljs-symbol">:account</span>, <span class="hljs-symbol">user:</span> user, <span class="hljs-symbol">balance:</span> <span class="hljs-number">1000</span>) }
  let(<span class="hljs-symbol">:account_to</span>) { create(<span class="hljs-symbol">:account</span>, <span class="hljs-symbol">user:</span> user, <span class="hljs-symbol">balance:</span> <span class="hljs-number">0</span>) }
  let(<span class="hljs-symbol">:params</span>) <span class="hljs-keyword">do</span>
    {
      <span class="hljs-symbol">user_id:</span> user.id,
      <span class="hljs-symbol">account_from_id:</span> account_from.id,
      <span class="hljs-symbol">account_to_id:</span> account_to.id,
      <span class="hljs-symbol">amount:</span> <span class="hljs-number">100</span>
    }
  <span class="hljs-keyword">end</span>

  describe <span class="hljs-string">'#call'</span> <span class="hljs-keyword">do</span>
    it <span class="hljs-string">'transfers money between accounts'</span> <span class="hljs-keyword">do</span>
      service = described_class.new(params)
      result = service.call

      expect(result).to be_success
      expect(account_from.reload.balance).to eq(<span class="hljs-number">900</span>)
      expect(account_to.reload.balance).to eq(<span class="hljs-number">100</span>)
    <span class="hljs-keyword">end</span>

    it <span class="hljs-string">'creates two transactions'</span> <span class="hljs-keyword">do</span>
      expect { service.call }
        .to change(Transaction, <span class="hljs-symbol">:count</span>).by(<span class="hljs-number">2</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-shared-examples-for-common-behaviors">Shared Examples for Common Behaviors</h2>
<p>To maintain DRY tests, we use shared examples for common behaviors:</p>
<pre><code class="lang-ruby">RSpec.shared_examples <span class="hljs-string">'an audit log'</span> <span class="hljs-keyword">do</span> <span class="hljs-params">|service_call, should_create|</span>
  <span class="hljs-keyword">if</span> should_create
    it <span class="hljs-string">'creates an audit log'</span> <span class="hljs-keyword">do</span>
      expect { instance_exec(&amp;service_call) }
        .to change(AuditLog, <span class="hljs-symbol">:count</span>).by(<span class="hljs-number">1</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">else</span>
    it <span class="hljs-string">'does not create an audit log'</span> <span class="hljs-keyword">do</span>
      expect { instance_exec(&amp;service_call) }
        .not_to change(AuditLog, <span class="hljs-symbol">:count</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># Usage in specs</span>
describe <span class="hljs-string">'successful transaction'</span> <span class="hljs-keyword">do</span>
  include_examples <span class="hljs-string">'an audit log'</span>, 
    -&gt; { service.call }, 
    <span class="hljs-literal">true</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-testing-edge-cases">Testing Edge Cases</h2>
<p>Financial applications must handle edge cases gracefully:</p>
<pre><code class="lang-ruby">describe <span class="hljs-string">'edge cases'</span> <span class="hljs-keyword">do</span>
  context <span class="hljs-string">'with insufficient funds'</span> <span class="hljs-keyword">do</span>
    let(<span class="hljs-symbol">:account</span>) { create(<span class="hljs-symbol">:account</span>, <span class="hljs-symbol">balance:</span> <span class="hljs-number">10</span>) }
    let(<span class="hljs-symbol">:params</span>) <span class="hljs-keyword">do</span>
      {
        <span class="hljs-symbol">user_id:</span> user.id,
        <span class="hljs-symbol">account_id:</span> account.id,
        <span class="hljs-symbol">amount:</span> <span class="hljs-number">100</span>,
        <span class="hljs-symbol">transaction_type:</span> <span class="hljs-string">'expense'</span>
      }
    <span class="hljs-keyword">end</span>

    it <span class="hljs-string">'fails the transaction'</span> <span class="hljs-keyword">do</span>
      result = service.call
      expect(result).not_to be_success
      expect(account.reload.balance).to eq(<span class="hljs-number">10</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  context <span class="hljs-string">'with invalid amounts'</span> <span class="hljs-keyword">do</span>
    let(<span class="hljs-symbol">:params</span>) <span class="hljs-keyword">do</span>
      {
        <span class="hljs-symbol">user_id:</span> user.id,
        <span class="hljs-symbol">account_id:</span> account.id,
        <span class="hljs-symbol">amount:</span> -<span class="hljs-number">50</span>,
        <span class="hljs-symbol">transaction_type:</span> <span class="hljs-string">'expense'</span>
      }
    <span class="hljs-keyword">end</span>

    it <span class="hljs-string">'rejects negative amounts'</span> <span class="hljs-keyword">do</span>
      result = service.call
      expect(result).not_to be_success
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-testing-currency-conversions">Testing Currency Conversions</h2>
<p>When dealing with multiple currencies, we need to test conversion accuracy:</p>
<pre><code class="lang-ruby">RSpec.describe Currency <span class="hljs-keyword">do</span>
  let(<span class="hljs-symbol">:usd</span>) { create(<span class="hljs-symbol">:currency</span>, <span class="hljs-symbol">code:</span> <span class="hljs-string">'USD'</span>, <span class="hljs-symbol">amount:</span> <span class="hljs-number">1.0</span>) }
  let(<span class="hljs-symbol">:eur</span>) { create(<span class="hljs-symbol">:currency</span>, <span class="hljs-symbol">code:</span> <span class="hljs-string">'EUR'</span>, <span class="hljs-symbol">amount:</span> <span class="hljs-number">0</span>.<span class="hljs-number">85</span>) }

  describe <span class="hljs-string">'currency conversion'</span> <span class="hljs-keyword">do</span>
    it <span class="hljs-string">'converts amounts correctly'</span> <span class="hljs-keyword">do</span>
      account = create(<span class="hljs-symbol">:account</span>, <span class="hljs-symbol">currency:</span> eur, <span class="hljs-symbol">balance:</span> <span class="hljs-number">100</span>)

      <span class="hljs-comment"># Verify USD equivalent</span>
      expect(account.balance_in_usd).to eq(<span class="hljs-number">117.65</span>) <span class="hljs-comment"># 100 / 0.85</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-testing-graphql-mutations">Testing GraphQL Mutations</h2>
<p>Our financial operations are exposed via GraphQL. Here's how we test them:</p>
<pre><code class="lang-ruby">RSpec.describe Mutations::TransactionCreate <span class="hljs-keyword">do</span>
  let(<span class="hljs-symbol">:user</span>) { create(<span class="hljs-symbol">:user</span>) }
  let(<span class="hljs-symbol">:account</span>) { create(<span class="hljs-symbol">:account</span>, <span class="hljs-symbol">user:</span> user) }

  let(<span class="hljs-symbol">:query</span>) <span class="hljs-keyword">do</span>
    <span class="hljs-string">&lt;&lt;~GQL
      mutation($input: TransactionCreateInput!) {
        transactionCreate(input: $input) {
          success
          transaction { id amount }
        }
      }
    GQL</span>
  <span class="hljs-keyword">end</span>

  it <span class="hljs-string">'creates a transaction through GraphQL'</span> <span class="hljs-keyword">do</span>
    variables = {
      <span class="hljs-symbol">input:</span> {
        <span class="hljs-symbol">accountId:</span> account.id,
        <span class="hljs-symbol">amount:</span> <span class="hljs-number">100</span>,
        <span class="hljs-symbol">transactionType:</span> <span class="hljs-string">'EXPENSE'</span>
      }
    }

    post <span class="hljs-string">'/graphql'</span>, <span class="hljs-symbol">params:</span> { 
      <span class="hljs-symbol">query:</span> query, 
      <span class="hljs-symbol">variables:</span> variables.to_json 
    }, <span class="hljs-symbol">headers:</span> auth_headers(user)

    expect(response.parsed_body[<span class="hljs-string">'data'</span>][<span class="hljs-string">'transactionCreate'</span>])
      .to <span class="hljs-keyword">include</span>(<span class="hljs-string">'success'</span> =&gt; <span class="hljs-literal">true</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-best-practices-and-recommendations">Best Practices and Recommendations</h2>
<ol>
<li><p>Always test in a transaction block to ensure database cleanliness</p>
</li>
<li><p>Use factory_bot for test data setup</p>
</li>
<li><p>Test both happy and unhappy paths</p>
</li>
<li><p>Verify audit logs for sensitive operations</p>
</li>
<li><p>Use decimal for money calculations to avoid floating-point errors</p>
</li>
<li><p>Test currency conversions with known exchange rates</p>
</li>
<li><p>Verify transaction atomicity in transfers</p>
</li>
<li><p>Test authorization and authentication</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Testing financial operations requires a comprehensive approach that covers not just the happy path but also edge cases, error conditions, and audit requirements. By following these patterns and practices, you can build reliable financial applications with confidence in their correctness.</p>
<p>Remember that financial data is critical, and tests are your first line of defense against bugs and inconsistencies. Take the time to write thorough tests, and your future self (and your users) will thank you.</p>
<p>This testing approach has been battle-tested in production environments and provides a solid foundation for building robust financial applications. The key is to think about all possible scenarios and edge cases while maintaining clean, readable, and maintainable tests.</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Building a GitHub Activity CLI - A Ruby Journey]]></title><description><![CDATA[Links

GitHub Repo → https://github.com/sulmanweb/github-activity-cli

Roadmap.sh Task → https://roadmap.sh/projects/github-user-activity

My Solution for UpVotes → https://roadmap.sh/projects/github-user-activity/solutions?u=67124acb791f57dd60b7e0e4...]]></description><link>https://sulmanweb.com/building-github-activity-cli-ruby-guide</link><guid isPermaLink="true">https://sulmanweb.com/building-github-activity-cli-ruby-guide</guid><category><![CDATA[Ruby]]></category><category><![CDATA[command line]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Thu, 26 Dec 2024 05:00:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734579731980/cc724367-df32-4e2d-81d8-797f6ce84d5c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-links">Links</h2>
<ul>
<li><p>GitHub Repo → <a target="_blank" href="https://github.com/sulmanweb/github-activity-cli">https://github.com/sulmanweb/github-activity-cli</a></p>
</li>
<li><p>Roadmap.sh Task → <a target="_blank" href="https://roadmap.sh/projects/github-user-activity">https://roadmap.sh/projects/github-user-activity</a></p>
</li>
<li><p>My Solution for UpVotes → <a target="_blank" href="https://roadmap.sh/projects/github-user-activity/solutions?u=67124acb791f57dd60b7e0e4">https://roadmap.sh/projects/github-user-activity/solutions?u=67124acb791f57dd60b7e0e4</a></p>
</li>
</ul>
<hr />
<p>Have you ever wanted to quickly check someone's GitHub activity right from your terminal? I recently took on this challenge by building a simple yet powerful CLI tool in Ruby. Let me walk you through how I built this GitHub Activity CLI, sharing both the technical details and lessons learned along the way.</p>
<h2 id="heading-project-overview">Project Overview</h2>
<p>The GitHub Activity CLI is a command-line tool that fetches and displays a user's recent GitHub activities. It's built with Ruby and follows clean code principles while keeping the implementation straightforward and maintainable.</p>
<h2 id="heading-the-challenge">The Challenge</h2>
<p>The project requirements came from <a target="_blank" href="https://roadmap.sh/projects/github-user-activity">roadmap.sh's GitHub User Activity project</a>. The main goals were to:</p>
<ul>
<li><p>Fetch a user's recent GitHub activities using the GitHub API</p>
</li>
<li><p>Display the activities in a readable format</p>
</li>
<li><p>Handle various types of GitHub events (pushes, issues, PRs, etc.)</p>
</li>
<li><p>Implement proper error handling</p>
</li>
</ul>
<h2 id="heading-project-structure">Project Structure</h2>
<p>I organized the code into a clean, modular structure:</p>
<pre><code class="lang-ruby">├── bin
│   └── github-activity    <span class="hljs-comment"># Executable CLI script</span>
└── lib
    ├── activity_formatter.rb   <span class="hljs-comment"># Formats different event types</span>
    ├── github_activity.rb      <span class="hljs-comment"># Main application logic</span>
    └── github_client.rb        <span class="hljs-comment"># Handles GitHub API communication</span>
</code></pre>
<h2 id="heading-technical-implementation">Technical Implementation</h2>
<h3 id="heading-1-the-github-client">1. The GitHub Client</h3>
<p>The heart of the application is the <code>GitHubClient</code> class that handles all API communications:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GitHubClient</span></span>
  BASE_URL = <span class="hljs-string">'https://api.github.com'</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_user_events</span><span class="hljs-params">(username)</span></span>
    uri = URI(<span class="hljs-string">"<span class="hljs-subst">#{BASE_URL}</span>/users/<span class="hljs-subst">#{username}</span>/events"</span>)
    response = make_request(uri)

    <span class="hljs-keyword">case</span> response
    <span class="hljs-keyword">when</span> Net::HTTPSuccess
      JSON.parse(response.body)
    <span class="hljs-keyword">when</span> Net::HTTPNotFound
      raise <span class="hljs-string">"User '<span class="hljs-subst">#{username}</span>' not found"</span>
    <span class="hljs-keyword">else</span>
      raise <span class="hljs-string">"Failed to fetch GitHub activity: <span class="hljs-subst">#{response.message}</span>"</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>I used Ruby's built-in <code>Net::HTTP</code> library to keep dependencies minimal. The client handles basic error cases and returns parsed JSON data.</p>
<h3 id="heading-2-event-formatting">2. Event Formatting</h3>
<p>One of the interesting challenges was formatting different types of GitHub events. I created a dedicated <code>ActivityFormatter</code> class that handles this using a clean pattern:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ActivityFormatter</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">format</span><span class="hljs-params">(event)</span></span>
    <span class="hljs-keyword">case</span> event[<span class="hljs-string">'type'</span>]
    <span class="hljs-keyword">when</span> <span class="hljs-string">'PushEvent'</span>
      format_push_event(event)
    <span class="hljs-keyword">when</span> <span class="hljs-string">'CreateEvent'</span>
      format_create_event(event)
    <span class="hljs-keyword">when</span> <span class="hljs-string">'IssuesEvent'</span>
      format_issues_event(event)
    <span class="hljs-comment"># ... other event types</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>This approach makes it easy to add support for new event types and keeps the formatting logic organized.</p>
<h3 id="heading-3-the-main-application">3. The Main Application</h3>
<p>The <code>GitHubActivity</code> class ties everything together:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GitHubActivity</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(username)</span></span>
    @username = username
    @client = GitHubClient.new
    @formatter = ActivityFormatter.new
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run</span></span>
    events = @client.fetch_user_events(@username)
    <span class="hljs-keyword">if</span> events.empty?
      puts <span class="hljs-string">"No recent activity found for user '<span class="hljs-subst">#{@username}</span>'"</span>
      <span class="hljs-keyword">return</span>
    <span class="hljs-keyword">end</span>

    display_events(events)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-user-experience">User Experience</h2>
<p>I focused on making the CLI intuitive and user-friendly. Users can simply run:</p>
<pre><code class="lang-bash">./bin/github-activity username
</code></pre>
<p>The output is clean and readable:</p>
<pre><code class="lang-ruby">Recent GitHub activity <span class="hljs-keyword">for</span> <span class="hljs-symbol">sulmanweb:</span>
--------------------------------------------------
Pushed <span class="hljs-number">2</span> commits to sulmanweb/project
Created branch <span class="hljs-keyword">in</span> sulmanweb/new-repo
Closed issue <span class="hljs-string">'Bug fix needed'</span> <span class="hljs-keyword">in</span> sulmanweb/app
</code></pre>
<h2 id="heading-learning-outcomes">Learning Outcomes</h2>
<p>Building this CLI taught me several valuable lessons:</p>
<ol>
<li><p><strong>API Integration</strong>: Working with the GitHub API showed me the importance of good error handling and response parsing.</p>
</li>
<li><p><strong>Code Organization</strong>: Breaking the functionality into separate classes made the code more maintainable and testable.</p>
</li>
<li><p><strong>User Experience</strong>: Even for a CLI tool, user experience matters. Clear output and helpful error messages make a big difference.</p>
</li>
</ol>
<h2 id="heading-supporting-the-project">Supporting the Project</h2>
<p>Want to help make the GitHub Activity CLI even better? There are several ways you can contribute:</p>
<h3 id="heading-financial-support">Financial Support</h3>
<ul>
<li><p>⭐ Star the repository to show your support</p>
</li>
<li><p>💝 Consider upvoting my solution at <a target="_blank" href="https://roadmap.sh/projects/github-user-activity/solutions?u=67124acb791f57dd60b7e0e4">GitHub Activity CLI solution page</a></p>
</li>
<li><p>🎁 Share the project with your network</p>
</li>
</ul>
<h3 id="heading-technical-contributions">Technical Contributions</h3>
<ul>
<li><p>🐛 Report bugs and suggest features through GitHub Issues</p>
</li>
<li><p>🔧 Submit pull requests for bug fixes or enhancements</p>
</li>
<li><p>📚 Help improve the documentation</p>
</li>
<li><p>🌐 Help with translations if you speak multiple languages</p>
</li>
</ul>
<h2 id="heading-future-improvements">Future Improvements</h2>
<p>While the current version works well, there's always room for improvement:</p>
<ul>
<li><p>Add authentication to handle API rate limits</p>
</li>
<li><p>Include more detailed event information</p>
</li>
<li><p>Add filtering options for specific event types</p>
</li>
<li><p>Implement caching for faster repeated queries</p>
</li>
</ul>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Want to try it out? Here's how:</p>
<ol>
<li><p>Clone the repository</p>
</li>
<li><p>Make the CLI executable: <code>chmod +x bin/github-activity</code></p>
</li>
<li><p>Run it: <code>./bin/github-activity &lt;username&gt;</code></p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building this GitHub Activity CLI was a great exercise in creating a focused, useful tool while maintaining clean code practices. It shows how a relatively simple idea can be turned into a practical utility that others can use and build upon.</p>
<p>The source code is available on GitHub, and I welcome any feedback or contributions from the community. Happy coding! 🚀</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Task Tracker CLI]]></title><description><![CDATA[Links

RubyGems.org → https://rubygems.org/gems/task_tracker_cli

GitHub Repo → https://github.com/sulmanweb/task_tracker_cli

Roadmap.sh Task → https://roadmap.sh/projects/task-tracker

My Solution for UpVotes → https://roadmap.sh/projects/task-trac...]]></description><link>https://sulmanweb.com/task-tracker-cli</link><guid isPermaLink="true">https://sulmanweb.com/task-tracker-cli</guid><category><![CDATA[Ruby]]></category><category><![CDATA[projects]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[command line]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Mon, 23 Dec 2024 05:00:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734580023936/63b8e446-b71a-43c2-8b58-b8e8ff670390.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-links">Links</h1>
<ul>
<li><p>RubyGems.org → <a target="_blank" href="https://rubygems.org/gems/task_tracker_cli">https://rubygems.org/gems/task_tracker_cli</a></p>
</li>
<li><p>GitHub Repo → <a target="_blank" href="https://github.com/sulmanweb/task_tracker_cli">https://github.com/sulmanweb/task_tracker_cli</a></p>
</li>
<li><p>Roadmap.sh Task → <a target="_blank" href="https://roadmap.sh/projects/task-tracker">https://roadmap.sh/projects/task-tracker</a></p>
</li>
<li><p>My Solution for UpVotes → <a target="_blank" href="https://roadmap.sh/projects/task-tracker/solutions?u=67124acb791f57dd60b7e0e4">https://roadmap.sh/projects/task-tracker/solutions?u=67124acb791f57dd60b7e0e4</a></p>
</li>
</ul>
<hr />
<p>As part of my ongoing journey to improve my skills as a developer, I decided to work on a portfolio project that could challenge me while solving a practical problem. The project requirements were shared on <a target="_blank" href="https://roadmap.sh/projects/task-tracker">roadmap.sh</a>, and it immediately struck me as an ideal opportunity to create something versatile—a command-line tool for tracking tasks. Naturally, I chose Ruby for its simplicity and flexibility, and what began as a simple idea soon led me to publish my first Ruby gem!</p>
<h1 id="heading-the-idea-a-command-line-task-tracker">The Idea: A Command-Line Task Tracker</h1>
<p>The project I picked from <a target="_blank" href="https://roadmap.sh/projects/task-tracker">roadmap.sh</a> was a task-tracking tool that could easily record, update, and delete tasks from the command line. I have always loved command-line utilities—there's something empowering about typing commands and getting instant feedback. So, I wanted to build a tool that not only met the project requirements but also provided a clean and seamless experience for command-line enthusiasts like myself.</p>
<h1 id="heading-creating-the-gem-tasktrackercli">Creating the Gem: <code>task_tracker_cli</code></h1>
<p>After some initial tinkering, I realized I wanted this task tracker to be more accessible and easy to use, not only for myself but for anyone who might find it useful. That's when I decided to go the extra mile and turn the tool into a Ruby gem: <a target="_blank" href="https://rubygems.org/gems/task_tracker_cli"><strong>task_tracker_cli</strong></a>.</p>
<p>The result? A simple installation process with <code>gem install task_tracker_cli</code>, and you're ready to start organizing your tasks right from the terminal!</p>
<h1 id="heading-example-usage">Example Usage</h1>
<p>Here is an example of how to use the tool:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install the gem and everything is ready to go</span>
gem install task_tracker_cli

<span class="hljs-comment"># Adding a new task</span>
task-cli add <span class="hljs-string">"Buy groceries"</span>
<span class="hljs-comment"># Output: Task added successfully (ID: 1)</span>

<span class="hljs-comment"># Updating and deleting tasks</span>
task-cli update 1 <span class="hljs-string">"Buy groceries and cook dinner"</span>
task-cli delete 1

<span class="hljs-comment"># Marking a task as in progress or done</span>
task-cli mark-in-progress 1
task-cli mark-done 1

<span class="hljs-comment"># Listing all tasks</span>
task-cli list

<span class="hljs-comment"># Listing tasks by status</span>
task-cli list <span class="hljs-keyword">done</span>
task-cli list todo
task-cli list in-progress

<span class="hljs-comment"># Uninstall the gem</span>
gem uninstall task_tracker_cli
</code></pre>
<p><strong>You can also find more usage examples in the</strong> <a target="_blank" href="https://github.com/sulmanweb/task_tracker_cli"><strong>README of the repository</strong></a> <strong>and on</strong> <a target="_blank" href="https://roadmap.sh/projects/task-tracker"><strong>roadmap.sh</strong></a><strong>.</strong></p>
<h1 id="heading-why-turn-it-into-a-gem">Why Turn It into a Gem?</h1>
<p>Creating a gem was not originally part of the plan, but as I kept working, I realized that this was a perfect opportunity to package my work in a way that others could easily use, contribute to, and learn from. Gems are essentially Ruby's way of packaging libraries or tools, which means anyone can install it, use it, or even tweak it to their needs. By publishing it, I also learned the whole process of how to create, build, and publish a gem—skills that will undoubtedly be useful in my future development work.</p>
<h1 id="heading-want-to-try-it">Want to Try It?</h1>
<p>If you're interested in giving this tool a try, you can find the gem on <a target="_blank" href="https://rubygems.org/gems/task_tracker_cli">RubyGems.org</a>, and the source code is available on <a target="_blank" href="https://github.com/sulmanweb/task_tracker_cli">GitHub</a>. Feel free to explore, fork, or even contribute to the project! Also, don't forget to star the repo 🙏</p>
<h1 id="heading-supporting-the-project">Supporting the Project</h1>
<p>This project was created as part of a community challenge on <a target="_blank" href="https://roadmap.sh/">roadmap.sh</a>. I'd greatly appreciate your support! You can check out my solution and upvote it on the <a target="_blank" href="https://roadmap.sh/projects/task-tracker/solutions?u=67124acb791f57dd60b7e0e4">task tracker solution page</a> 🙏. Your upvotes would mean a lot to me and encourage me to create even more helpful projects.</p>
<p>If you're on your own learning journey, I highly recommend joining <a target="_blank" href="https://roadmap.sh/">roadmap.sh</a> and checking out some of the projects there. It's a great way to level up your skills, share your work, and get feedback from an engaged community of learners and developers.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Creating this task tracker CLI tool was an incredibly enriching experience for me. I not only got to dive deep into Ruby and explore gem creation, but I also created a tool that could genuinely help others stay organized and productive. I hope you'll find it useful, and I'd love to hear any feedback or suggestions you may have.</p>
<hr />
<p>Happy coding, and keep building awesome stuff!</p>
]]></content:encoded></item><item><title><![CDATA[Build a Currency Exchange Service in Ruby on Rails]]></title><description><![CDATA[Working with exchange rates in Rails applications can be tricky. After implementing currency handling in several production apps, I've developed a robust pattern that I'll share with you today. We'll build a service that fetches live exchange rates f...]]></description><link>https://sulmanweb.com/rails-currency-exchange-service</link><guid isPermaLink="true">https://sulmanweb.com/rails-currency-exchange-service</guid><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[currency-converter]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Thu, 19 Dec 2024 05:00:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733796790415/6987180c-cae0-4a43-bb46-4fa97abbedce.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Working with exchange rates in Rails applications can be tricky. After implementing currency handling in several production apps, I've developed a robust pattern that I'll share with you today. We'll build a service that fetches live exchange rates from Currency Layer API and integrates smoothly with Rails.</p>
<p>🎯 What We'll Cover:</p>
<ul>
<li>Setting up a reusable currency exchange service</li>
<li>Implementing background rate updates</li>
<li>Handling API integration gracefully</li>
<li>Testing our implementation</li>
</ul>
<h2 id="heading-project-setup">Project Setup</h2>
<p>First, let's add the necessary gems to our <code>Gemfile</code>:</p>
<pre><code class="lang-ruby">gem <span class="hljs-string">'httparty'</span>  <span class="hljs-comment"># For API requests</span>
gem <span class="hljs-string">'solid_queue'</span>  <span class="hljs-comment"># For background jobs</span>
</code></pre>
<h2 id="heading-building-the-currency-exchange-service">Building the Currency Exchange Service</h2>
<p>I prefer using Plain Old Ruby Objects (POROs) for API integrations. Here's why - they're easy to test, maintain, and modify. Let's build our service:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># app/services/currency_exchange.rb</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CurrencyExchange</span></span>
  <span class="hljs-keyword">include</span> HTTParty
  base_uri <span class="hljs-string">'https://api.currencylayer.com'</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span></span>
    @options = { 
      <span class="hljs-symbol">query:</span> { 
        <span class="hljs-symbol">access_key:</span> Rails.application.credentials.currency_layer.api_key 
      } 
    }
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">self</span>.<span class="hljs-title">list</span></span>
    new.list
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">self</span>.<span class="hljs-title">live</span></span>
    new.live
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<blockquote>
<p> <strong>Pro Tip 💡</strong>
    I'm using class methods (<code>self.list</code> and <code>self.live</code>) as convenience wrappers around instance methods. This gives us flexibility - we can instantiate the class directly when we need to customize behavior, or use the class methods for quick access.</p>
</blockquote>
<h2 id="heading-handling-api-responses">Handling API Responses</h2>
<p>Let's build clean value objects for our data:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CurrencyExchange</span></span>
  <span class="hljs-comment"># ...</span>

  GlobalCurrency = Struct.new(<span class="hljs-symbol">:code</span>, <span class="hljs-symbol">:name</span>)
  Conversion = Struct.new(<span class="hljs-symbol">:from</span>, <span class="hljs-symbol">:to</span>, <span class="hljs-symbol">:rate</span>)

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">list</span></span>
    res = <span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.get(<span class="hljs-string">'/list'</span>, @options)
    <span class="hljs-keyword">return</span> res.parsed_response[<span class="hljs-string">'currencies'</span>].map { <span class="hljs-params">|code, name|</span> 
      GlobalCurrency.new(code, name) 
    } <span class="hljs-keyword">if</span> res.success?
    []
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">live</span></span>
    res = <span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.get(<span class="hljs-string">'/live'</span>, @options)
    <span class="hljs-keyword">return</span> [] <span class="hljs-keyword">unless</span> res.success?

    res.parsed_response[<span class="hljs-string">'quotes'</span>].map <span class="hljs-keyword">do</span> <span class="hljs-params">|code, rate|</span>
      Conversion.new(code[<span class="hljs-number">0</span>..<span class="hljs-number">2</span>], code[<span class="hljs-number">3</span>..], rate.to_f.round(<span class="hljs-number">4</span>))
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-why-this-approach-works">Why This Approach Works 🎯</h3>
<ol>
<li>Value objects provide a clean interface</li>
<li>Empty arrays as fallbacks prevent nil checking</li>
<li>Response parsing is encapsulated</li>
<li>Rate rounding handles floating-point precision</li>
</ol>
<h2 id="heading-database-integration">Database Integration</h2>
<p>We need to store our currency data. Here's our migration:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CreateCurrencies</span> &lt; ActiveRecord::Migration[7.2]</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change</span></span>
    create_table <span class="hljs-symbol">:currencies</span> <span class="hljs-keyword">do</span> <span class="hljs-params">|t|</span>
      t.string <span class="hljs-symbol">:code</span>, <span class="hljs-symbol">null:</span> <span class="hljs-literal">false</span>
      t.decimal <span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">precision:</span> <span class="hljs-number">14</span>, <span class="hljs-symbol">scale:</span> <span class="hljs-number">4</span>, <span class="hljs-symbol">default:</span> <span class="hljs-number">1.0</span>
      t.string <span class="hljs-symbol">:name</span>, <span class="hljs-symbol">null:</span> <span class="hljs-literal">false</span>
      t.datetime <span class="hljs-symbol">:converted_at</span>
      t.datetime <span class="hljs-symbol">:deleted_at</span>

      t.timestamps
    <span class="hljs-keyword">end</span>

    add_index <span class="hljs-symbol">:currencies</span>, <span class="hljs-symbol">:code</span>, <span class="hljs-symbol">unique:</span> <span class="hljs-literal">true</span>
    add_index <span class="hljs-symbol">:currencies</span>, <span class="hljs-symbol">:name</span>
    add_index <span class="hljs-symbol">:currencies</span>, <span class="hljs-symbol">:deleted_at</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-model-implementation">Model Implementation</h3>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Currency</span> &lt; ApplicationRecord</span>
  acts_as_paranoid  <span class="hljs-comment"># Soft deletes</span>

  has_many <span class="hljs-symbol">:accounts</span>, <span class="hljs-symbol">dependent:</span> <span class="hljs-symbol">:nullify</span>

  validates <span class="hljs-symbol">:name</span>, <span class="hljs-symbol">presence:</span> <span class="hljs-literal">true</span>
  validates <span class="hljs-symbol">:code</span>, <span class="hljs-symbol">presence:</span> <span class="hljs-literal">true</span>, <span class="hljs-symbol">uniqueness:</span> <span class="hljs-literal">true</span>
  validates <span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">presence:</span> <span class="hljs-literal">true</span>, 
            <span class="hljs-symbol">numericality:</span> { <span class="hljs-symbol">greater_than_or_equal_to:</span> <span class="hljs-number">0</span>.<span class="hljs-number">0</span> }
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-automated-rate-updates">Automated Rate Updates</h2>
<p>Here's where it gets interesting. We'll use a background job to update rates:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UpdateCurrencyRatesJob</span> &lt; ApplicationJob</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span></span>
    Currencies::UpdateRatesService.call
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The actual update service:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Currencies</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UpdateRatesService</span> &lt; ApplicationService</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span>
      CurrencyExchange.live.each <span class="hljs-keyword">do</span> <span class="hljs-params">|conversion|</span>
        update_rate(conversion)
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>

    private

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_rate</span><span class="hljs-params">(conversion)</span></span>
      Currency.find_by(<span class="hljs-symbol">code:</span> conversion.to)&amp;.update(
        <span class="hljs-symbol">amount:</span> conversion.rate,
        <span class="hljs-symbol">converted_at:</span> Time.current
      )
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-scheduling-updates">Scheduling Updates</h3>
<p>Configure your scheduler in <code>config/recurring.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">staging:</span>
  <span class="hljs-attr">update_currency_rates:</span>
    <span class="hljs-attr">class:</span> <span class="hljs-string">UpdateCurrencyRatesJob</span>
    <span class="hljs-attr">queue:</span> <span class="hljs-string">background</span>
    <span class="hljs-attr">schedule:</span> <span class="hljs-string">'0 1 */2 * *'</span>  <span class="hljs-comment"># Every 2 days at 1 AM</span>
</code></pre>
<h2 id="heading-testing-strategy">Testing Strategy</h2>
<p>Here's how I test this setup:</p>
<pre><code class="lang-ruby">RSpec.describe CurrencyExchange <span class="hljs-keyword">do</span>
  describe <span class="hljs-string">'.list'</span> <span class="hljs-keyword">do</span>
    it <span class="hljs-string">'returns structured currency data'</span> <span class="hljs-keyword">do</span>
      VCR.use_cassette(<span class="hljs-string">'currency_layer_list'</span>) <span class="hljs-keyword">do</span> <span class="hljs-comment"># https://github.com/vcr/vcr</span>
        currencies = described_class.list
        expect(currencies).to all(be_a(described_class::GlobalCurrency))
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>

    it <span class="hljs-string">'handles API failures gracefully'</span> <span class="hljs-keyword">do</span>
      allow(described_class).to receive(<span class="hljs-symbol">:get</span>).and_return(
        double(<span class="hljs-symbol">success?:</span> <span class="hljs-literal">false</span>)
      )
      expect(described_class.list).to eq([])
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-model-tests">Model Tests</h3>
<pre><code class="lang-ruby">RSpec.describe Currency <span class="hljs-keyword">do</span>
  describe <span class="hljs-string">'validations'</span> <span class="hljs-keyword">do</span>
    it <span class="hljs-string">'requires a valid exchange rate'</span> <span class="hljs-keyword">do</span>
      currency = build(<span class="hljs-symbol">:currency</span>, <span class="hljs-symbol">amount:</span> -<span class="hljs-number">1</span>)
      expect(currency).not_to be_valid
    <span class="hljs-keyword">end</span>

    it <span class="hljs-string">'enforces unique currency codes'</span> <span class="hljs-keyword">do</span>
      create(<span class="hljs-symbol">:currency</span>, <span class="hljs-symbol">code:</span> <span class="hljs-string">'USD'</span>)
      duplicate = build(<span class="hljs-symbol">:currency</span>, <span class="hljs-symbol">code:</span> <span class="hljs-string">'USD'</span>)
      expect(duplicate).not_to be_valid
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-usage-in-your-application">Usage in Your Application</h2>
<p>Here's how you'd use this in your app:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># Fetch available currencies</span>
currencies = CurrencyExchange.list
puts <span class="hljs-string">"Available currencies: <span class="hljs-subst">#{currencies.map(&amp;<span class="hljs-symbol">:code</span>).join(<span class="hljs-string">', '</span>)}</span>"</span>

<span class="hljs-comment"># Get current rates</span>
rates = CurrencyExchange.live
rates.each <span class="hljs-keyword">do</span> <span class="hljs-params">|conversion|</span>
  puts <span class="hljs-string">"1 <span class="hljs-subst">#{conversion.from}</span> = <span class="hljs-subst">#{conversion.rate}</span> <span class="hljs-subst">#{conversion.to}</span>"</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-common-pitfalls-to-avoid">Common Pitfalls to Avoid ⚠️</h2>
<ol>
<li><p><strong>Don't</strong> store sensitive API keys in your codebase. Use Rails credentials:</p>
<pre><code class="lang-bash">EDITOR=<span class="hljs-string">"code --wait"</span> bin/rails credentials:edit
</code></pre>
</li>
<li><p><strong>Don't</strong> update rates synchronously during user requests. Always use background jobs.</p>
</li>
<li><p><strong>Don't</strong> forget to handle API rate limits. Currency Layer has different limits for different plans.</p>
</li>
</ol>
<h2 id="heading-production-considerations">Production Considerations 🚀</h2>
<ol>
<li><p><strong>Error Monitoring</strong>: Add Sentry or similar error tracking:</p>
<pre><code class="lang-ruby">Sentry.capture_exception(e) <span class="hljs-keyword">if</span> <span class="hljs-keyword">defined</span>?(Sentry)
</code></pre>
</li>
<li><p><strong>Rate Limiting</strong>: Implement exponential backoff for API failures:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">with_retry</span></span>
retries <span class="hljs-params">||</span>= <span class="hljs-number">0</span>
<span class="hljs-keyword">yield</span>
<span class="hljs-keyword">rescue</span> StandardError =&gt; e
<span class="hljs-keyword">retry</span> <span class="hljs-keyword">if</span> (retries += <span class="hljs-number">1</span>) &lt; <span class="hljs-number">3</span>
raise e
<span class="hljs-keyword">end</span>
</code></pre>
</li>
<li><p><strong>Logging</strong>: Add structured logging for debugging:</p>
<pre><code class="lang-ruby">Rails.logger.info(
<span class="hljs-symbol">event:</span> <span class="hljs-string">'currency_rate_update'</span>,
<span class="hljs-symbol">currency:</span> conversion.to,
<span class="hljs-symbol">rate:</span> conversion.rate
)
</code></pre>
</li>
</ol>
<blockquote>
<p>Remember: Currency exchange rates are critical financial data. Always validate your implementation thoroughly and consider using paid API tiers for production use.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Service Objects: Level Up Your Rails Architecture]]></title><description><![CDATA[Introduction
In modern Rails applications, organizing business logic effectively is crucial for maintainability and scalability. One powerful pattern that has emerged is the use of Service Objects - Plain Old Ruby Objects (POROs) that encapsulate spe...]]></description><link>https://sulmanweb.com/implementing-service-objects-rails-clean-architecture</link><guid isPermaLink="true">https://sulmanweb.com/implementing-service-objects-rails-clean-architecture</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Rails]]></category><category><![CDATA[architecture]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Mon, 16 Dec 2024 05:00:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733796142469/fa3a0e4e-046c-4053-b0c6-2c64d11adf1a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In modern Rails applications, organizing business logic effectively is crucial for maintainability and scalability. One powerful pattern that has emerged is the use of Service Objects - Plain Old Ruby Objects (POROs) that encapsulate specific business operations. In this article, we'll explore how Service Objects can be implemented for handling GraphQL mutations and background jobs, using real-world examples from a financial transaction management system.</p>
<h2 id="heading-why-service-objects">Why Service Objects?</h2>
<p>Traditional Rails applications often struggle with "fat" models and controllers that violate the Single Responsibility Principle. Service Objects help solve this by:</p>
<ol>
<li><p>Encapsulating complex business logic</p>
</li>
<li><p>Improving testability</p>
</li>
<li><p>Enhancing code reusability</p>
</li>
<li><p>Providing clearer application architecture</p>
</li>
</ol>
<h2 id="heading-basic-structure-of-a-service-object">Basic Structure of a Service Object</h2>
<p>Let's look at the base Service Object pattern implemented in our application:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationService</span></span>
  <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:params</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">self</span>.<span class="hljs-title">call</span><span class="hljs-params">(params = {})</span></span>
    new(params).call
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(params = {})</span></span>
    @params = params.is_a?(String) ? JSON.parse(params, <span class="hljs-symbol">symbolize_names:</span> <span class="hljs-literal">true</span>) : params
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span>
    raise NotImplementedError, <span class="hljs-string">"<span class="hljs-subst">#{<span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>}</span> must implement #call"</span>
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">transaction</span><span class="hljs-params">(&amp;)</span></span>
    ActiveRecord::Base.transaction(&amp;)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">log_event</span><span class="hljs-params">(<span class="hljs-symbol">user:</span>, <span class="hljs-symbol">data:</span> {})</span></span>
    event_data = { <span class="hljs-symbol">user:</span>, <span class="hljs-symbol">data:</span>, <span class="hljs-symbol">class_name:</span> <span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.to_s }.compact
    AuditLog.create(event_data)
  <span class="hljs-keyword">end</span>

  Result = Struct.new(<span class="hljs-symbol">:success</span>, <span class="hljs-symbol">:data</span>, <span class="hljs-symbol">:errors</span>, <span class="hljs-symbol">keyword_init:</span> <span class="hljs-literal">true</span>) <span class="hljs-keyword">do</span>
    alias_method <span class="hljs-symbol">:success?</span>, <span class="hljs-symbol">:success</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">success</span><span class="hljs-params">(data = {})</span></span>
    Result.new(<span class="hljs-symbol">success:</span> <span class="hljs-literal">true</span>, <span class="hljs-symbol">data:</span>, <span class="hljs-symbol">errors:</span> <span class="hljs-literal">nil</span>)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">failure</span><span class="hljs-params">(errors)</span></span>
    Result.new(<span class="hljs-symbol">success:</span> <span class="hljs-literal">false</span>, <span class="hljs-symbol">data:</span> <span class="hljs-literal">nil</span>, <span class="hljs-symbol">errors:</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-real-world-example-transaction-management">Real-World Example: Transaction Management</h2>
<p>Let's examine how Service Objects handle complex business logic in a financial transaction system:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Transactions</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CreateService</span> &lt; ApplicationService</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span>
      validate_dependencies <span class="hljs-params">||</span>
        create_transaction
    <span class="hljs-keyword">end</span>

    private

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">validate_dependencies</span></span>
      <span class="hljs-keyword">return</span> failure([USER_NOT_FOUND_MESSAGE]) <span class="hljs-keyword">unless</span> user
      <span class="hljs-keyword">return</span> failure([ACCOUNT_NOT_FOUND_MESSAGE]) <span class="hljs-keyword">unless</span> account
      <span class="hljs-keyword">return</span> failure([CATEGORY_NOT_FOUND_MESSAGE]) <span class="hljs-keyword">unless</span> category <span class="hljs-params">||</span> permitted_params[<span class="hljs-symbol">:transfer</span>] == <span class="hljs-literal">true</span>

      <span class="hljs-literal">false</span>
    <span class="hljs-keyword">end</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_transaction</span></span>
      ActiveRecord::Base.transaction <span class="hljs-keyword">do</span>
        transaction = Transaction.new(permitted_params)
        transaction.transaction_time = Time.current <span class="hljs-keyword">if</span> transaction.transaction_time.blank?
        transaction.save!

        multiplier = transaction.transaction_type == <span class="hljs-string">'expense'</span> ? -<span class="hljs-number">1</span> : <span class="hljs-number">1</span>
        account.update!(<span class="hljs-symbol">balance:</span> account.balance + (transaction.amount * multiplier))

        log_event(<span class="hljs-symbol">user:</span>, <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">transaction:</span> })
        success(transaction)
      <span class="hljs-keyword">rescue</span> ActiveRecord::RecordInvalid =&gt; e
        failure(e.record.errors.full_messages)
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-benefits-in-practice">Benefits in Practice</h2>
<h3 id="heading-1-clear-interface">1. Clear Interface</h3>
<p>Service Objects provide a consistent interface through the <code>call</code> method:</p>
<pre><code class="lang-ruby">result = Transactions::CreateService.call(
  <span class="hljs-symbol">user_id:</span> current_user.id,
  <span class="hljs-symbol">account_id:</span> params[<span class="hljs-symbol">:account_id</span>],
  <span class="hljs-symbol">amount:</span> <span class="hljs-number">100.00</span>,
  <span class="hljs-symbol">transaction_type:</span> <span class="hljs-string">'expense'</span>
)

<span class="hljs-keyword">if</span> result.success?
  <span class="hljs-comment"># Handle success</span>
<span class="hljs-keyword">else</span>
  <span class="hljs-comment"># Handle failure</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-2-integrated-error-handling">2. Integrated Error Handling</h3>
<p>The Result object pattern provides a clean way to handle success and failure states:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">failure</span><span class="hljs-params">(errors)</span></span>
  Result.new(<span class="hljs-symbol">success:</span> <span class="hljs-literal">false</span>, <span class="hljs-symbol">data:</span> <span class="hljs-literal">nil</span>, <span class="hljs-symbol">errors:</span>)
<span class="hljs-keyword">end</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">success</span><span class="hljs-params">(data = {})</span></span>
  Result.new(<span class="hljs-symbol">success:</span> <span class="hljs-literal">true</span>, <span class="hljs-symbol">data:</span>, <span class="hljs-symbol">errors:</span> <span class="hljs-literal">nil</span>)
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-3-transaction-safety">3. Transaction Safety</h3>
<p>Services can easily wrap operations in database transactions:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">transfer_money</span></span>
  ActiveRecord::Base.transaction <span class="hljs-keyword">do</span>
    CreateService.call(<span class="hljs-symbol">user_id:</span> user.id, <span class="hljs-symbol">account_id:</span> account_to.id,
                       <span class="hljs-symbol">transaction_type:</span> <span class="hljs-string">'income'</span>, <span class="hljs-symbol">transfer:</span> <span class="hljs-literal">true</span>, **permitted_params)
    CreateService.call(<span class="hljs-symbol">user_id:</span> user.id, <span class="hljs-symbol">account_id:</span> account_from.id,
                       <span class="hljs-symbol">transaction_type:</span> <span class="hljs-string">'expense'</span>, <span class="hljs-symbol">transfer:</span> <span class="hljs-literal">true</span>, **permitted_params)
    success(TRANSACTION_CREATED_MESSAGE)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-4-audit-trail-integration">4. Audit Trail Integration</h3>
<p>Built-in logging capabilities for tracking business operations:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">log_event</span><span class="hljs-params">(<span class="hljs-symbol">user:</span>, <span class="hljs-symbol">data:</span> {})</span></span>
  event_data = { <span class="hljs-symbol">user:</span>, <span class="hljs-symbol">data:</span>, <span class="hljs-symbol">class_name:</span> <span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.to_s }.compact
  AuditLog.create(event_data)
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-integration-with-graphql-mutations">Integration with GraphQL Mutations</h2>
<p>Service Objects integrate seamlessly with GraphQL mutations:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Mutations</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TransactionCreate</span> &lt; BaseMutationWithErrors</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">resolve</span><span class="hljs-params">(**args)</span></span>
      auth_result = authenticate_user!
      <span class="hljs-keyword">return</span> auth_result <span class="hljs-keyword">unless</span> auth_result[<span class="hljs-symbol">:success</span>]

      result = Transactions::CreateService.call(<span class="hljs-symbol">user_id:</span> current_user.id, **args)
      {
        <span class="hljs-symbol">errors:</span> result.errors,
        <span class="hljs-symbol">success:</span> result.success?,
        <span class="hljs-symbol">transaction:</span> result.success? ? result.data : <span class="hljs-literal">nil</span>
      }
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-testing-service-objects">Testing Service Objects</h2>
<p>Service Objects are highly testable, as demonstrated by our RSpec examples:</p>
<pre><code class="lang-ruby">RSpec.describe Transactions::CreateService <span class="hljs-keyword">do</span>
  subject(<span class="hljs-symbol">:service</span>) { described_class.new(params) }

  let(<span class="hljs-symbol">:user</span>) { create(<span class="hljs-symbol">:user</span>) }
  let(<span class="hljs-symbol">:account</span>) { create(<span class="hljs-symbol">:account</span>, <span class="hljs-symbol">balance:</span> <span class="hljs-number">100</span>, <span class="hljs-symbol">user:</span>) }
  let(<span class="hljs-symbol">:params</span>) <span class="hljs-keyword">do</span>
    {
      <span class="hljs-symbol">user_id:</span> user.id,
      <span class="hljs-symbol">account_id:</span> account.id,
      <span class="hljs-symbol">amount:</span> <span class="hljs-number">100</span>,
      <span class="hljs-symbol">transaction_type:</span> <span class="hljs-string">'expense'</span>
    }
  <span class="hljs-keyword">end</span>

  describe <span class="hljs-string">'#call'</span> <span class="hljs-keyword">do</span>
    context <span class="hljs-string">'when valid'</span> <span class="hljs-keyword">do</span>
      it <span class="hljs-string">'creates the transaction and updates balance'</span> <span class="hljs-keyword">do</span>
        result = service.call
        expect(result).to be_success
        expect(account.reload.balance).to eq(<span class="hljs-number">0</span>)
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-best-practices-for-service-objects">Best Practices for Service Objects</h2>
<ol>
<li><p><strong>Single Responsibility</strong>: Each service should do one thing well</p>
</li>
<li><p><strong>Immutable Input</strong>: Use initialized parameters instead of instance variables</p>
</li>
<li><p><strong>Result Objects</strong>: Always return a consistent result object</p>
</li>
<li><p><strong>Transaction Safety</strong>: Wrap related operations in transactions</p>
</li>
<li><p><strong>Audit Trail</strong>: Log important business events</p>
</li>
<li><p><strong>Validation</strong>: Handle all edge cases and validation upfront</p>
</li>
<li><p><strong>Testing</strong>: Write comprehensive tests for each service</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Service Objects provide a powerful way to organize business logic in Rails applications. They offer clear benefits in terms of code organization, maintainability, and testability. When implemented consistently, they create a predictable pattern for handling complex business operations, whether in GraphQL mutations, background jobs, or other application components.</p>
<p>Remember that Service Objects are not a silver bullet - they should be used judiciously where they add value to your application architecture. For simple CRUD operations, traditional Rails patterns might still be the better choice.</p>
<p>The key is to identify complex business operations that benefit from encapsulation and isolation, and implement Service Objects for those specific cases. This approach leads to more maintainable and testable code while keeping the rest of your application simple and Rails-like.</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Setting Up GraphQL and GraphiQL in Rails: A Comprehensive Guide]]></title><description><![CDATA[GraphQL is becoming increasingly popular as an alternative to REST APIs, offering more flexibility and efficiency in data fetching. In this guide, we'll walk through setting up GraphQL and GraphiQL in a Rails application, using practical examples fro...]]></description><link>https://sulmanweb.com/implementing-graphql-graphiql-rails-7</link><guid isPermaLink="true">https://sulmanweb.com/implementing-graphql-graphiql-rails-7</guid><category><![CDATA[GraphQL]]></category><category><![CDATA[Rails]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Thu, 12 Dec 2024 05:00:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733570314182/75d27952-3acf-4e18-9155-73d795f299e4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>GraphQL is becoming increasingly popular as an alternative to REST APIs, offering more flexibility and efficiency in data fetching. In this guide, we'll walk through setting up GraphQL and GraphiQL in a Rails application, using practical examples from a real-world implementation.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Ruby on Rails 7.2+</p>
</li>
<li><p>Ruby 3.3+</p>
</li>
<li><p>Basic understanding of Rails applications</p>
</li>
</ul>
<h2 id="heading-step-1-adding-required-gems">Step 1: Adding Required Gems</h2>
<p>First, add the necessary gems to your <code>Gemfile</code>:</p>
<pre><code class="lang-ruby">gem <span class="hljs-string">'graphql'</span>, <span class="hljs-string">'2.3.14'</span>
gem <span class="hljs-string">'graphiql-rails'</span>, <span class="hljs-string">'1.10.1'</span>
</code></pre>
<h2 id="heading-step-2-installing-graphql">Step 2: Installing GraphQL</h2>
<p>Run the following commands to install GraphQL and generate the base files:</p>
<pre><code class="lang-bash">bundle install
rails generate graphql:install
</code></pre>
<p>This will create the basic GraphQL structure in your Rails application:</p>
<pre><code class="lang-ruby">app/
└── graphql/
    ├── types/
    │   ├── base_object.rb
    │   ├── base_enum.rb
    │   ├── base_input_object.rb
    │   ├── base_interface.rb
    │   ├── base_scalar.rb
    │   ├── base_union.rb
    │   ├── query_type.rb
    │   └── mutation_type.rb
    ├── mutations/
    │   └── base_mutation.rb
    └── [your_app]_schema.rb
</code></pre>
<h2 id="heading-step-3-setting-up-graphql-controller">Step 3: Setting Up GraphQL Controller</h2>
<p>Create a GraphQL controller to handle requests:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GraphqlController</span> &lt; ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">execute</span></span>
    variables = prepare_variables(params[<span class="hljs-symbol">:variables</span>])
    query = params[<span class="hljs-symbol">:query</span>]
    operation_name = params[<span class="hljs-symbol">:operationName</span>]
    context = {
      <span class="hljs-symbol">current_user:</span> current_user  <span class="hljs-comment"># This comes from ApplicationController</span>
    }
    result = YourAppSchema.execute(query,
                                 <span class="hljs-symbol">variables:</span> variables,
                                 <span class="hljs-symbol">context:</span> context,
                                 <span class="hljs-symbol">operation_name:</span> operation_name)
    render <span class="hljs-symbol">json:</span> result
  <span class="hljs-keyword">rescue</span> StandardError =&gt; e
    raise e <span class="hljs-keyword">unless</span> Rails.env.development?
    handle_error_in_development(e)
  <span class="hljs-keyword">end</span>

  private
  <span class="hljs-comment"># ... [rest of the controller code]</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-authentication-setup">Authentication Setup</h3>
<p>The <code>current_user</code> method is defined in your <code>ApplicationController</code>. Here's how to set it up:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationController</span> &lt; ActionController::API</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">auth_header</span></span>
    request.headers[<span class="hljs-string">'Authorization'</span>]&amp;.split&amp;.last
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">current_user</span></span>
    Current.user = User.find_by_token_for(<span class="hljs-symbol">:auth_token</span>, auth_header)
    @current_user <span class="hljs-params">||</span>= Current.user
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>This setup allows you to:</p>
<ol>
<li><p>Extract the authentication token from the request headers</p>
</li>
<li><p>Find the corresponding user</p>
</li>
<li><p>Make the user available throughout your GraphQL resolvers via context</p>
</li>
</ol>
<p>You can then access the current user in any resolver or mutation:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Mutations</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseMutation</span> &lt; GraphQL::Schema::<span class="hljs-title">RelayClassicMutation</span></span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">current_user</span></span>
      context[<span class="hljs-symbol">:current_user</span>]
    <span class="hljs-keyword">end</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">authenticate_user!</span></span>
      raise GraphQL::ExecutionError, <span class="hljs-string">'Not authenticated'</span> <span class="hljs-keyword">unless</span> current_user
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-step-4-configuring-graphiql">Step 4: Configuring GraphiQL</h2>
<p>Add GraphiQL configuration in <code>config/initializers/graphiql.rb</code>:</p>
<pre><code class="lang-ruby">GraphiQL::Rails.config.header_editor_enabled = <span class="hljs-literal">true</span>
GraphiQL::Rails.config.title = <span class="hljs-string">'Your API Name'</span>
</code></pre>
<p>Update your <code>config/application.rb</code> to handle cookies for GraphiQL:</p>
<pre><code class="lang-ruby">config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
config.middleware.insert_after(ActionDispatch::Cookies, ActionDispatch::Session::CookieStore)
</code></pre>
<h2 id="heading-step-5-setting-up-routes">Step 5: Setting Up Routes</h2>
<p>Add GraphQL routes to <code>config/routes.rb</code>:</p>
<pre><code class="lang-ruby">Rails.application.routes.draw <span class="hljs-keyword">do</span>
  post <span class="hljs-string">"/graphql"</span>, <span class="hljs-symbol">to:</span> <span class="hljs-string">"graphql#execute"</span>

  <span class="hljs-keyword">if</span> !Rails.env.production?
    mount GraphiQL::Rails::Engine, <span class="hljs-symbol">at:</span> <span class="hljs-string">"/graphiql"</span>, <span class="hljs-symbol">graphql_path:</span> <span class="hljs-string">"/graphql"</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-step-6-cors-configuration">Step 6: CORS Configuration</h2>
<p>If you're building an API, configure CORS in <code>config/initializers/cors.rb</code>:</p>
<pre><code class="lang-ruby">Rails.application.config.middleware.insert_before <span class="hljs-number">0</span>, Rack::Cors <span class="hljs-keyword">do</span>
  allow <span class="hljs-keyword">do</span>
    origins <span class="hljs-string">"*"</span>
    resource <span class="hljs-string">"*"</span>,
      <span class="hljs-symbol">headers:</span> <span class="hljs-symbol">:any</span>,
      <span class="hljs-symbol">methods:</span> [<span class="hljs-symbol">:get</span>, <span class="hljs-symbol">:post</span>, <span class="hljs-symbol">:put</span>, <span class="hljs-symbol">:patch</span>, <span class="hljs-symbol">:delete</span>, <span class="hljs-symbol">:options</span>, <span class="hljs-symbol">:head</span>]
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-best-practices">Best Practices</h2>
<h3 id="heading-1-structured-type-definitions">1. Structured Type Definitions</h3>
<p>Organize your types in a modular way:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Types</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseObject</span> &lt; GraphQL::Schema::<span class="hljs-title">Object</span></span>
    edge_type_class(Types::BaseEdge)
    connection_type_class(Types::BaseConnection)
    field_class Types::BaseField
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-2-implement-base-mutations">2. Implement Base Mutations</h3>
<p>Create a base mutation class for common functionality:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Mutations</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseMutation</span> &lt; GraphQL::Schema::<span class="hljs-title">RelayClassicMutation</span></span>
    argument_class Types::BaseArgument
    field_class Types::BaseField
    input_object_class Types::BaseInputObject
    object_class Types::BaseObject
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-3-error-handling">3. Error Handling</h3>
<p>Implement consistent error handling across your GraphQL API:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Mutations</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseMutationWithErrors</span> &lt; BaseMutation</span>
    field <span class="hljs-symbol">:errors</span>, [String], <span class="hljs-symbol">null:</span> <span class="hljs-literal">true</span>
    field <span class="hljs-symbol">:success</span>, Boolean, <span class="hljs-symbol">null:</span> <span class="hljs-literal">false</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_errors</span><span class="hljs-params">(record)</span></span>
      {
        <span class="hljs-symbol">success:</span> <span class="hljs-literal">false</span>,
        <span class="hljs-symbol">errors:</span> record.errors.full_messages
      }
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-testing-your-setup">Testing Your Setup</h2>
<p>After completing the setup, you can access GraphiQL at <code>http://localhost:3000/graphiql</code> in development. Try this query:</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">query</span> {
  __schema {
    types {
      name
    }
  }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733472993687/79f325c0-10ce-4e0b-9745-2939146f13a1.png" alt="GraphiQL View in Rails" class="image--center mx-auto" /></p>
<h2 id="heading-security-considerations">Security Considerations</h2>
<ol>
<li><p>Limit GraphiQL to non-production environments</p>
</li>
<li><p>Implement proper authentication</p>
</li>
<li><p>Use query depth limiting to prevent complex nested queries</p>
</li>
<li><p>Consider implementing rate limiting</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>With this setup, you have a solid foundation for building a GraphQL API in Rails. The included GraphiQL interface provides a powerful tool for testing and documenting your API during development.</p>
<p>Remember to:</p>
<ul>
<li><p>Keep your schema well-organized</p>
</li>
<li><p>Implement proper error handling</p>
</li>
<li><p>Follow security best practices</p>
</li>
<li><p>Write comprehensive tests for your GraphQL endpoints</p>
</li>
</ul>
<p>This setup provides a robust starting point for building scalable GraphQL APIs with Rails.</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Transform Your Codebase into Comprehensive Documentation with Markdown]]></title><description><![CDATA[Introduction
Welcome to the age of AI. The world is moving at lightning speed towards artificial intelligence, and programmers have an array of built-in tools in code editors like Zed, VSCode, and Cursor. These editors have the capability to analyze ...]]></description><link>https://sulmanweb.com/transform-codebase-into-markdown-documentation</link><guid isPermaLink="true">https://sulmanweb.com/transform-codebase-into-markdown-documentation</guid><category><![CDATA[chatgpt]]></category><category><![CDATA[markdown]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Mon, 09 Dec 2024 05:00:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732970654155/c6ea905b-eced-4eea-bf26-658d4626559c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Welcome to the age of AI. The world is moving at lightning speed towards artificial intelligence, and programmers have an array of built-in tools in code editors like Zed, VSCode, and Cursor. These editors have the capability to analyze large codebases and assist in resolving issues or creating features.</p>
<p>I've tested many of these editors, but sometimes even including the codebase in chat doesn't provide the full picture, which means the results often fall short or lack the quality the repository requires. The worst-case scenario arises when an AI chat starts producing circular problems: solving one problem introduces a new issue, and fixing that issue reintroduces the first problem.</p>
<p>The core challenge here is the limited access to the whole codebase due to constraints on the number of files or file sizes that AI tools can process.</p>
<p>Moreover, let’s talk about direct AI models like Claude, Perplexity, and ChatGPT. Since their inception, these tools have come a long way. Now, ChatGPT allows attachment of files in chat, but it still doesn't support submitting zipped folders or entire repositories, meaning it cannot consider your whole code. The same limitation exists for Claude and Perplexity. It would be incredibly beneficial if we could give these AI tools our code in a compressed form—something that isn’t spread across hundreds of files and is readable by the AI.</p>
<h2 id="heading-solution-a-ruby-script-to-convert-a-codebase-into-markdown">Solution: A Ruby Script to Convert a Codebase into Markdown</h2>
<h3 id="heading-why-ruby">Why Ruby?</h3>
<p>The first question that comes to mind is: why Ruby?</p>
<p>Simply put, I like and work in Ruby. There is no ulterior or mind-boggling reason.</p>
<h3 id="heading-what-does-this-ruby-script-do">What Does This Ruby Script Do?</h3>
<p>You provide the root folder path of your codebase to the script, and it will create Markdown files, each with a maximum size of 100KB. These files will contain code blocks with the content of each file, alongside a project structure tree. Files and folders listed in your <code>.gitignore</code> or specified in the script will be excluded.</p>
<h3 id="heading-why-markdown-and-100kb-files">Why Markdown and 100KB Files?</h3>
<ul>
<li><p><strong>Markdown</strong>: Markdown is a lightweight markup language that works well for documentation. It’s text-based, making it easy to read and compatible with most personal knowledge management tools like Notion or Obsidian.</p>
</li>
<li><p><strong>File Size Limitation</strong>: Some AI tools, especially Claude, do not read files larger than 100KB. Therefore, the script enforces this limit. You can adjust the limit by changing the script parameters.</p>
</li>
</ul>
<h2 id="heading-the-ruby-script-rubytomdrb">The Ruby Script: <code>ruby_to_md.rb</code></h2>
<p>Below is the Ruby script that converts your codebase into Markdown files:</p>
<p><a target="_blank" href="https://gist.github.com/sulmanweb/ee1541b1739b06db6695370cbc8a480d">https://gist.github.com/sulmanweb/ee1541b1739b06db6695370cbc8a480d</a></p>
<pre><code class="lang-ruby"><span class="hljs-keyword">require</span> <span class="hljs-string">'fileutils'</span>
<span class="hljs-keyword">require</span> <span class="hljs-string">'digest'</span>
ALWAYS_IGNORE = [<span class="hljs-string">'.git'</span>, <span class="hljs-string">'tmp'</span>, <span class="hljs-string">'log'</span>, <span class="hljs-string">'.ruby-lsp'</span>, <span class="hljs-string">'.github'</span>, <span class="hljs-string">'.devcontainer'</span>, <span class="hljs-string">'storage'</span>, <span class="hljs-string">'.annotaterb.yml'</span>, <span class="hljs-string">'public'</span>, <span class="hljs-string">'.cursorrules'</span>].freeze
IGNORED_EXTENSIONS = <span class="hljs-string">%w[.jpg .jpeg .png .gif .bmp .svg .webp .ico .pdf .tiff .raw .keep .gitkeep .sample .staging]</span>.freeze
MAX_FILE_SIZE = <span class="hljs-number">1_000_000</span> <span class="hljs-comment"># 1MB</span>
CHUNK_SIZE = <span class="hljs-number">100_000</span> <span class="hljs-comment"># 100KB</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">read_gitignore</span><span class="hljs-params">(directory_path)</span></span>
  gitignore_path = File.join(directory_path, <span class="hljs-string">'.gitignore'</span>)
  <span class="hljs-keyword">return</span> [] <span class="hljs-keyword">unless</span> File.exist?(gitignore_path)
  File.readlines(gitignore_path).map(&amp;<span class="hljs-symbol">:chomp</span>).reject(&amp;<span class="hljs-symbol">:empty?</span>)
<span class="hljs-keyword">end</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">ignored?</span><span class="hljs-params">(path, base_path, ignore_patterns)</span></span>
  relative_path = path.sub(<span class="hljs-string">"<span class="hljs-subst">#{base_path}</span>/"</span>, <span class="hljs-string">''</span>)
  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span> <span class="hljs-keyword">if</span> ALWAYS_IGNORE.any? { <span class="hljs-params">|dir|</span> relative_path.start_with?(dir + <span class="hljs-string">'/'</span>) <span class="hljs-params">||</span> relative_path == dir }
  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span> <span class="hljs-keyword">if</span> IGNORED_EXTENSIONS.<span class="hljs-keyword">include</span>?(File.extname(path).downcase) <span class="hljs-params">||</span> File.basename(path) == <span class="hljs-string">'.keep'</span>
  ignore_patterns.any? <span class="hljs-keyword">do</span> <span class="hljs-params">|pattern|</span>
    File.fnmatch?(pattern, relative_path, File::FNM_PATHNAME <span class="hljs-params">| File::FNM_DOTMATCH) |</span><span class="hljs-params">|
      File.fnmatch?(File.join('**', pattern), relative_path, File::FNM_PATHNAME |</span> File::FNM_DOTMATCH)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">convert_to_markdown</span><span class="hljs-params">(file_path)</span></span>
  extension = File.extname(file_path).downcase[<span class="hljs-number">1</span>..]
  format = extension.<span class="hljs-literal">nil</span>? <span class="hljs-params">||</span> extension.empty? ? <span class="hljs-string">'text'</span> : extension
  <span class="hljs-keyword">begin</span>
    content = File.read(file_path, <span class="hljs-symbol">encoding:</span> <span class="hljs-string">'UTF-8'</span>)
    <span class="hljs-string">"## <span class="hljs-subst">#{File.basename(file_path)}</span>\n\n```<span class="hljs-subst">#{format}</span>\n<span class="hljs-subst">#{content.strip}</span>\n```\n\n"</span>
  <span class="hljs-keyword">rescue</span> StandardError =&gt; e
    <span class="hljs-string">"## <span class="hljs-subst">#{File.basename(file_path)}</span>\n\n[File content not displayed: <span class="hljs-subst">#{e.message}</span>]\n\n"</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_tree_markdown</span><span class="hljs-params">(tree, prefix = <span class="hljs-string">''</span>)</span></span>
  result = <span class="hljs-string">''</span>
  tree.each <span class="hljs-keyword">do</span> <span class="hljs-params">|key, value|</span>
    result += <span class="hljs-string">"<span class="hljs-subst">#{prefix}</span>- <span class="hljs-subst">#{key}</span>\n"</span>
    result += generate_tree_markdown(value, prefix + <span class="hljs-string">'  '</span>) <span class="hljs-keyword">if</span> value.is_a?(Hash)
  <span class="hljs-keyword">end</span>
  result
<span class="hljs-keyword">end</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">write_chunked_output</span><span class="hljs-params">(output_file, content)</span></span>
  base_name = File.basename(output_file, <span class="hljs-string">'.*'</span>)
  extension = File.extname(output_file)
  dir_name = File.dirname(output_file)
  chunk_index = <span class="hljs-number">1</span>
  offset = <span class="hljs-number">0</span>
  <span class="hljs-keyword">while</span> offset &lt; content.length
    chunk = content[offset, CHUNK_SIZE]
    chunk_file = File.join(dir_name, <span class="hljs-string">"<span class="hljs-subst">#{base_name}</span>_part<span class="hljs-subst">#{chunk_index}</span><span class="hljs-subst">#{extension}</span>"</span>)
    File.open(chunk_file, <span class="hljs-string">'w:UTF-8'</span>) <span class="hljs-keyword">do</span> <span class="hljs-params">|file|</span>
      file.write(<span class="hljs-string">"---\n"</span>)
      file.write(<span class="hljs-string">"chunk: <span class="hljs-subst">#{chunk_index}</span>\n"</span>)
      file.write(<span class="hljs-string">"total_chunks: <span class="hljs-subst">#{(content.length.to_f / CHUNK_SIZE).ceil}</span>\n"</span>)
      file.write(<span class="hljs-string">"---\n\n"</span>)
      file.write(chunk)
    <span class="hljs-keyword">end</span>
    puts <span class="hljs-string">"Markdown file created: <span class="hljs-subst">#{chunk_file}</span>"</span>
    offset += CHUNK_SIZE
    chunk_index += <span class="hljs-number">1</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process_directory</span><span class="hljs-params">(directory_path, output_file)</span></span>
  ignore_patterns = read_gitignore(directory_path)
  markdown_content = <span class="hljs-string">"---\nencoding: utf-8\n---\n\n# Project Structure\n\n"</span>
  file_contents = []
  file_tree = {}
  Dir.glob(<span class="hljs-string">"<span class="hljs-subst">#{directory_path}</span>/**/*"</span>, File::FNM_DOTMATCH).each <span class="hljs-keyword">do</span> <span class="hljs-params">|file_path|</span>
    <span class="hljs-keyword">next</span> <span class="hljs-keyword">if</span> File.directory?(file_path)
    <span class="hljs-keyword">next</span> <span class="hljs-keyword">if</span> [<span class="hljs-string">'.'</span>, <span class="hljs-string">'..'</span>].<span class="hljs-keyword">include</span>?(File.basename(file_path))
    <span class="hljs-keyword">next</span> <span class="hljs-keyword">if</span> ignored?(file_path, directory_path, ignore_patterns)
    <span class="hljs-keyword">next</span> <span class="hljs-keyword">if</span> File.size(file_path) &gt; MAX_FILE_SIZE
    relative_path = file_path.sub(<span class="hljs-string">"<span class="hljs-subst">#{directory_path}</span>/"</span>, <span class="hljs-string">''</span>)
    parts = relative_path.split(<span class="hljs-string">'/'</span>)
    current = file_tree
    parts.each_with_index <span class="hljs-keyword">do</span> <span class="hljs-params">|part, index|</span>
      <span class="hljs-keyword">if</span> index == parts.size - <span class="hljs-number">1</span>
        current[part] = <span class="hljs-literal">nil</span>
      <span class="hljs-keyword">else</span>
        current[part] <span class="hljs-params">||</span>= {}
        current = current[part]
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>
    file_contents &lt;&lt; convert_to_markdown(file_path)
  <span class="hljs-keyword">end</span>
  markdown_content += generate_tree_markdown(file_tree)
  markdown_content += <span class="hljs-string">"\n# File Contents\n\n"</span>
  markdown_content += file_contents.join(<span class="hljs-string">"\n"</span>)
  write_chunked_output(output_file, markdown_content)
<span class="hljs-keyword">end</span>
<span class="hljs-keyword">if</span> ARGV.length != <span class="hljs-number">2</span>
  puts <span class="hljs-string">"Usage: ruby script.rb &lt;input_directory&gt; &lt;output_file&gt;"</span>
  exit <span class="hljs-number">1</span>
<span class="hljs-keyword">end</span>
input_directory = ARGV[<span class="hljs-number">0</span>]
output_file = ARGV[<span class="hljs-number">1</span>]
process_directory(input_directory, output_file)
</code></pre>
<h3 id="heading-script-usage">Script Usage</h3>
<p>The script requires two arguments:</p>
<ol>
<li><p><strong>Path to Codebase</strong>: The root directory of your project.</p>
</li>
<li><p><strong>Output File Name</strong>: The base name for the generated Markdown files.</p>
</li>
</ol>
<p>You can run the script, for example, as follows:</p>
<pre><code class="lang-bash">ruby ruby_to_md.rb ~/st/gradwinner gradwinner.md
</code></pre>
<p>This will create files like:</p>
<ul>
<li><p><code>gradwinner_part1.md</code></p>
</li>
<li><p><code>gradwinner_part2.md</code></p>
</li>
<li><p><code>gradwinner_part3.md</code> and so on.</p>
</li>
</ul>
<blockquote>
<p><strong>Note</strong>: Files and folders listed in <code>.gitignore</code> will be ignored in the resultant documentation files.</p>
</blockquote>
<h3 id="heading-key-script-features">Key Script Features</h3>
<ul>
<li><p><strong>ALWAYS_IGNORE</strong>: This constant lists folders and files that need to be ignored in addition to those specified in <code>.gitignore</code>.</p>
</li>
<li><p><strong>IGNORED_EXTENSIONS</strong>: This constant lists the file extensions (e.g., images) that should not be included in the documentation.</p>
</li>
<li><p><strong>CHUNK_SIZE</strong>: You can modify this constant to increase or decrease the amount of data in each Markdown file.</p>
</li>
<li><p><strong>MAX_FILE_SIZE</strong>: Files larger than this size will be ignored to prevent overwhelming the documentation.</p>
</li>
</ul>
<blockquote>
<p><strong>Note</strong>: Many chat tools have a limit of 25 files that can be uploaded at once, so you may need to adjust the script according to your requirements.</p>
</blockquote>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This Ruby script helps you convert a code repository into documentation in Markdown format, providing a structured overview and content breakdown. It’s an ideal solution when working with AI tools that have file size or file number limitations, making your codebase more accessible to them in a condensed and readable form.</p>
<p>If you have feedback or improvements to suggest, feel free to contribute!</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Implementing Multi-Select Enums in Ruby on Rails with PostgreSQL]]></title><description><![CDATA[In our previous article (linked below), we explored how to implement single-select enums in a Ruby on Rails application. In this post, we will extend that discussion to include the implementation of multi-select enums using arrays in Ruby on Rails wi...]]></description><link>https://sulmanweb.com/multi-select-enums-ruby-rails-postgresql</link><guid isPermaLink="true">https://sulmanweb.com/multi-select-enums-ruby-rails-postgresql</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Ruby]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Mon, 02 Dec 2024 05:00:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732802109619/89feb582-33b4-45ee-bb55-d879aa03fae0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In our previous article (linked below), we explored how to implement single-select enums in a Ruby on Rails application. In this post, we will extend that discussion to include the implementation of multi-select enums using arrays in Ruby on Rails with PostgreSQL.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://sulmanweb.com/postgresql-enum-types-rails-7-guide">https://sulmanweb.com/postgresql-enum-types-rails-7-guide</a></div>
<p> </p>
<h2 id="heading-introduction-to-multi-select-enums">Introduction to Multi-Select Enums</h2>
<p>Enums are a convenient way to handle a set of predefined values in your application. Sometimes, however, you might need to associate multiple values with a single record. For example, an article might belong to several categories such as "blog," "snippet," or "vlog." In this case, using an array of enums is a suitable solution that provides the flexibility to assign multiple categories.</p>
<h2 id="heading-why-use-enum-arrays">Why Use Enum Arrays?</h2>
<p>PostgreSQL enum arrays offer several advantages:</p>
<ul>
<li><p><strong>Better performance</strong> compared to string arrays due to internal optimizations.</p>
</li>
<li><p><strong>Type safety</strong> at the database level, reducing the chance of invalid data being saved.</p>
</li>
<li><p><strong>Reduced storage space</strong> compared to text arrays, making your database more efficient.</p>
</li>
<li><p><strong>Improved query performance</strong> with GIN (Generalized Inverted Index) indexing, which is particularly useful for fast lookups on array columns.</p>
</li>
</ul>
<h2 id="heading-setting-up-the-migration">Setting Up the Migration</h2>
<p>Let's implement a multi-category system for an <code>Article</code> model. First, generate the migration:</p>
<pre><code class="lang-bash">rails g migration AddCategoriesToArticles categories:enum
</code></pre>
<p>Modify the generated migration to define an enum type and add the new column:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AddCategoriesToArticles</span> &lt; ActiveRecord::Migration[7.2]</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change</span></span>
    create_enum <span class="hljs-symbol">:categories_enum</span>, <span class="hljs-string">%w[
      blog
      snippet
      vlog
    ]</span>
    add_column <span class="hljs-symbol">:articles</span>, <span class="hljs-symbol">:categories</span>, <span class="hljs-symbol">:enum</span>, <span class="hljs-symbol">enum_type:</span> <span class="hljs-symbol">:categories_enum</span>, <span class="hljs-symbol">array:</span> <span class="hljs-literal">true</span>, <span class="hljs-symbol">default:</span> []
    add_index <span class="hljs-symbol">:articles</span>, <span class="hljs-symbol">:categories</span>, <span class="hljs-symbol">using:</span> <span class="hljs-string">'gin'</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>In the migration above, the <code>array: true</code> option enables multiple values to be stored in the <code>categories</code> column. For optimal performance with array columns, PostgreSQL recommends using a GIN index to speed up querying.</p>
<p>Apply the migration:</p>
<pre><code class="lang-bash">rails db:migrate
</code></pre>
<h2 id="heading-model-configuration">Model Configuration</h2>
<p>Next, define the enum in your <code>Article</code> model with proper validations and methods:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Article</span> &lt; ApplicationRecord</span>
  validates <span class="hljs-symbol">:categories</span>, <span class="hljs-symbol">array_inclusion:</span> { <span class="hljs-symbol">in:</span> <span class="hljs-string">%w[blog snippet vlog]</span> }

  <span class="hljs-comment"># Remove duplicates before saving</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">categories=</span><span class="hljs-params">(types)</span></span>
    <span class="hljs-keyword">super</span>(types.uniq)
  <span class="hljs-keyword">end</span>

  <span class="hljs-comment"># Convenience method for checking category presence</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">has_category?</span><span class="hljs-params">(category)</span></span>
    categories.<span class="hljs-keyword">include</span>?(category)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>In this model, the <code>array_inclusion</code> validation ensures that only the specified strings are included in the <code>categories</code> array. The custom setter method <code>categories=</code> removes duplicate values before saving them to the database, ensuring data integrity.</p>
<h2 id="heading-usage-examples">Usage Examples</h2>
<p>Here are some examples of how you can work with multi-select enums in Rails:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># Create an article with multiple categories</span>
article = Article.create(<span class="hljs-symbol">categories:</span> [<span class="hljs-string">'blog'</span>, <span class="hljs-string">'vlog'</span>])

<span class="hljs-comment"># Add a category</span>
article.categories &lt;&lt; <span class="hljs-string">'snippet'</span>
article.save

<span class="hljs-comment"># Query articles with a specific category</span>
articles_with_blog = Article.where(<span class="hljs-string">"'blog' = ANY(categories)"</span>)
</code></pre>
<p>The <code>array_inclusion</code> validation ensures that only permitted values are stored, while the custom setter prevents duplicate categories. For optimal query performance when filtering by categories, PostgreSQL will utilize the GIN index automatically.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>PostgreSQL enum arrays in Rails provide a robust solution for handling multi-select fields with better performance and data integrity compared to string arrays. This pattern is particularly valuable for content management systems, tagging systems, or any scenario that requires multiple categorizations. By combining the use of enum arrays and proper indexing, you can ensure that your application remains performant as the data grows.</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item><item><title><![CDATA[Leveraging PostgreSQL 16 Enum Types in Rails 7+: A Step-by-Step Guide]]></title><description><![CDATA[If you're a Rails developer, you've probably heard about the recent improvements in Rails 8, including optimizations for SQLite. While I mainly use PostgreSQL for my projects, these updates have piqued my curiosity, and I'm always interested in explo...]]></description><link>https://sulmanweb.com/postgresql-enum-types-rails-7-guide</link><guid isPermaLink="true">https://sulmanweb.com/postgresql-enum-types-rails-7-guide</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Ruby]]></category><dc:creator><![CDATA[Sulman Baig]]></dc:creator><pubDate>Mon, 25 Nov 2024 05:00:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732364419520/cc219c6c-f272-4508-95f7-8e7be5260e07.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're a Rails developer, you've probably heard about the recent improvements in Rails 8, including optimizations for SQLite. While I mainly use PostgreSQL for my projects, these updates have piqued my curiosity, and I'm always interested in exploring what's new.</p>
<p>In this article, I'll guide you through leveraging PostgreSQL 16 enums in Rails 7+, showing you how to add an <code>enum</code> column to an <code>article</code> model, allowing you to define specific categories like <code>blog</code>, <code>snippet</code>, and <code>vlog</code>. Enums offer better data integrity and readability, making your code more maintainable.</p>
<h2 id="heading-adding-an-enum-column-to-your-rails-model">Adding an Enum Column to Your Rails Model</h2>
<p>To add a <code>category</code> enum column to the <code>articles</code> table, start by generating a migration:</p>
<pre><code class="lang-bash">rails g migration AddCategoryToArticles category:string
</code></pre>
<p>Next, modify the generated migration as follows to create an enum type and assign it to the column:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AddCategoryToArticles</span> &lt; ActiveRecord::Migration[7.2]</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change</span></span>
    create_enum <span class="hljs-symbol">:categories</span>, <span class="hljs-string">%w[
      blog
      snippet
      vlog
    ]</span>
    add_column <span class="hljs-symbol">:articles</span>, <span class="hljs-symbol">:category</span>, <span class="hljs-symbol">:enum</span>, <span class="hljs-symbol">enum_type:</span> <span class="hljs-symbol">:categories</span>
    add_index <span class="hljs-symbol">:articles</span>, <span class="hljs-symbol">:category</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Run the migration:</p>
<pre><code class="lang-bash">rails db:migrate
</code></pre>
<p>This will create an enum type called <code>categories</code> in PostgreSQL and add a corresponding enum column to the <code>articles</code> table. You can verify the change in the schema file, where you'll see both the enum definition and the updated column.</p>
<h2 id="heading-defining-enum-in-your-model">Defining Enum in Your Model</h2>
<p>Now, let's add the enum definition in the <code>Article</code> model:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Article</span> &lt; ApplicationRecord</span>
  enum <span class="hljs-symbol">:category</span>, {
    <span class="hljs-symbol">blog:</span> <span class="hljs-string">"blog"</span>,
    <span class="hljs-symbol">snippet:</span> <span class="hljs-string">"snippet"</span>,
    <span class="hljs-symbol">vlog:</span> <span class="hljs-string">"vlog"</span>,
  }, <span class="hljs-symbol">validate:</span> <span class="hljs-literal">true</span>

  validates <span class="hljs-symbol">:category</span>, <span class="hljs-symbol">presence:</span> <span class="hljs-literal">true</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The <code>validate: true</code> option ensures that the enum is validated automatically with inclusion checks. If the <code>category</code> field is required, make sure to backfill existing articles before adding <code>validates :category, presence: true</code>.</p>
<h2 id="heading-adding-a-scope-for-filtering-by-category">Adding a Scope for Filtering by Category</h2>
<p>To easily filter articles by their category, you can add a scope to your model:</p>
<pre><code class="lang-ruby">scope <span class="hljs-symbol">:by_category</span>, -&gt;(category) { where(<span class="hljs-symbol">category:</span> category) }
</code></pre>
<p>This scope allows you to perform queries like <code>Article.by_category("blog")</code> to filter articles by a specific category.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Using PostgreSQL enums in Rails provides type safety and keeps your database schema organized. This approach is particularly useful for fields with a fixed set of values, like categories in a blog or types in a content management system.</p>
<hr />
<p>Happy Coding!</p>
]]></content:encoded></item></channel></rss>