<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged ruby</title>
<link>https://coderlegion.com/tag/ruby</link>
<description>Powered by Question2Answer</description>
<item>
<title>What Is Idempotency?</title>
<link>https://coderlegion.com/33068/what-is-idempotency</link>
<description>&lt;p&gt;🚀 &lt;strong&gt;What Happens When Your API Receives the Same Request Twice?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Imagine a customer clicks &lt;strong&gt;“Pay Now”&lt;/strong&gt; twice because the page freezes.&lt;/p&gt;
&lt;p&gt;Or a payment gateway retries a request because it never received your response.&lt;/p&gt;
&lt;p&gt;💥 Without proper protection, your application might charge the customer twice!&lt;/p&gt;
&lt;p&gt;This is where &lt;strong&gt;Idempotency&lt;/strong&gt; becomes essential.&lt;/p&gt;
&lt;h4 id=&quot;-what-is-idempotency-&quot;&gt;🔍 What Is Idempotency?&lt;/h4&gt;
&lt;p&gt;Idempotency means that &lt;strong&gt;performing the same operation multiple times produces the same result as performing it once.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For example, if a client retries a payment request, the system should return the original result instead of creating another payment.&lt;/p&gt;
&lt;h4 id=&quot;-how-do-we-implement-it-in-ruby-on-rails-&quot;&gt;🛠️ How Do We Implement It in Ruby on Rails?&lt;/h4&gt;
&lt;p&gt;A common approach is using an &lt;strong&gt;Idempotency-Key&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The client sends a unique key with the request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;POST /api/v1/payments
Idempotency-Key: order-123-payment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rails can use that key to identify duplicate requests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Add a database constraint&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;add_index :payments, :idempotency_key, unique: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Store the key with the payment&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;Payment.create!(
  idempotency_key: idempotency_key,
  amount: amount,
  status: &quot;pending&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This is only a simplified example. A production implementation must handle duplicate-key exceptions, concurrent requests, and retries safely.&lt;/p&gt;
&lt;p&gt;You should also define what happens when the same key is reused with different request parameters.&lt;/p&gt;
&lt;p&gt;For payment APIs, store and return the original result for a matching key rather than processing the payment again.&lt;/p&gt;
&lt;h4 id=&quot;-why-is-idempotency-important-&quot;&gt;💎 Why Is Idempotency Important?&lt;/h4&gt;
&lt;p&gt;✅ &lt;strong&gt;Prevents Duplicate Payments&lt;/strong&gt; — Avoid charging customers twice when requests are retried.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Makes Retries Safer&lt;/strong&gt; — Network failures become easier to handle.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Improves API Reliability&lt;/strong&gt; — Clients can retry requests without accidentally repeating business operations.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Protects Background Jobs&lt;/strong&gt; — Prevent duplicate emails, orders, or other side effects when jobs run more than once.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Supports Distributed Systems&lt;/strong&gt; — Helps manage unreliable networks and repeated messages.&lt;/p&gt;
&lt;h4 id=&quot;-one-important-detail&quot;&gt;⚠️ One Important Detail&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Idempotency is not just checking whether a request has been received before.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You must handle concurrent requests, database transactions, unique constraints, and external service calls carefully.&lt;/p&gt;
&lt;p&gt;For example, a database transaction alone cannot guarantee that an external payment provider won't charge a customer twice.&lt;/p&gt;
&lt;p&gt;💡 &lt;strong&gt;The goal is simple: make retries safe, even when failures happen.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Because in real-world applications, requests will fail, networks will time out, and background jobs will retry.&lt;/p&gt;
&lt;p&gt;Your architecture should be ready for it!&lt;/p&gt;
&lt;p&gt;💬 &lt;strong&gt;How do you handle duplicate requests in your Rails applications?&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;rubyonrails-api-idempotency-backenddevelopment-softwarearchitecture-distributedsystems-webdevelopment-softwareengineering&quot;&gt;RubyOnRails #API #Idempotency #BackendDevelopment #SoftwareArchitecture #DistributedSystems #WebDevelopment #SoftwareEngineering&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33068/what-is-idempotency</guid>
<pubDate>Fri, 30 Oct 2026 09:28:35 +0000</pubDate>
</item>
<item>
<title>Building RAG on PostgreSQL with Pgvector and Ruby on rails</title>
<link>https://coderlegion.com/31432/building-rag-on-postgresql-with-pgvector-and-ruby-on-rails</link>
<description>&lt;p&gt;🧠 Your Database Is Already a Vector Database&lt;/p&gt;
&lt;p&gt;Most RAG tutorials start the same way: &quot;First, spin up a vector database.&quot;&lt;/p&gt;
&lt;p&gt;But Stop.&lt;/p&gt;
&lt;p&gt;If you're on Rails, you already have PostgreSQL. &lt;br&gt;
With the pgvector extension it can store and search embeddings  and for most applications, that's the whole answer.&lt;/p&gt;
&lt;p&gt;🐘 Setup is one migration&lt;/p&gt;
&lt;p&gt;`enable_extension &quot;vector&quot;&lt;/p&gt;
&lt;p&gt;add_column :document_chunks, :embedding, :vector, limit: 1536`&lt;/p&gt;
&lt;p&gt;💎 And one line in your model&lt;/p&gt;
&lt;h2 id=&quot;gemfile&quot;&gt;Gemfile&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gem &quot;neighbor&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;class DocumentChunk &amp;lt; ApplicationRecord
  has_neighbors :embedding, dimensions: 1536, normalize: true
end&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;🔍 Searching is just ActiveRecord&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DocumentChunk
  .where(tenant: Current.tenant)
  .nearest_neighbors(:embedding, query_vector, distance: &quot;cosine&quot;)
  .limit(6)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Scoped, chainable, joinable. A normal Rails query.&lt;/p&gt;
&lt;p&gt;🚀 Why this beats a separate vector DB&lt;/p&gt;
&lt;p&gt;1️⃣ No new infrastructure&lt;br&gt;
Nothing extra to deploy, monitor, back up, or upgrade.&lt;/p&gt;
&lt;p&gt;2️⃣ Transactional consistency&lt;br&gt;
Embeddings commit in the same transaction as the record. Two systems can't drift apart.&lt;/p&gt;
&lt;p&gt;3️⃣ Authorization you already trust&lt;br&gt;
This is the big one. Retrieval almost always needs scoping  by tenant, project, or permission. In Postgres that's a WHERE clause. In an external store it's metadata filtering you have to keep in sync with your auth model.&lt;br&gt;
Get it wrong, and one customer's documents land in another customer's LLM prompt.&lt;/p&gt;
&lt;p&gt;4️⃣ Real joins&lt;br&gt;
Chunks plus their documents, authors, and timestamps in one query.&lt;/p&gt;
&lt;p&gt;⚠️ Three things that will bite you&lt;/p&gt;
&lt;p&gt;❌ Index opclass mismatch&lt;br&gt;
vector_cosine_ops goes with cosine distance. Mismatch them and Postgres silently skips your index and sequential-scans the table.&lt;/p&gt;
&lt;p&gt;❌ No similarity floor&lt;br&gt;
Nearest neighbours are nearest, not relevant. Without a threshold, an unrelated question still returns chunks — which the model will happily use to hallucinate.&lt;/p&gt;
&lt;p&gt;❌ Pure vector search&lt;br&gt;
Embeddings are weak at exactly what users type literally: error codes, SKUs, invoice numbers. ERR_4012 and ERR_4013 are nearly identical in vector space.&lt;br&gt;
Fix: hybrid search — pgvector + Postgres full-text, fused with Reciprocal Rank Fusion.&lt;/p&gt;
&lt;p&gt;🎯 When to actually leave Postgres&lt;/p&gt;
&lt;p&gt;Past roughly 5–10 million chunks, or when the vector workload starves your transactional workload for memory.&lt;br&gt;
Until then, a dedicated vector database buys you one more service to operate and a second place for your authorization logic to be wrong.&lt;/p&gt;
&lt;p&gt;Your database stays the source of truth. The vector index is a lookup layer on top of it.&lt;/p&gt;
&lt;p&gt;💬 If you've shipped RAG in Rails — what pushed you off Postgres, if anything?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31432/building-rag-on-postgresql-with-pgvector-and-ruby-on-rails</guid>
<pubDate>Fri, 16 Oct 2026 18:17:18 +0000</pubDate>
</item>
<item>
<title>Elasticsearch with Ruby on Rails</title>
<link>https://coderlegion.com/30406/elasticsearch-with-ruby-on-rails</link>
<description>&lt;p&gt;🚀 &lt;strong&gt;Elasticsearch + Ruby on Rails: Beyond Database Queries&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ever noticed how searching millions of records can become slow and frustrating?&lt;/p&gt;
&lt;p&gt;When your application needs &lt;strong&gt;fast, advanced, and intelligent search&lt;/strong&gt;, Elasticsearch can make a real difference.&lt;/p&gt;
&lt;h4 id=&quot;-what-is-elasticsearch-&quot;&gt;🔍 What is Elasticsearch?&lt;/h4&gt;
&lt;p&gt;Elasticsearch is a distributed search and analytics engine built on Apache Lucene. It helps applications search, filter, and analyze large amounts of data efficiently.&lt;/p&gt;
&lt;p&gt;Unlike traditional database queries, it offers powerful full-text search, relevance scoring, and flexible filtering.&lt;/p&gt;
&lt;h4 id=&quot;-how-do-we-use-it-with-ruby-on-rails-&quot;&gt;🛠️ How do we use it with Ruby on Rails?&lt;/h4&gt;
&lt;p&gt;Imagine you're building an e-commerce platform with thousands of products.&lt;/p&gt;
&lt;p&gt;Users expect to search by product name, description, category, or even partial keywords — and get relevant results quickly.&lt;/p&gt;
&lt;p&gt;With Rails, you can integrate Elasticsearch using gems such as &lt;code&gt;elasticsearch-rails&lt;/code&gt; or &lt;code&gt;searchkick&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example, using Searchkick:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# Gemfile
gem &quot;searchkick&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class Product &amp;lt; ApplicationRecord
  searchkick

  def search_data
    {
      name: name,
      description: description,
      category: category
    }
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Search products with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;Product.search(&quot;wireless headphones&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of relying only on basic SQL matching, Elasticsearch can provide relevance-based results, typo tolerance, and advanced filtering.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: Elasticsearch must be installed and configured, and the data must be indexed before searching.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;-why-use-elasticsearch-&quot;&gt;💡 Why use Elasticsearch?&lt;/h4&gt;
&lt;p&gt;✅ &lt;strong&gt;Faster Search&lt;/strong&gt; — Search large datasets efficiently.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Better Relevance&lt;/strong&gt; — Rank results based on how closely they match the query.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Typo Tolerance&lt;/strong&gt; — Help users find results even when they misspell a word.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Advanced Filtering&lt;/strong&gt; — Combine full-text search with categories, prices, and other filters.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Analytics &amp;amp; Aggregations&lt;/strong&gt; — Analyze search trends and summarize large datasets.&lt;/p&gt;
&lt;h4 id=&quot;-one-important-architectural-decision&quot;&gt;⚠️ One important architectural decision&lt;/h4&gt;
&lt;p&gt;Elasticsearch is not a replacement for your primary database.&lt;/p&gt;
&lt;p&gt;Use PostgreSQL or MySQL as your source of truth, and Elasticsearch as your search engine.&lt;/p&gt;
&lt;p&gt;When Rails data changes, keep the search index synchronized — often through background jobs with Sidekiq — and account for indexing delays and failed jobs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The goal isn't to replace SQL. It's to give your application a search experience that SQL alone may struggle to deliver at scale.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30406/elasticsearch-with-ruby-on-rails</guid>
<pubDate>Fri, 09 Oct 2026 06:22:39 +0000</pubDate>
</item>
<item>
<title>API Architecture</title>
<link>https://coderlegion.com/29816/api-architecture</link>
<description>&lt;p&gt;Here’s a concise LinkedIn-ready article focused on practical API architecture in Rails.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;API Architecture: It’s More Than Just Endpoints&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A well-designed API isn't simply about creating routes like:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GET /users&lt;/code&gt;&lt;br&gt;
&lt;code&gt;POST /orders&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The real challenge is designing an architecture that remains &lt;strong&gt;secure, scalable, maintainable, and easy to integrate&lt;/strong&gt; as the application grows.&lt;/p&gt;
&lt;h4 id=&quot;-how-i-approach-api-architecture-with-ruby-on-rails&quot;&gt;️ How I approach API architecture with Ruby on Rails&lt;/h4&gt;
&lt;p&gt;A typical Rails API can be structured around clear layers:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Client → Routes → Controllers → Services → Models → Database&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;POST /api/v1/orders&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The controller should handle the HTTP request and response, while the business logic can live in a &lt;strong&gt;Service Object&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;Orders::Create.call(
  user: current_user,
  params: order_params
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps controllers thin and business logic easier to test and reuse.&lt;/p&gt;
&lt;h4 id=&quot;important-architecture-decisions&quot;&gt;Important architecture decisions&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Versioning&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/api/v1/...&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;API versioning allows you to evolve the API without immediately breaking existing clients.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Authentication &amp;amp; Authorization&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use mechanisms such as JWT, OAuth2, or token-based authentication, combined with authorization rules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consistent Responses&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Define predictable JSON structures for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Success responses&lt;/li&gt;
&lt;li&gt;Validation errors&lt;/li&gt;
&lt;li&gt;Authentication failures&lt;/li&gt;
&lt;li&gt;API errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Background Processing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Heavy operations shouldn't block the API request.&lt;/p&gt;
&lt;p&gt;Rails + &lt;strong&gt;Sidekiq + Redis&lt;/strong&gt; can move tasks such as emails, notifications, and data processing into background jobs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Caching&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Frequently requested data can be cached with Redis to reduce database load and improve response time.&lt;/p&gt;
&lt;h4 id=&quot;why-good-api-architecture-matters&quot;&gt;Why good API architecture matters&lt;/h4&gt;
&lt;p&gt;A good architecture makes it easier to:&lt;/p&gt;
&lt;p&gt;✅ Scale the application&lt;br&gt;
✅ Add new API versions&lt;br&gt;
✅ Integrate third-party systems&lt;br&gt;
✅ Test business logic&lt;br&gt;
✅ Maintain clean controllers&lt;br&gt;
✅ Improve security&lt;br&gt;
✅ Reduce technical debt&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The goal isn't to build more endpoints.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The goal is to build an API that can &lt;strong&gt;evolve without becoming a bottleneck for the entire system.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/mhdrs72mej30ce0sh4a8.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;rubyonrails-ruby-api-apidesign-softwarearchitecture-backenddevelopment-webdevelopment-restapi-softwareengineering&quot;&gt;RubyOnRails #Ruby #API #APIDesign #SoftwareArchitecture #BackendDevelopment #WebDevelopment #RESTAPI #SoftwareEngineering&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29816/api-architecture</guid>
<pubDate>Mon, 05 Oct 2026 08:01:11 +0000</pubDate>
</item>
<item>
<title>One Rails App, Multiple Customers</title>
<link>https://coderlegion.com/29815/one-rails-app-multiple-customers</link>
<description>&lt;p&gt; Why Multi-Tenancy Matters&lt;/p&gt;
&lt;p&gt;Imagine you are building a SaaS application.&lt;/p&gt;
&lt;p&gt;You have &lt;strong&gt;100 companies&lt;/strong&gt;, but you don't want to deploy and maintain 100 separate applications.&lt;/p&gt;
&lt;p&gt;That's where &lt;strong&gt;Multi-Tenancy&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;h4 id=&quot;what-is-multi-tenancy-&quot;&gt;What is Multi-Tenancy?&lt;/h4&gt;
&lt;p&gt;Multi-tenancy allows a single application to serve multiple customers — called &lt;strong&gt;tenants&lt;/strong&gt; — while keeping each tenant's data isolated.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                    Rails Application
                           │
          ┌────────────────┼────────────────┐
          ↓                ↓                ↓
       Tenant A         Tenant B         Tenant C
       Company A        Company B        Company C
          │                │                │
       Users/Data       Users/Data       Users/Data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each company uses the same application, but should only be able to access &lt;strong&gt;its own data&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;how-can-we-implement-it-with-rails-&quot;&gt;How can we implement it with Rails?&lt;/h4&gt;
&lt;p&gt;A common approach is adding a &lt;code&gt;tenant_id&lt;/code&gt; to tenant-owned tables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class User &amp;lt; ApplicationRecord
  belongs_to :tenant
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then queries are scoped to the current tenant:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;current_tenant.users
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/wz8nqkg6jgu429eyrwr7.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;For larger systems, another approach is &lt;strong&gt;database-per-tenant&lt;/strong&gt;, where each customer gets a separate database.&lt;/p&gt;
&lt;p&gt;The right strategy depends on your scale, security requirements, infrastructure, and operational complexity.&lt;/p&gt;
&lt;h4 id=&quot;why-use-multi-tenancy-&quot;&gt;Why use Multi-Tenancy?&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1️⃣ Lower Infrastructure Costs&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One application can serve many customers instead of running separate deployments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2️⃣ Easier Maintenance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Deploy features and bug fixes once instead of updating every customer environment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3️⃣ Faster Scaling&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can scale application servers and background workers as the customer base grows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4️⃣ Better SaaS Architecture&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Multi-tenancy fits naturally with subscription-based products where many businesses share the same platform.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5️⃣ Centralized Development&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One codebase means consistent features, security updates, and improvements across tenants.&lt;/p&gt;
&lt;h4 id=&quot;-the-important-part&quot;&gt;⚠️ The important part&lt;/h4&gt;
&lt;p&gt;Multi-tenancy isn't simply adding a &lt;code&gt;tenant_id&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You need to think about:&lt;/p&gt;
&lt;p&gt; Data isolation&lt;br&gt;
️ Authorization&lt;br&gt;
️ Database architecture&lt;br&gt;
⚡ Caching&lt;br&gt;
 Background jobs&lt;br&gt;
 Reporting&lt;br&gt;
 Tenant-aware integrations&lt;/p&gt;
&lt;p&gt;A single missing tenant filter can become a serious data-isolation problem.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29815/one-rails-app-multiple-customers</guid>
<pubDate>Mon, 05 Oct 2026 08:00:48 +0000</pubDate>
</item>
<item>
<title>How you zip larger files in ruby</title>
<link>https://coderlegion.com/24061/how-you-zip-larger-files-in-ruby</link>
<description>&lt;p&gt;Easy way that compress files in ruby is using &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rubydoc.info/github/rubyzip/rubyzip/Zip/File&quot;&gt;zip&lt;/a&gt; lib.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;Zip::File.open('./compress.zip', create: true) do |zip|
  zip.get_output_stream(&quot;./stuff_to_ziped.pdf&quot;) do |f|
    f.puts File.read(&quot;./stuff_to_zip.pdf&quot;)
   end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you need to zip larger files or when you need to perform any task that requires opening many files, good practice is to avoid loading all files into memory. To achieve this, you can use stream operations available in IO classes.&lt;/p&gt;
&lt;p&gt;Let's look at the code above again but with a thread that monitory the memory for us.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;Thread.new do
  loop do
    print &quot;\r memory: %d MB&quot; % &quot;#{`ps -o rss= -p #{Process.pid}`.to_i/1024}\t&quot;
  end
end

Zip::File.open('./compress.zip', create: true) do |zip|
  zip.get_output_stream(&quot;./stuff_to_ziped.pdf&quot;) do |f|
    f.puts File.read(&quot;./stuff_to_zip.pdf&quot;)
   end
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can see how much memory the program consumis until done. The method File.read load all bytes of file in memory. Lets see other implementation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;Thread.new do
  loop do
    print &quot;\r memory: %d MB&quot; % &quot;#{`ps -o rss= -p #{Process.pid}`.to_i/1024}\t&quot;
  end
end


Zip::File.open('./compactados.zip', create: true) do |zip|
	10.times do |i|
		zip.get_output_stream(&quot;./copy_#{i}.pdf&quot;) do |f| 
			input = File.new(&quot;./copy_#{i}.pdf&quot;)
			while !input.eof?
				f.puts(input.gets)
			end
		end
	end
end
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24061/how-you-zip-larger-files-in-ruby</guid>
<pubDate>Mon, 03 Aug 2026 21:42:38 +0000</pubDate>
</item>
<item>
<title>Web &quot;Hello World&quot; in Ruby without RoR!</title>
<link>https://coderlegion.com/24060/web-hello-world-in-ruby-without-ror</link>
<description>&lt;p&gt;If you want to understand how things work beyond what's under the hood, the first good step is to try to write something from scratch. So, in this post, I will show you how to write a 'hello world' app for the web in pure Ruby. Above, you'll find the simple 'Hello World' code example.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class MyApp
 def say_hello
   &quot;Hello World&quot;
 end
end
MyApp.new.say_hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to turn this code into a Rack app. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rack/rack&quot;&gt;Rack&lt;/a&gt; is an interface for developing web applications in Ruby. The structure of a Rack app looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;run do |env|
  [200, {}, [&quot;Hello World&quot;]]
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, the code works like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class MyApp
 def say_hello
   &quot;Hello World&quot;
 end
end

myapp = proc do |env|
  [200, {'Content-Type' =&amp;gt; 'text/plain'}, [MyApp.new.say_hello]]
end
run myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run this using the rackup gem or another supported web server.(&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rack/rack#usage&quot;&gt;read more here&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;rackup myapp.ru
--&amp;gt; Puma starting in single mode...
--&amp;gt; * Version 4.2.0 (ruby 2.6.5-p114), codename: Distant Airhorns
--&amp;gt; * Min threads: 0, max threads: 16
--&amp;gt; * Environment: development
--&amp;gt; * Listening on tcp://127.0.0.1:9292
--&amp;gt; * Listening on tcp://[::1]:9292
--&amp;gt; Use Ctrl-C to stop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can access the app by typing in your browser. &lt;a rel=&quot;nofollow&quot; href=&quot;denied:127.0.0.1:9292&quot;&gt;127.0.0.1:9292&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24060/web-hello-world-in-ruby-without-ror</guid>
<pubDate>Mon, 03 Aug 2026 21:41:20 +0000</pubDate>
</item>
<item>
<title>Skip the RSpec examples you don't need: rspec-tracer 2.0 in pre-release</title>
<link>https://coderlegion.com/17611/skip-the-rspec-examples-you-dont-need-rspec-tracer-2-0-in-pre-release</link>
<description>&lt;p&gt;A ground-up rewrite of test-skip caching for RSpec, with pluggable storage, Rails-aware tracking, and a per-example tracks: DSL.&lt;/p&gt;
&lt;p&gt;You change one model. You run &lt;code&gt;bundle exec rspec&lt;/code&gt;. Twenty minutes later, every test in the suite has run again — including the 1,800 examples that don't touch that model.&lt;/p&gt;
&lt;p&gt;This is the problem &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/avmnu-sng/rspec-tracer&quot;&gt;rspec-tracer&lt;/a&gt; solves. Cold run of a suite: everything runs. Second run with no edits: nothing runs. Edit one file: only the examples that depend on that file re-run.&lt;/p&gt;
&lt;p&gt;The 1.x line has been on RubyGems since 2021. I just shipped a 2.0 pre-release after a 4-year gap and a 17-day rebuild. This post is the field guide for what changed and how to test it.&lt;/p&gt;
&lt;h3 id=&quot;install&quot;&gt;Install&lt;/h3&gt;
&lt;p&gt;Pre-releases require explicit opt-in. In your &lt;code&gt;Gemfile&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# 2.0 is in pre-release. Pin to the pre-release version explicitly;
# switch to '~&amp;gt; 2.0' once 2.0.0 final ships.
gem 'rspec-tracer', '= 2.0.0.pre.1', group: :test, require: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or one-shot:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;gem install rspec-tracer --pre
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then in your &lt;code&gt;spec_helper.rb&lt;/code&gt; or &lt;code&gt;rails_helper.rb&lt;/code&gt;, &lt;strong&gt;before any application code:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;require 'rspec_tracer'
RSpecTracer.start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it for the basic install. Run your suite twice; the second run skips everything that didn't change.&lt;/p&gt;
&lt;h3 id=&quot;what-changed-since-1-x&quot;&gt;What changed since 1.x&lt;/h3&gt;
&lt;p&gt;The 2.0 surface is rebuilt around an explicit input-taxonomy model (every test is a pure function of its inputs; cache invalidation is input-digest mismatch). What that translates to for users:&lt;/p&gt;
&lt;h4 id=&quot;1-pluggable-storage&quot;&gt;1. Pluggable storage&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# Default: JSON (preserves 1.x layout)
storage_backend :json

# SQLite: single-file DB, MRI-only
storage_backend :sqlite
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For large suites (5,000+ examples) the SQLite backend cuts cache-load time substantially. JRuby falls back to &lt;code&gt;:json&lt;/code&gt; automatically with a one-time warn.&lt;/p&gt;
&lt;h4 id=&quot;2-pluggable-remote-cache-for-ci-&quot;&gt;2. Pluggable remote cache (for CI)&lt;/h4&gt;
&lt;p&gt;The 1.x line had S3-only remote cache. 2.0 ships three backends:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# S3 (preserves the 1.x layout, including LocalStack support)
remote_cache_backend :s3, bucket: 'my-cache', prefix: 'rspec-tracer'

# Local filesystem (NFS / shared mount / dev cache)
remote_cache_backend :local_fs, path: '/mnt/shared-cache'

# Redis (with optional per-key TTL + PR-branches sidecar)
remote_cache_backend :redis,
  url: ENV['REDIS_URL'],
  ttl: 7 * 24 * 3600,
  prefix: 'rspec-tracer'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CI flow (&lt;code&gt;rake rspec_tracer:remote_cache:download&lt;/code&gt; / &lt;code&gt;:upload&lt;/code&gt;) is preserved bit-for-bit. The only thing that changes is the backend you point at.&lt;/p&gt;
&lt;h4 id=&quot;3-per-example-tracks-dsl&quot;&gt;3. Per-example &lt;code&gt;tracks:&lt;/code&gt; DSL&lt;/h4&gt;
&lt;p&gt;This is the headline new surface. RSpec metadata key:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;RSpec.describe AdminController,
               tracks: { files: 'app/policies/**/*.rb', env: 'ROLE_CONFIG' } do
  it 'gates on the feature flag' do
    # ...
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use cases the tracker can't auto-observe:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Config files baked at boot&lt;/strong&gt; — Oj-loaded JSON / YAML that's read once and mmap'd; the IO hooks won't see runtime reads, so declared globs are the escape hatch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Env-var branches&lt;/strong&gt; — code that does &lt;code&gt;if ENV['FEATURE_X'] == 'on'&lt;/code&gt;; flipping the env should invalidate the example.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pre-compiled Ruby constants&lt;/strong&gt; — values baked at boot that the per-example coverage delta can't see.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cascade is union (parent + child both contribute), unlike RSpec's default metadata cascade which clobbers on shared keys.&lt;/p&gt;
&lt;h4 id=&quot;4-track-env-names-config-dsl&quot;&gt;4. &lt;code&gt;track_env(*names)&lt;/code&gt; config DSL&lt;/h4&gt;
&lt;p&gt;Symmetric to &lt;code&gt;track_files&lt;/code&gt; but for env vars every test depends on:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# In .rspec-tracer
track_env 'AUTH_TOKEN', 'DATABASE_URL', 'RAILS_*', '*_API_KEY'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Single-wildcard patterns supported: &lt;code&gt;'PREFIX_*'&lt;/code&gt;, &lt;code&gt;'*_SUFFIX'&lt;/code&gt;, bare &lt;code&gt;'*'&lt;/code&gt;. Patterns are expanded against the live ENV at config-load time; the persisted &lt;code&gt;env_snapshot.json&lt;/code&gt; carries concrete keys only.&lt;/p&gt;
&lt;h4 id=&quot;5-rails-preset&quot;&gt;5. Rails preset&lt;/h4&gt;
&lt;p&gt;For Rails projects, the tracker has to attach inputs Coverage doesn't observe — views, locales, fixtures, factories, helpers, config YAML, schema. The preset handles this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# In .rspec-tracer
track_rails_defaults
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Opt out per category:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;track_rails_defaults except: [:views, :locales]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Plus auto-detection: when &lt;code&gt;::Rails::VERSION&lt;/code&gt; is defined at &lt;code&gt;RSpecTracer.start&lt;/code&gt; time, the engine attaches an &lt;code&gt;ActionView&lt;/code&gt; template subscriber + an opt-in AR schema subscriber.&lt;/p&gt;
&lt;p&gt;The ERB/Slim/Jbuilder tracking via &lt;code&gt;render_template.action_view&lt;/code&gt; was the most-requested missing 1.x feature. It's the canonical path now: a view edit invalidates exactly the examples that rendered the partial.&lt;/p&gt;
&lt;h4 id=&quot;6-bin-rspec-tracer-cli&quot;&gt;6. &lt;code&gt;bin/rspec-tracer&lt;/code&gt; CLI&lt;/h4&gt;
&lt;p&gt;Five sub-commands, all opt-in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ rspec-tracer doctor
OK   ruby:        ruby 3.3.10 (2026-04-18)
OK   rspec-tracer: 2.0.0.pre.1
OK   root:        /Users/me/projects/my-app
OK   cache_path:  /Users/me/projects/my-app/rspec_tracer_cache
INFO SimpleCov:   not loaded (this is fine; SimpleCov is optional)
OK   Rails:       7.2.0.1
OK   schema:      v3 (matches gem)
INFO remote_cache: not configured (skip)

$ rspec-tracer cache:info
Cache size: 14.4 MiB · last run: 2026-05-06T14:13:22Z · 12 runs retained

$ rspec-tracer cache:clear
Removed: rspec_tracer_cache/, rspec_tracer_coverage/, rspec_tracer_report/

$ rspec-tracer report:open
Opening rspec_tracer_report/index.html ...

$ rspec-tracer explain spec.foo_spec.rb_42
Example: spec/foo_spec.rb:42 - &quot;GET /admin returns 403 for unauthenticated&quot;
Status: re-run scheduled
Reason: Files changed (app/controllers/admin_controller.rb)
Dependencies (12):
  - app/controllers/admin_controller.rb [changed since cache]
  - app/policies/admin_policy.rb
  - config/locales/en.yml
  - ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CLI is opt-in for local dev convenience; the &lt;code&gt;rake rspec_tracer:remote_cache:*&lt;/code&gt; tasks remain first-class for CI integration.&lt;/p&gt;
&lt;h4 id=&quot;7-html-json-reporters&quot;&gt;7. HTML + JSON reporters&lt;/h4&gt;
&lt;p&gt;The 1.x line had a single HTML reporter. 2.0 adds a JSON reporter for CI dashboards and a terminal-summary reporter that fires after every run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;rspec-tracer: 1887 examples tracked · 5 re-run · 1882 skipped (99% cached)
by reason: 5 Files changed
cache: /path/to/rspec_tracer_cache (14.4 MiB; +6.7 KiB vs prev run)
report: /path/to/rspec_tracer_report/report.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For parallel_tests users: 1.x emitted reports per worker, then &lt;code&gt;purge_worker_dirs!&lt;/code&gt; deleted them — leaving users with no usable output. 2.0 emits ONCE at the merged top-level location after all workers finish.&lt;/p&gt;
&lt;h4 id=&quot;8-simplecov-branch-coverage-now-works&quot;&gt;8. SimpleCov branch coverage now works&lt;/h4&gt;
&lt;p&gt;The 1.x caveat (&quot;SimpleCov would not report branch coverage results even when enabled&quot;) is gone. The coverage-stack rewrite decoupled rspec-tracer's line-only emission from SimpleCov's branch tracking. Users who turned &lt;code&gt;enable_coverage :branch&lt;/code&gt; off when adopting rspec-tracer 1.x can re-enable in 2.0.&lt;/p&gt;
&lt;h4 id=&quot;9-boot-time-warns&quot;&gt;9. Boot-time warns&lt;/h4&gt;
&lt;p&gt;For two common user-trust traps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SimpleCov loaded but not started before &lt;code&gt;RSpecTracer.start&lt;/code&gt; (load-order is part of the documented contract; the warn surfaces it instead of silently bolting onto a Coverage already in flight).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;track_ar_schema_notifications&lt;/code&gt; enabled with &lt;code&gt;use_transactional_fixtures&lt;/code&gt; defaulting to true (per-example BEGIN/COMMIT fires &lt;code&gt;sql.active_record&lt;/code&gt; and attributes &lt;code&gt;db/schema.rb&lt;/code&gt; to every AR-touching example; the warn points at the README &quot;Narrow AR-schema attribution&quot; guidance).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;10-schema-version-field-rails-8-0-ci-recipes&quot;&gt;10. Schema-version field + Rails 8.0 + CI recipes&lt;/h4&gt;
&lt;p&gt;Schema-version field in &lt;code&gt;last_run.json&lt;/code&gt; for explicit cross-version cache validation. Cache-shape changes bump the field; mismatched caches refuse-to-load with an info-level &quot;cold run&quot; log line.&lt;/p&gt;
&lt;p&gt;Rails 8.0 is CI-gated (Ruby 3.2+). &lt;code&gt;docs/CI_RECIPES.md&lt;/code&gt; translates the GitHub Actions cache pattern to CircleCI / GitLab CI / Buildkite / Heroku CI — the 4-component cache-key (&lt;code&gt;runner.os&lt;/code&gt; + &lt;code&gt;.ruby-version&lt;/code&gt; + &lt;code&gt;lib/rspec_tracer/version.rb&lt;/code&gt; + Gemfile-hash) translates 1:1 across providers.&lt;/p&gt;
&lt;h3 id=&quot;migration-from-1-x&quot;&gt;Migration from 1.x&lt;/h3&gt;
&lt;p&gt;The full guide is in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/avmnu-sng/rspec-tracer/blob/main/UPGRADING.md&quot;&gt;UPGRADING.md&lt;/a&gt;. Headlines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ruby ≥ 3.1&lt;/strong&gt; is the floor (1.x supported Ruby 2.5+). Ruby 3.2 / 3.3 / 3.4 / 4.0 + JRuby 9.4 are CI-gated. Rails 7.0 / 7.1 / 7.2 / 8.0 + RSpec 3.12 / 3.13 are CI-gated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache schema bump&lt;/strong&gt; — first run on 2.0 is cold; warm caches resume on subsequent runs. Existing CI integration (the rake tasks, env vars, &lt;code&gt;rspec_tracer_cache/&lt;/code&gt; directory contracts) is preserved bit-for-bit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deprecated&lt;/strong&gt; — four configs continue to work with one warn line each, slated for removal in 3.0: &lt;code&gt;reports_s3_path&lt;/code&gt; / &lt;code&gt;use_local_aws&lt;/code&gt; config DSL, and &lt;code&gt;RSPEC_TRACER_REPORTS_S3_PATH&lt;/code&gt; / &lt;code&gt;RSPEC_TRACER_USE_LOCAL_AWS&lt;/code&gt; env vars.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Removed&lt;/strong&gt; — cucumber feature-file integration suite (contributor-facing only), Ruby ≤ 3.0 / Rails ≤ 6.x support, Windows.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-pre-release&quot;&gt;Why pre-release&lt;/h3&gt;
&lt;p&gt;A 2.0 of a gem people use in their CI deserves a real testing window. The pre-release is on RubyGems with &lt;code&gt;--pre&lt;/code&gt; opt-in; 2.0.0 final is targeted in ~2 weeks once the observation-window feedback is incorporated.&lt;/p&gt;
&lt;p&gt;If you're a 1.x user: the migration is &lt;code&gt;bundle update rspec-tracer&lt;/code&gt; after pinning the pre-release version, plus optionally adopting &lt;code&gt;track_rails_defaults&lt;/code&gt; if you're on Rails. The first run is cold (cache schema cut); subsequent runs return to warm.&lt;/p&gt;
&lt;p&gt;If you're new to rspec-tracer: try the cookbook recipes; the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/avmnu-sng/rspec-tracer#quick-start&quot;&gt;Quick start&lt;/a&gt; gets a fresh project running in ~5 minutes.&lt;/p&gt;
&lt;h3 id=&quot;where-to-send-feedback&quot;&gt;Where to send feedback&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Discussions&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/avmnu-sng/rspec-tracer/discussions/180&quot;&gt;the pre-release thread&lt;/a&gt; for usage questions, install reports, rough edges.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Issues&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/avmnu-sng/rspec-tracer/issues/new/choose&quot;&gt;open one&lt;/a&gt; for bugs. Add the &lt;code&gt;2.0-feedback&lt;/code&gt; label if applicable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migration questions&lt;/strong&gt;: comment on the discussion thread; I'll answer in the open so others see.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'll cut a &lt;code&gt;pre.2&lt;/code&gt; if pre.1 surfaces blockers; otherwise 2.0.0.rc.1 in ~2 weeks, then 2.0.0 final after a final review window.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Repo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/avmnu-sng/rspec-tracer&quot;&gt;https://github.com/avmnu-sng/rspec-tracer&lt;/a&gt;&lt;br&gt;
Release: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/avmnu-sng/rspec-tracer/releases/tag/v2.0.0.pre.1&quot;&gt;https://github.com/avmnu-sng/rspec-tracer/releases/tag/v2.0.0.pre.1&lt;/a&gt;&lt;br&gt;
Discussion: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/avmnu-sng/rspec-tracer/discussions/180&quot;&gt;https://github.com/avmnu-sng/rspec-tracer/discussions/180&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thanks for reading. Bug reports + thoughts welcome.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17611/skip-the-rspec-examples-you-dont-need-rspec-tracer-2-0-in-pre-release</guid>
<pubDate>Tue, 12 May 2026 17:28:12 +0000</pubDate>
</item>
<item>
<title>I Built a Bash Script That Traces Code Faster Than Your IDE (And Saves AI Tokens)</title>
<link>https://coderlegion.com/13522/i-built-a-bash-script-that-traces-code-faster-than-your-ide-and-saves-ai-tokens</link>
<description>&lt;p&gt;You're debugging a Rails app. The bug is somewhere in &lt;code&gt;process_payment&lt;/code&gt;. You could:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A)&lt;/strong&gt; Open your IDE, wait for LSP to index, click through 12 files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;B)&lt;/strong&gt; Paste the whole service into ChatGPT. Hope it doesn't hallucinate the call graph.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;C)&lt;/strong&gt; Run one command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codetracer process_payment . --mode flow --scope
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In under a second: where it's defined, every call site with enclosing method, every assignment, every place the return value goes — across all files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I chose option C.&lt;/strong&gt; So I built it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-codetracer-&quot;&gt;What is codetracer?&lt;/h3&gt;
&lt;p&gt;A single-file bash script (~1400 lines) that traces symbols across Ruby and JavaScript/TypeScript codebases. No IDE, no LSP, no AI agent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt; bash 4+ (or zsh 5+) and ripgrep. That's it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Install
curl -fsSL https://raw.githubusercontent.com/mirzalazuardi/codetracer/main/codetracer.sh \
  -o /usr/local/bin/codetracer &amp;amp;&amp;amp; chmod +x /usr/local/bin/codetracer
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-killer-feature-case-variant-expansion&quot;&gt;The Killer Feature: Case-Variant Expansion&lt;/h3&gt;
&lt;p&gt;Type a symbol in &lt;strong&gt;any&lt;/strong&gt; naming convention. codetracer finds all forms automatically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Input: processPayment

Searches simultaneously:
  process_payment      (snake_case)
  PROCESS_PAYMENT      (SCREAMING_SNAKE)
  processPayment       (camelCase)
  ProcessPayment       (PascalCase)
  process-payment      (kebab-case)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All these commands produce &lt;strong&gt;identical results&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codetracer process_payment
codetracer processPayment
codetracer ProcessPayment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No more &quot;I searched for &lt;code&gt;process_payment&lt;/code&gt; but the JS file uses &lt;code&gt;processPayment&lt;/code&gt;&quot; moments.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;five-modes-for-different-questions&quot;&gt;Five Modes for Different Questions&lt;/h3&gt;
&lt;h4 id=&quot;1-where-is-it-defined-&quot;&gt;1. Where is it defined?&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codetracer PaymentService . --mode def
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finds &lt;code&gt;def&lt;/code&gt;, &lt;code&gt;class&lt;/code&gt;, &lt;code&gt;module&lt;/code&gt;, &lt;code&gt;function&lt;/code&gt;, &lt;code&gt;const =&lt;/code&gt;, &lt;code&gt;async function&lt;/code&gt;, etc.&lt;/p&gt;
&lt;h4 id=&quot;2-who-calls-it-&quot;&gt;2. Who calls it?&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codetracer send_email . --mode call --scope
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every call site with full scope chain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;app/services/order_service.rb:42:    send_email(user, receipt)
  scope -&amp;gt; mod Billing &amp;gt; cls OrderService &amp;gt; def complete_order:38
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-how-does-data-flow-&quot;&gt;3. How does data flow?&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codetracer current_user . --mode flow
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tracks: assignments, passed as argument, returned/yielded, mutations.&lt;/p&gt;
&lt;h4 id=&quot;4-which-files-touch-it-&quot;&gt;4. Which files touch it?&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codetracer stripe_key . --mode file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;File map with hit counts per file.&lt;/p&gt;
&lt;h4 id=&quot;5-everything-at-once&quot;&gt;5. Everything at once&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codetracer order_total . --mode full --scope
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;new-rails-route-tracing&quot;&gt;NEW: Rails Route Tracing&lt;/h3&gt;
&lt;p&gt;Just shipped this week. Trace a route through the full request lifecycle:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codetracer --action &quot;OrdersController#refund&quot; ./app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;━━━ ROUTE TRACE: OrdersController#refund ━━━

OrdersController (app/controllers/orders_controller.rb)
├── before_action :authenticate_user!          :23  [ApplicationController]
├── before_action :set_order                   :8
├── def refund                                 :67
│   ├── params: :reason                        :68  [query]
│   ├── if @order.refundable?                  :69
│   │   ├── call: RefundService.call           :70
│   │   └── enqueue: RefundNotificationJob     :75  [async]
│   └── else                                   :77
│       └── render json: errors                :78
└── after_action :log_refund_attempt           :10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Callbacks, conditionals, params access, service calls, Sidekiq jobs — all in one tree. Perfect for understanding unfamiliar Rails controllers or creating sequence diagrams.&lt;/p&gt;
&lt;h4 id=&quot;trace-directly-from-your-curl-files&quot;&gt;Trace directly from your curl files&lt;/h4&gt;
&lt;p&gt;Got a directory of curl scripts for testing your API? Point codetracer at them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Your existing curl file
cat api_tests/refund.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST &quot;http://localhost:3000/orders/123/refund?notify=true&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d '{&quot;reason&quot;: &quot;damaged&quot;, &quot;amount&quot;: 50.00}'
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Trace it
codetracer --route api_tests/refund.sh ./app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;codetracer automatically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extracts the HTTP verb (&lt;code&gt;POST&lt;/code&gt;) and path (&lt;code&gt;/orders/:id/refund&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Converts numeric IDs to &lt;code&gt;:id&lt;/code&gt; (123 → :id)&lt;/li&gt;
&lt;li&gt;Extracts query params (&lt;code&gt;notify&lt;/code&gt;) and JSON body keys (&lt;code&gt;reason&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Shows expected params in the trace output&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Expected params from curl:
  query: notify
  body: reason amount

├── def refund
    ├── params: :reason    [query]  ← matches!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No more wondering &quot;does this endpoint actually use the params I'm sending?&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-not-just-use-ai-&quot;&gt;Why Not Just Use AI?&lt;/h3&gt;
&lt;p&gt;I use AI daily. But every question you ask about code structure — &lt;em&gt;&quot;where is this defined?&quot;&lt;/em&gt;, &lt;em&gt;&quot;who calls this?&quot;&lt;/em&gt; — costs tokens, takes seconds, and might be wrong.&lt;/p&gt;
&lt;p&gt;codetracer gives those answers &lt;strong&gt;instantly, deterministically, for free.&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;save-tokens-when-you-do-use-ai&quot;&gt;Save Tokens When You Do Use AI&lt;/h4&gt;
&lt;p&gt;Instead of pasting 500-line files, extract only what matters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Get 60 focused lines instead of 2000+ raw lines
codetracer process_payment . --mode flow --scope 2&amp;gt;&amp;amp;1 | head -60
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's a &lt;strong&gt;30x reduction in tokens&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-scope-chain&quot;&gt;The Scope Chain&lt;/h3&gt;
&lt;p&gt;This is my favorite feature. Every match shows its full nesting context:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codetracer process_payment . --mode call --scope
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;fixture_payment_service.rb:40:        process_payment(order, order.amount)
  scope -&amp;gt; mod Billing:5 &amp;gt; cls PaymentService:6 &amp;gt; def batch_process:38 &amp;gt; blk each:39
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You instantly know: this call happens inside an &lt;code&gt;each&lt;/code&gt; block, inside &lt;code&gt;batch_process&lt;/code&gt; method, inside &lt;code&gt;PaymentService&lt;/code&gt; class, inside &lt;code&gt;Billing&lt;/code&gt; module.&lt;/p&gt;
&lt;p&gt;No clicking through files. No mental stack tracing.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;works-on-macos-and-linux&quot;&gt;Works on macOS and Linux&lt;/h3&gt;
&lt;p&gt;Tested on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;macOS with bash 4+ or zsh 5+&lt;/li&gt;
&lt;li&gt;Ubuntu/Debian&lt;/li&gt;
&lt;li&gt;Arch Linux&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS
brew install ripgrep

# Ubuntu
sudo apt install ripgrep

# Optional: fzf for interactive mode, ctags for precise indexing
brew install fzf universal-ctags
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;interactive-mode-with-fzf&quot;&gt;Interactive Mode with fzf&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codetracer render . --inter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fuzzy-pick any match, preview context, press Enter to open in your editor.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;real-workflow-example&quot;&gt;Real Workflow Example&lt;/h3&gt;
&lt;p&gt;Debugging an unknown bug:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Where does this symbol exist?
codetracer process_payment . --mode file

# 2. Read the definition
codetracer process_payment . --mode def --ctags

# 3. Find callers and their context
codetracer process_payment . --mode call --scope

# 4. Trace data through the system
codetracer process_payment . --mode flow --scope --ctx 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four commands. Full picture. No IDE required.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it&quot;&gt;Try It&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# One-liner install
curl -fsSL https://raw.githubusercontent.com/mirzalazuardi/codetracer/main/codetracer.sh \
  -o /usr/local/bin/codetracer &amp;amp;&amp;amp; chmod +x /usr/local/bin/codetracer

# Try it
codetracer YourClassName ./your-project --mode def
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mirzalazuardi/codetracer&quot;&gt;github.com/mirzalazuardi/codetracer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If this looks useful, I'd appreciate a star. And if you have feature requests or find bugs, issues and PRs are welcome.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Built for engineers who prefer a terminal over a GUI and a shell script over a language server.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13522/i-built-a-bash-script-that-traces-code-faster-than-your-ide-and-saves-ai-tokens</guid>
<pubDate>Thu, 26 Mar 2026 17:19:50 +0000</pubDate>
</item>
<item>
<title>Favourite IDE for Ruby Work?</title>
<link>https://coderlegion.com/8269/favourite-ide-for-ruby-work</link>
<description>&lt;p&gt;Hey everyone!&lt;/p&gt;
&lt;p&gt;Quick question for the Ruby folks here — what’s your IDE or editor of choice for day-to-day Ruby/Rails work?&lt;/p&gt;
&lt;p&gt;Do you swear by RubyMine? Live inside VS Code with a handful of extensions? Or are you one of the brave souls still hacking away in Vim/Neovim? I’m curious not just what you use, but why you chose it. Speed? Tooling? Habit? Muscle memory?&lt;/p&gt;
&lt;p&gt;It’d be great to hear the range of setups people rely on. Always interesting to see how different developers shape their workflow.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/8269/favourite-ide-for-ruby-work</guid>
<pubDate>Mon, 08 Dec 2025 01:33:41 +0000</pubDate>
</item>
<item>
<title>My Editor Journey: From Heavy IDEs to Plain Old Vim</title>
<link>https://coderlegion.com/7685/my-editor-journey-from-heavy-ides-to-plain-old-vim</link>
<description>&lt;p&gt;I’ve been through a long parade of editors and IDEs over the years, and each phase almost feels like a snapshot of where I was as a developer at the time. Back at university the big names were NetBeans and Eclipse—heavy, clunky, but they got the job done and felt “proper” in an academic setting.&lt;/p&gt;
&lt;p&gt;When I moved into the Ruby world, the tool everyone raved about was RubyMine (a JetBrains product). And for good reason: it did everything for you—refactors, navigation, inspections. For a while, it felt like the most grown-up way to write Ruby.&lt;/p&gt;
&lt;p&gt;But I kept bumping into these YouTube videos of people practically gliding through code in Vim or Emacs. No mouse, no ceremony, just pure speed. They made it look like wizardry. Eventually curiosity got the better of me, and I decided to teach myself Vim. It was painful at first—when you start, you genuinely don’t know how to quit, which feels like a metaphor for the entire learning curve.&lt;/p&gt;
&lt;p&gt;But once the modal editing philosophy clicked, something shifted. I started shaping my own config instead of relying on someone else’s mega-distribution. Every keybind had a purpose, every plugin earned its place, and over the years it became a tool that fit my hands perfectly. Today, for Ruby, I wouldn’t use anything else. Vim just feels like an extension of my brain.&lt;/p&gt;
&lt;p&gt;That said, IDEs still have their place. If I’m writing Java, for example, a full JetBrains IDE is undeniably better for certain kinds of refactoring and analysis. Some languages just benefit more from all the tooling an IDE can provide.&lt;/p&gt;
&lt;p&gt;But for my day-to-day coding, Vim is home.&lt;br&gt;
Curious to hear what everyone else here is using—and what brought you to it.&lt;/p&gt;
</description>
<category>Backend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/7685/my-editor-journey-from-heavy-ides-to-plain-old-vim</guid>
<pubDate>Sun, 23 Nov 2025 23:27:42 +0000</pubDate>
</item>
<item>
<title>How We Solved “Unable to Get Certificate CRL” in Rails: A Debugging Story</title>
<link>https://coderlegion.com/7310/how-we-solved-unable-to-get-certificate-crl-in-rails-a-debugging-story</link>
<description>&lt;p&gt;If you’ve been around Rails long enough, you’ve probably battled your fair share of SSL demons. But this one? This one had &lt;em&gt;personality&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;It was a &lt;code&gt;Seahorse::Client::NetworkingError&lt;/code&gt; that failed &lt;strong&gt;only on macOS&lt;/strong&gt; — yet worked perfectly inside Docker. The kind of “it works on my container” bug that makes you question every life choice leading up to this point.&lt;/p&gt;
&lt;p&gt;So here’s the story of how we chased down a ghostly SSL error, only to discover that the real culprit was… OpenSSL itself.&lt;/p&gt;
&lt;h2&gt;⚠️ The Error&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Seahorse::Client::NetworkingError
SSL_connect returned=1 errno=0 peeraddr=[...]
state=error: certificate verify failed (unable to get certificate CRL)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This popped up whenever our Rails app tried connecting to AWS S3 using the &lt;code&gt;aws-sdk-ruby&lt;/code&gt; gem.&lt;/p&gt;
&lt;h2&gt; The Contradiction That Made No Sense&lt;/h2&gt;
&lt;p&gt;From the get-go, the bug refused to play by any rules of logic.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;❌ &lt;strong&gt;It failed locally&lt;/strong&gt; but&lt;/li&gt;
  &lt;li&gt;✅ &lt;strong&gt;Worked flawlessly in Docker&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That told us one thing: the problem wasn’t our code. It was the environment. macOS and Docker’s Linux setup were behaving differently.&lt;/p&gt;
&lt;h2&gt; The “Wait, What?” Moment&lt;/h2&gt;
&lt;p&gt;We decided to test the SSL connection manually, expecting it to fail the same way:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl s_client -connect 52.219.66.109:443 -servername s3.ap-south-1.amazonaws.com &amp;lt; /dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Verify return code: 0 (ok)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, macOS’s OpenSSL tool had no problem verifying the certificate.  
But Rails (and the AWS SDK) still choked on it.&lt;/p&gt;
&lt;p&gt;At this point, confusion levels were high. Coffee consumption was higher.&lt;/p&gt;
&lt;h2&gt; The Real Test — Inside the Rails Console&lt;/h2&gt;
&lt;p&gt;To rule out any system-level differences, we went straight into &lt;code&gt;rails c&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require 'aws-sdk-s3'
s3 = Aws::S3::Client.new(region: 'ap-south-1')
s3.list_buckets
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certificate verify failed (unable to get certificate CRL)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Boom. So the system OpenSSL was fine, but Ruby’s built-in OpenSSL (via &lt;code&gt;Net::HTTP&lt;/code&gt;) was throwing a tantrum.&lt;/p&gt;
&lt;h2&gt; The Root Cause — A Policy Change in OpenSSL&lt;/h2&gt;
&lt;p&gt;That cryptic message — &lt;em&gt;“unable to get certificate CRL”&lt;/em&gt; — turned out to be the key.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;CRL (Certificate Revocation List)&lt;/strong&gt; is basically a “blacklist” for certificates that have been revoked. The problem? The AWS certificate didn’t actually &lt;em&gt;have&lt;/em&gt; a CRL defined.&lt;/p&gt;
&lt;p&gt;We checked with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl s_client ... | openssl x509 -noout -text | grep &quot;CRL Distribution Points&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X509v3 CRL Distribution Points:
# (empty)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Comparing environments made things click:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Environment&lt;/th&gt;
      &lt;th&gt;OpenSSL Version&lt;/th&gt;
      &lt;th&gt;Result&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;macOS (failing)&lt;/td&gt;
      &lt;td&gt;3.4.0 (new)&lt;/td&gt;
      &lt;td&gt;❌ Fails&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Docker (working)&lt;/td&gt;
      &lt;td&gt;3.0.11 (stable)&lt;/td&gt;
      &lt;td&gt;✅ Works&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Turns out, &lt;strong&gt;OpenSSL 3.4.0 introduced stricter validation rules&lt;/strong&gt;.  
It now treats missing CRL entries as fatal errors — whereas the 3.0.x series simply shrugged and moved on.&lt;/p&gt;
&lt;p&gt;So no, your Mac isn’t broken — it’s just being &lt;em&gt;too helpful&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt; The Fixes — Two Paths to Sanity&lt;/h2&gt;
&lt;p&gt;After a lot of trial (and some error), we landed on two solid fixes.&lt;/p&gt;
&lt;h3&gt; Fix 1: The Local-Only (Insecure but Practical) Workaround&lt;/h3&gt;
&lt;p&gt;Since this issue only showed up in development, one quick way to move on is to tell AWS SDK to skip SSL verification locally.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create:&lt;/strong&gt; &lt;code&gt;config/initializers/aws_ssl_workaround.rb&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# WARNING: LOCAL DEVELOPMENT ONLY
# OpenSSL 3.4.0 enforces stricter CRL policies, so skip verification here.
if Rails.env.development?
  Aws.config.update(ssl_verify_peer: false)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;✅ Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Works instantly&lt;/li&gt;
  &lt;li&gt;Doesn’t affect production&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Insecure (but fine for local development)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt; Fix 2: Match Environments (I tried this one!)&lt;/h3&gt;
&lt;p&gt;The better long-term solution is to make your local OpenSSL match Docker’s version.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Install an older OpenSSL (3.0.x):&lt;/strong&gt;
    &lt;pre&gt;&lt;code&gt;brew install openssl@3.0&lt;/code&gt;&lt;/pre&gt;
  &lt;/li&gt;

  &lt;li&gt;&lt;strong&gt;Reinstall Ruby:&lt;/strong&gt;
    &lt;pre&gt;&lt;code&gt;rbenv uninstall 3.2.2
RUBY_CONFIGURE_OPTS=&quot;--with-openssl-dir=$(brew --prefix openssl@3.0)&quot; rbenv install 3.2.2&lt;/code&gt;&lt;/pre&gt;
  &lt;/li&gt;

  &lt;li&gt;&lt;strong&gt;Reinstall gems:&lt;/strong&gt;
    &lt;pre&gt;&lt;code&gt;gem install bundler
bundle install&lt;/code&gt;&lt;/pre&gt;
  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now your local Ruby is linked against OpenSSL 3.0 — just like Docker and peace is restored.&lt;/p&gt;
&lt;h2&gt; Confirmation &amp;amp; References&lt;/h2&gt;
&lt;p&gt;Turns out, we weren’t the only ones haunted by this ghost:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/openssl/openssl/issues/28758&quot; target=&quot;_blank&quot;&gt;openssl/openssl#28758&lt;/a&gt; — CRL verification failures in OpenSSL 3.6.0&lt;/li&gt;
  &lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ruby/openssl/issues/949&quot; target=&quot;_blank&quot;&gt;ruby/openssl#949&lt;/a&gt; — Net::HTTP failing due to stricter CRL rules&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt; Takeaway&lt;/h2&gt;
&lt;p&gt;SSL errors can feel like black magic, but more often than not, they’re &lt;em&gt;version mismatches in disguise&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So next time something works in Docker but not on your laptop, remember:  
sometimes it’s not your code, it’s just your OpenSSL being extra.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thanks for reading! If you found this helpful, share it with a fellow developer who’s currently staring at an SSL error and losing their mind.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Backend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/7310/how-we-solved-unable-to-get-certificate-crl-in-rails-a-debugging-story</guid>
<pubDate>Thu, 13 Nov 2025 11:53:55 +0000</pubDate>
</item>
<item>
<title>What’s your current Ruby setup?</title>
<link>https://coderlegion.com/7274/whats-your-current-ruby-setup</link>
<description>&lt;p&gt;Hey everyone!&lt;/p&gt;
&lt;p&gt;Thought we’d kick things off with some introductions.&lt;/p&gt;
&lt;p&gt;What’s your experience been like working with Ruby or Rails so far? Are you deep in a legacy app, or running shiny Rails 8 with all the trimmings?&lt;/p&gt;
&lt;p&gt;I’ll start: I’ve been working with Ruby (on Rails) for over 10 years now — from scrappy greenfield startup apps to large-scale corporate systems. I’m curious to hear what versions (and horror stories!) you’ve got to share.&lt;/p&gt;
&lt;p&gt;And if you’ve built something cool, show it off — we’d love to see it!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/7274/whats-your-current-ruby-setup</guid>
<pubDate>Thu, 13 Nov 2025 01:44:02 +0000</pubDate>
</item>
<item>
<title>The &amp;&amp; Fallacy: How --command Fixed VS Code Extensions in a Declarative Nix Shell</title>
<link>https://coderlegion.com/6685/the-fallacy-how-command-fixed-vs-code-extensions-in-a-declarative-nix-shell</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/beyond-rbenv-and-pyenv-achieving-reproducible-dev-setups-with-nix-04b610175a59&quot;&gt;In the last article&lt;/a&gt;, we discussed the evolution of my development setup, focusing on how it became reproducible over time. It was originally written to document the process for my own amusement. Surprisingly, the stats tell a different story — apparently it resonated with enough people sharing the same pain. Over time, revisions were made alongside new discoveries, and I figured the journey was worth a retelling. Read on to find out about an unexpected milestone related to this very blog.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*GzOY6iSa5mArW2CikDQTDQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Cute illustration on the topic, by Microsoft Copilot&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-editor-s-blind-spot-nix-vs-vs-code-extensions&quot;&gt;The Editor’s Blind Spot: Nix vs. VS Code Extensions&lt;/h4&gt;
&lt;p&gt;Up until the last article, we managed to get a rather consistent setup with home-manager to manage development tools, and subsequently &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/beyond-rbenv-and-pyenv-achieving-reproducible-dev-setups-with-nix-04b610175a59&quot;&gt;retired tools like&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/beyond-rbenv-and-pyenv-achieving-reproducible-dev-setups-with-nix-04b610175a59&quot;&gt;pyenv and&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/beyond-rbenv-and-pyenv-achieving-reproducible-dev-setups-with-nix-04b610175a59&quot;&gt;rbenv with the use of&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/beyond-rbenv-and-pyenv-achieving-reproducible-dev-setups-with-nix-04b610175a59&quot;&gt;nix-develop&lt;/a&gt;. Little did I know, it wasn’t the end of the story. This time, we are tackling a new issue — the lack of nix-develop support of the Visual Studio Code extensions for development work in Ruby.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/from-dotfile-hacks-to-open-source-my-development-environment-evolution-1hce&quot;&gt;From Dotfile Hacks to Open Source: My Development Environment Evolution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/beyond-rbenv-and-pyenv-achieving-reproducible-dev-setups-with-nix-5gaa&quot;&gt;Beyond rbenv and pyenv: Achieving Reproducible Dev Setups with Nix&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Forcing the ruby-lsp extension to source an external Gemfile as detailed was a hackish solution. Although it worked for a bit, most of the time the extension failed to initialize the language server. Observing how inconsistent the behavior is, something must be wrong. On the other hand, setup seems impeccable, tests were passing, and projects were building fine too.&lt;/p&gt;
&lt;p&gt;Unfortunately, not much luck with the team’s desired formatter.&lt;/p&gt;
&lt;p&gt;It wouldn’t work in the editor.&lt;/p&gt;
&lt;p&gt;A type checker extension called sorbet is also common, and that too wouldn’t work at all. Considering the team doesn’t type annotate the code, I quickly dismissed and deactivated the extension. Combining this with the previous problem where ruby-lsp failed to work, the itch grew as time passes on.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*fQ284dwYbjqK6d0B&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Abu Saeid on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Worse still, a formatter that doesn’t work at all times yields errors in the automated checks on every push.&lt;/p&gt;
&lt;p&gt;Something was definitely wrong, was it because of VSCode failing to pick up the environment in a nix-develop session? My initial attempts to fix the flake file to include a command to &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Shopify/ruby-lsp/issues/1851&quot;&gt;activate the language server properly as shared&lt;/a&gt;. Still, none of them seemed to work reliably.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Shopify/ruby-lsp/issues/1851&quot;&gt;How To: Use vscode-ruby-lsp with flake.nix based dev enviroment · Issue #1851 · Shopify/ruby-lsp&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After hitting walls enough times, I decided to post my setup to the discussion board to seek for help.&lt;/p&gt;
&lt;h4 id=&quot;the-log-line-that-clicked-differently&quot;&gt;The Log Line That Clicked Differently&lt;/h4&gt;
&lt;p&gt;A problem can only be solved if we can clearly define it. We know the setup is failing, but what caused it? Interestingly enough, achieving the problem definition is a series of unlikely coincidences. It almost felt like solving a jigsaw puzzle, where each seemingly unrelated piece ultimately leads to the solution. So what are the missing pieces stalling our progress in understanding what went wrong?&lt;/p&gt;
&lt;p&gt;Let’s start with the discovery of previously unused --command argument for nix-develop.&lt;/p&gt;
&lt;p&gt;Initially, the gemini-generated flake file for the nix-develop sessions contained some commands to start my favorite fish shell. The snippet on starting a new shell was taken out from the previous discussion as it was somewhat unrelated. It was rather complicated and bound to fail because I was expecting it to work differently in different scenarios. Naturally, I wanted to simplify it somewhat, and eventually I learned about the --command argument.&lt;/p&gt;
&lt;p&gt;Instead of relying on a clever (or dumb, depending on how you perceive it) detection logic, I could start a new fish shell session with&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nix develop /path/to/flake/dir --command fish
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Besides a shell, it could also run any command in the environment defined by the flake file. Discovering this was a crucial step toward fixing the launch of the ruby-lsp language server in the editor.&lt;/p&gt;
&lt;p&gt;Remember I posted &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Shopify/ruby-lsp/discussions/3683&quot;&gt;an SOS message on the discussion board&lt;/a&gt;? Luckily, it caught the attention of &lt;a rel=&quot;nofollow&quot; href=&quot;https://vinistock.com/&quot;&gt;Vinicius Stock&lt;/a&gt;, the project owner. Helpfully enough, he pointed out that an external Gemfile is not necessary, and asked me to check if the language server is launched properly. With that, I checked the log and found this line&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2025-07-27 16:56:30.669 [info] (ship_it) Running command: `nix develop /Users/jeffrey04/Projects/home-manager/devenvs/ruby-3.3 &amp;amp;&amp;amp; ruby -EUTF-8:UTF-8 '/Users/jeffrey04/.vscode-oss/extensions/shopify.ruby-lsp-0.9.31/activation.rb'` in /Users/jeffrey04/Projects/ship_it using shell: /Users/jeffrey04/.nix-profile/bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Despite seeing the log multiple times before, now that I know about the --command argument, the same line clicked differently. Does the ruby command after the &amp;amp;&amp;amp; operator run in the nix-develop session? Validating in this case is rather easy, just run&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nix develop /path/to/flake/dir &amp;amp;&amp;amp; ruby --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oops, why am I being dropped to a new shell session? Let me exit the session, and sure enough, the ruby command refers to the version offered by the operating system.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ nix develop ~/Projects/home-manager/devenvs/ruby-3.3 &amp;amp;&amp;amp; ruby --version
warning: updating lock file &quot;/Users/jeffrey04/Projects/home-manager/devenvs/ruby-3.3/flake.lock&quot;:
• Added input 'nixpkgs':
    'github:NixOS/nixpkgs/544961dfcce86422ba200ed9a0b00dd4b1486ec5?narHash=sha256-EVAqOteLBFmd7pKkb0%2BFIUyzTF61VKi7YmvP1tw4nEw%3D' (2025-10-15)
(nix:ruby-3.3-env) wukong:ib-vpn jeffrey04$
exit
ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.x86_64-darwin24]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*jSpPmgyc2V98iFRo&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Matias Malka on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Clearly, the assumption that the command following the &amp;amp;&amp;amp; operator is flawed from the very beginning. With this revelation, we can finally define what the problem is — the language server is not properly launched. In this case, all we need to do is to replace the boolean AND operator into --command.&lt;/p&gt;
&lt;p&gt;Easy to say, but implementation is a challenge for someone who has no experience with vscode extension development like myself.&lt;/p&gt;
&lt;p&gt;And this is exactly when &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/vibe-coding-storytelling-and-llms-a-collaborative-approach-2l4h&quot;&gt;vibe-coding is useful&lt;/a&gt;, with a clear problem definition and goal, the solution was just a prompt or two away with the help of gemini-cli. As expected, the patch worked flawlessly.&lt;/p&gt;
&lt;p&gt;Firstly, add a new enum entry to ManagerIdentifier in vscode/src/ruby.ts&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export enum ManagerIdentifier {
  Asdf = &quot;asdf&quot;,
  Auto = &quot;auto&quot;,
  Chruby = &quot;chruby&quot;,
  Rbenv = &quot;rbenv&quot;,
  Rvm = &quot;rvm&quot;,
  Shadowenv = &quot;shadowenv&quot;,
  Mise = &quot;mise&quot;,
  RubyInstaller = &quot;rubyInstaller&quot;,
  NixDevelop = &quot;nix-develop&quot;, # add this line
  None = &quot;none&quot;,
  Custom = &quot;custom&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then a new case in the runManagerActivation method&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case ManagerIdentifier.NixDevelop:
  await this.runActivation(
    new NixDevelop(this.workspaceFolder, this.outputChannel, this.context, this.manuallySelectRuby.bind(this)),
  );
  break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Followed by a new module at vscode/src/ruby/nixDevelop.ts that contains the activation logic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as vscode from &quot;vscode&quot;;

import { VersionManager, ActivationResult } from &quot;./versionManager&quot;;

export class NixDevelop extends VersionManager {
  async activate(): Promise&amp;lt;ActivationResult&amp;gt; {
    const customCommand = this.customCommand();
    const command = `nix develop ${customCommand} --command ruby`;
    const parsedResult = await this.runEnvActivationScript(command);

    return {
      env: { ...process.env, ...parsedResult.env },
      yjit: parsedResult.yjit,
      version: parsedResult.version,
      gemPath: parsedResult.gemPath,
    };
  }

  private customCommand() {
    const configuration = vscode.workspace.getConfiguration(&quot;rubyLsp&quot;);
    const customCommand: string | undefined =
      configuration.get(&quot;customRubyCommand&quot;);

    return customCommand || &quot;&quot;;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly, enable the option forrubyVersionManager in vscode/package.json&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &quot;rubyLsp.rubyVersionManager&quot;: {
          &quot;type&quot;: &quot;object&quot;,
          &quot;properties&quot;: {
            &quot;identifier&quot;: {
              &quot;description&quot;: &quot;The Ruby version manager to use&quot;,
              &quot;type&quot;: &quot;string&quot;,
              &quot;enum&quot;: [
                &quot;asdf&quot;,
                &quot;auto&quot;,
                &quot;chruby&quot;,
                &quot;none&quot;,
                &quot;rbenv&quot;,
                &quot;rvm&quot;,
                &quot;shadowenv&quot;,
                &quot;mise&quot;,
                &quot;nix-develop&quot;, // add this line
                &quot;custom&quot;
              ],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Don’t forget about the actual config (omit the rubyLsp.customRubyCommand if flake.nix is in the workspace)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;rubyLsp.rubyVersionManager&quot;: {
    &quot;identifier&quot;: &quot;nix-develop&quot;
},
&quot;rubyLsp.customRubyCommand&quot;: &quot;/Users/jeffrey04/Projects/home-manager/devenvs/ruby-3.3&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The patch is definitely not perfect, and it is reusing the optional customRubyCommand configuration item to define where the directory containing the flake file is. Yet, considering this likely affects only a small group of users, I don’t see a need to introduce a new configuration field for the purpose. Contributions are definitely welcome at the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Shopify/ruby-lsp/pull/3691&quot;&gt;submitted pull request&lt;/a&gt;, as it has been stalling for months.&lt;/p&gt;
&lt;p&gt;For the sorbet part, I managed to get it working one day out of boredom. Most of the Ruby code I have access for now is not type-annotated, so the effort is purely for fun. A quick and dirty way to do so, is building the language server application directly, and configure vscode as follows&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;sorbet.enabled&quot;: true,
&quot;sorbet.selectedLspConfigId&quot;: &quot;without-bundler&quot;,
&quot;sorbet.userLspConfigs&quot;: [
    {
        &quot;id&quot;: &quot;without-bundler&quot;,
        &quot;name&quot;: &quot;sorbet&quot;,
        &quot;description&quot;: &quot;Launch locally compiled sorbet&quot;,
        &quot;command&quot;: [&quot;/path/to/sorbet&quot;, &quot;typecheck&quot;, &quot;--lsp&quot;, &quot;--dir=/path/to/project&quot;]
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For some reason I needed to --disable-watchman flag in the command too, but as there’s no real need for now I am not going to dig further.&lt;/p&gt;
&lt;p&gt;There you have it, with simplified flake files (removing the detection logic to start fish shell) and invoking fish shell explicitly with --command, and patches and hacks to enable the ruby toolings, we finally have a functional setup for development work in Ruby. Seeing the pull request being stalled for months (and their bot closed my PR recently due to inactivity) does make me upset for a little bit. Moving forward I may need to consider maintaining a fork for it as it does affect my work.&lt;/p&gt;
&lt;h4 id=&quot;stalled-pr-sudden-payoff-the-two-timelines&quot;&gt;Stalled PR, Sudden Payoff: The Two Timelines&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*cP36WVy-iKsY5dMX&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by engin akyurt on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Patches to the ruby-lsp were submitted in the late July, and I was hoping to start drafting this article when it received a review. Unfortunately, that didn’t happen. I was caught up with my new job and several major family issues while waiting on the PR. Things finally settled down for a little bit, and I should be back to a relatively more predictable routine. Hopefully this translates to a more consistent publishing schedule for &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/&quot;&gt;KitFu Coda&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As for the unexpected milestone teased in the beginning, it is about a talk I am about to deliver during the upcoming &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pycon.my/&quot;&gt;PyCon Malaysia&lt;/a&gt;. My mentor encouraged me to submit for the CFP and &lt;a rel=&quot;nofollow&quot; href=&quot;https://cfp.pycon.my/pyconmy-2025/talk/review/LNN8WWYV93JYTKZMEGKE79KBWJM73J8M&quot;&gt;it somehow went through&lt;/a&gt;. If you are interested in applying AsyncIO in projects, do feel free to drop by and say Hi. For reference, it is based on the articles I published earlier, as linked below.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/understanding-awaitables-coroutines-tasks-and-futures-in-python-gk7&quot;&gt;Understanding Awaitables: Coroutines, Tasks, and Futures in Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/asyncio-task-management-a-hands-on-scheduler-project-2e54&quot;&gt;AsyncIO Task Management: A Hands-On Scheduler Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/concurrency-vs-parallelism-achieving-scalability-with-processpoolexecutor-1n7n&quot;&gt;Concurrency vs. Parallelism: Achieving Scalability with ProcessPoolExecutor&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also discussed briefly with my supervisor in one of our 1 on 1 sessions, mainly to check on any necessary approvals or conflict of interest. He was incredibly supportive about it and offered some tips on presenting ideas to a wider audience. As I am new to Ruby, he also briefly explained how the language approaches asynchronous programming. On a lighter note, he’s even been checking on my preparation progress (zero so far), especially in recent sessions!&lt;/p&gt;
&lt;p&gt;With this article checked off in my TODO, I can finally allocate some time on the preparation work. I sometimes feel like I’m running on an energy tank whose maximum capacity depletes as we age. These days, even finding time to game is a challenge. I feel bad for my character in &lt;em&gt;Xenoblade X&lt;/em&gt; and I wonder if I’ll ever have time for the new &lt;em&gt;Pokémon Legends Z-A&lt;/em&gt; game I’m about to purchase.&lt;/p&gt;
&lt;p&gt;Finally, I would like to thank everyone for the support, especially to everyone who reads my work here. Though I have not been publishing much, since starting this technical writing project opens up fascinating new opportunities. Besides scoring a new job, I also started exploring scripted video making, and now a talk. I am thankful and hope to inspire more people through this trying time.&lt;/p&gt;
&lt;p&gt;Thanks again for reading, I shall write again soon.&lt;/p&gt;
&lt;p&gt;Throughout this journey, Gemini served as a valuable editorial companion, helping refine my prose. However, the voice and code remain entirely my own. I welcome your feedback and recommendations in the comments below, and invite you to subscribe to my &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/&quot;&gt;Medium&lt;/a&gt; for more content on my development adventures!&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/6685/the-fallacy-how-command-fixed-vs-code-extensions-in-a-declarative-nix-shell</guid>
<pubDate>Tue, 21 Oct 2025 02:40:26 +0000</pubDate>
</item>
<item>
<title>Welcome to the Ruby &amp; Rails Community</title>
<link>https://coderlegion.com/5869/welcome-to-the-ruby-rails-community</link>
<description>&lt;p&gt;Hey everyone.  Welcome to our brand-new Ruby &amp;amp; Rails group! This is a space for anyone working with Ruby — whether you’re wrangling ActiveRecord callbacks, crafting APIs, or pushing the edges of what Rails can do in 2025.&lt;/p&gt;
&lt;p&gt;The goal here is simple: share articles, swap stories, and learn from each other. If you’ve got a question, an aha-moment, or just something cool you built, drop it in — chances are someone else will benefit (or at least laugh in sympathy).&lt;/p&gt;
&lt;p&gt;We want a community where all Rubyists feel at home — from those hacking together their first Sinatra app to folks scaling Rails to handle thousands of requests per second. The only rule: be helpful, be respectful, and bring your curiosity.&lt;/p&gt;
&lt;p&gt;So grab your favourite editor (yes, even if it’s VS Code — we’ll only judge you quietly ) and let’s start building a Ruby community worth being part of. Welcome aboard!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/5869/welcome-to-the-ruby-rails-community</guid>
<pubDate>Wed, 01 Oct 2025 23:12:42 +0000</pubDate>
</item>
<item>
<title>Feature Flags | Controlled Chaos in Production</title>
<link>https://coderlegion.com/5261/feature-flags-controlled-chaos-in-production</link>
<description>&lt;p&gt;Feature flags are a way of life when developing software. They let you deploy code to production without immediately exposing it to users — like flipping a switch without blowing the fuse.&lt;/p&gt;
&lt;p&gt;In practice, they're used to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ship features incrementally&lt;/li&gt;
&lt;li&gt;Test changes in production with internal users&lt;/li&gt;
&lt;li&gt;Run A/B tests or multivariate experiments&lt;/li&gt;
&lt;li&gt;Deliver bespoke functionality to specific customers (not my favourite approach, but hey, sometimes we have to pick our battles)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Feature flags introduce flexibility — but also the responsibility to manage them well. Left unchecked, they can become a tangle of forgotten toggles, quietly rotting your codebase from within.&lt;/p&gt;
&lt;p&gt;Let’s take a look at one of the more robust feature flag services out there.&lt;/p&gt;
&lt;h3 id=&quot;launchdarkly&quot;&gt;LaunchDarkly&lt;/h3&gt;
&lt;p&gt;One of the more popular services (at least in my limited experience) is &lt;a rel=&quot;nofollow&quot; href=&quot;https://launchdarkly.com/&quot;&gt;LaunchDarkly&lt;/a&gt;. It allows you to create and manage feature flags, define targeting rules (e.g., which users get what), and view flag usage analytics — including which flags are no longer being used.&lt;/p&gt;
&lt;p&gt;That last bit is more useful than it sounds. Stale flags can linger for months or even years, adding dead weight to your app and making the logic harder to follow. Being able to identify and prune them is a big plus.&lt;/p&gt;
&lt;p&gt;Here’s a basic integration pattern for using LaunchDarkly in a Rails app:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;module FeatureFlag
  class &amp;lt;&amp;lt; self
    def for(entity)
      @entity = entity
      self
    end

    def feature_a? = check_for(&quot;feature-a&quot;, default: true) # this default will make sense shortly
    def feature_b? = check_for(&quot;feature-b&quot;)
    def feature_c? = true # hard-coded flag (sometimes needed)

    private

    attr_reader :entity

    def check_for(flag_name, default: false)
      return false unless entity.present?

      ld_client.variation(flag_name, entity_context(entity), default)
    end

    def ld_client
      @ld_client ||= Rails.configuration.ld_client
    end

    def entity_context(entity)
      LaunchDarkly::LDContext.create(
        {
          key: entity.identifier, # a unique identifier for the entity
          kind: entity.type       # usually &quot;user&quot;, but could be &quot;account&quot;, etc.
        }
      )
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern gives you a central place to check feature availability in your app. You might wire this into your controllers, services, or views like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;if FeatureFlag.for(current_user).feature_a?
  render :new_feature_version
else
  render :legacy_version
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s readable, testable, and keeps your flag logic out of the weeds.&lt;/p&gt;
&lt;h3 id=&quot;handling-flags-in-test-and-development-environments&quot;&gt;Handling Flags in Test and Development Environments&lt;/h3&gt;
&lt;p&gt;Since LaunchDarkly is an external service that communicates via HTTP, you definitely don’t want your test suite making live requests every time it runs. That’s just asking for flakiness and slow feedback loops.&lt;/p&gt;
&lt;p&gt;Instead, we can use a mock client in the test environment to simulate feature flag behavior without hitting the actual API. Here’s a simple example I popped into our &lt;code&gt;lib&lt;/code&gt; folder:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;module LaunchDarkly
  class MockLdClient
    def initialize(*) = true

    def initialized? = true

    def variation(_key, _user, default) = default
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This mock client always returns the &lt;code&gt;default&lt;/code&gt; value passed into the &lt;code&gt;variation&lt;/code&gt; method — making it trivial to control feature behaviour explicitly in your tests.&lt;/p&gt;
&lt;p&gt;But wait — why not just put LaunchDarkly into &lt;code&gt;&quot;offline&quot;&lt;/code&gt; mode in the test environment, like we do below for development?&lt;/p&gt;
&lt;p&gt;Good question. While &lt;code&gt;offline: true&lt;/code&gt; does prevent real network calls, it still requires the full LaunchDarkly SDK to be initialized — which adds unnecessary overhead and dependencies to your test suite. By using a lightweight mock client instead, you get a few key benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Faster tests&lt;/strong&gt; — no SDK initialization, no config loading, just a plain Ruby object&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero external dependencies&lt;/strong&gt; — the mock removes the need to load the LaunchDarkly gem at all in tests&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simpler control&lt;/strong&gt; — it's easier to stub or extend the mock for edge cases if needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolation&lt;/strong&gt; — your tests won't break due to SDK upgrades or changes in default behaviours&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In short, the mock client keeps your test environment lean and focused. It’s a small abstraction that pays for itself in speed and stability.&lt;/p&gt;
&lt;p&gt;With that in place, in your LaunchDarkly initializer (&lt;code&gt;config/initializers/launch_darkly.rb&lt;/code&gt;), you can wire it in like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;require &quot;#{Rails.root}/lib/launch_darkly/mock_ld_client.rb&quot;

case Rails.env
when &quot;test&quot;
  Rails.configuration.ld_client = LaunchDarkly::MockLdClient.new(&quot;FAKE API KEY&quot;)
when &quot;development&quot;
  config = LaunchDarkly::Config.new(offline: true)
  Rails.configuration.ld_client = LaunchDarkly::LDClient.new(ENV['LD_KEY'], config)
else
  Rails.configuration.ld_client = LaunchDarkly::LDClient.new(ENV['LD_KEY'])
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the &lt;strong&gt;test&lt;/strong&gt; environment, we avoid any HTTP calls by using the mock client.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;development&lt;/strong&gt;, we set LaunchDarkly into &lt;code&gt;offline&lt;/code&gt; mode. This disables all outbound network traffic and ensures your app doesn't hang or fail due to a misconfigured API key. You can still control the outcome of flags by setting the default values in your FeatureFlag methods — which is usually more than enough for local dev.&lt;/p&gt;
&lt;p&gt;This setup gives you full control and isolation in non-prod environments, with zero reliance on external calls. Win-win.&lt;/p&gt;
&lt;h3 id=&quot;some-other-options&quot;&gt;Some Other Options&lt;/h3&gt;
&lt;p&gt;While this post focuses on LaunchDarkly, there are plenty of other options — some open-source, some more opinionated, and some simpler to get going with.&lt;/p&gt;
&lt;p&gt;Here are a few worth considering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/flippercloud/flipper&quot;&gt;Flipper&lt;/a&gt; – Powerful, flexible, and actively maintained. Can use a variety of backends (Redis, ActiveRecord, etc.) and integrates with a slick UI via Flipper Cloud.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/FetLife/rollout&quot;&gt;Rollout&lt;/a&gt; – A battle-tested gem that’s been around for a while. Simpler API, great for percentage rollouts and user targeting.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/voormedia/flipflop&quot;&gt;Flipflop&lt;/a&gt; – Great for toggling features via a web UI. Good for admin-driven flags or feature previews.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which one to use depends on your needs — whether you want self-hosted vs. SaaS, need advanced targeting and analytics, or just want a simple way to toggle features in development.&lt;/p&gt;
&lt;h3 id=&quot;parting-thoughts&quot;&gt;Parting Thoughts&lt;/h3&gt;
&lt;p&gt;Feature flags give you control, flexibility, and faster feedback loops — but like anything powerful, they need to be managed responsibly. Treat them like temporary scaffolding, not permanent fixtures. Set reminders to clean them up. Document what each flag does and who owns it. And when possible, limit the number of active flags in your system at a given time.&lt;/p&gt;
&lt;p&gt;If you’ve had war stories with forgotten flags or misconfigured rollouts, I’d love to hear them. Otherwise, happy toggling.&lt;/p&gt;
</description>
<category>Ruby on Rails</category>
<guid isPermaLink="true">https://coderlegion.com/5261/feature-flags-controlled-chaos-in-production</guid>
<pubDate>Tue, 09 Sep 2025 19:51:24 +0000</pubDate>
</item>
<item>
<title>Better Sidekiq Classes</title>
<link>https://coderlegion.com/5088/better-sidekiq-classes</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sidekiq/sidekiq&quot;&gt;Sidekiq&lt;/a&gt; is pretty much the go-to solution for enqueuing jobs for background processing when working on a Ruby-based project. It's simple to implement, has a clear DSL, and is well-supported by common testing frameworks like RSpec. That being said, and I may be nitpicking, but the standard implementation examples do add a little extra overhead if you wish to be more flexible with your job business logic, and additional test setup is required.&lt;/p&gt;
&lt;h3 id=&quot;the-normal-way&quot;&gt;The &quot;normal&quot; way&lt;/h3&gt;
&lt;p&gt;Given we want a little background job that updates a user's name and any other business logic needed (to keep the example simple), we could create a standard Sidekiq job with that logic and call it from our code as such:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class UpdateUserJob
  include Sidekiq::Job

  def perform(user_id, name)
    user = User.find(user_id)
    user.update!(name: name)
    # .. additional business logic here
  end
end

UpdateUserJob.perform_async(1, &quot;bob&quot;) # perform the job as soon as possible
UpdateUserJob.perform_in(5.minutes, 1, &quot;bob&quot;) # delay the processing by 5 minutes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is simple enough. But what if we want to test the logic being performed inside this job? Firstly, we'd need to update our &lt;code&gt;spec_helper&lt;/code&gt; to let it know what to do with a job. If you're using RSpec, you might even need to add the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/philostler/rspec-sidekiq&quot;&gt;rspec-sidekiq&lt;/a&gt; gem for additional configuration and matcher options. A classic-looking spec for this job might look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;it &quot;updates the user&quot; do
  UpdateUserJob.perform_async(1, &quot;bob&quot;)
  expect(UpdateUserJob.jobs.size).to eq(1)
  UpdateUserJob.drain
  expect(User.find(1).name).to eq(&quot;bob&quot;)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to &lt;code&gt;drain&lt;/code&gt; the job before we can test the result of its work. Alternatively, we could wrap the test inside one of the Sidekiq testing helper methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;it &quot;updates the user&quot; do
  Sidekiq::Testing.inline! do
    UpdateUserJob.perform_async(1, &quot;bob&quot;)
    expect(User.find(1).name).to eq(&quot;bob&quot;)
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This does work as expected however it does leave a little to be desired. We have to remember to do this whenever testing job logic (the number of times I've had to search through previous specs to remind myself of the syntax), it adds a few extra lines to the tests which are not necessary and there is a spec performance hit as well.&lt;/p&gt;
&lt;h3 id=&quot;the-use-case-way&quot;&gt;The &quot;use case&quot; way&lt;/h3&gt;
&lt;p&gt;So, what options do we have? Well, the first option would be to extract the logic performed by the job into a class which we would then pass the job attributes to for processing. That way we can independently test the business logic without having to mess with background workers and the like. I have used various names for these classes, &lt;strong&gt;Use Case&lt;/strong&gt;, &lt;strong&gt;Resource&lt;/strong&gt;, or &lt;strong&gt;Library&lt;/strong&gt; to name a few (use whatever best suits you and your conventions). Our new job would look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class UpdateUserJob
  include Sidekiq::Job

  def perform(user_id, name)
    UpdateUserUseCase.new(user_id, name).call
  end
end

class UpdateUserUseCase
  def initialize(user_id, name)
    @user_id = user_id
    @name = name
  end

  def call
    user = User.find(@user_id)
    user.update!(name: @name)
    # .. additional business logic here
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, when it comes to testing, we could just forgo testing the actual job class as it does nothing except pass on some arguments to a plain old Ruby object. We can just test the class directly. Sidekiq is also already extensively tested so there is little point in testing whether the jobs on the queue have increased or not unless you are expecting new jobs to be created as a knock-on effect of your business logic.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;it &quot;updates the user&quot; do
  UpdateUserUseCase.new(1, &quot;bob&quot;).call
  expect(User.find(1).name).to eq(&quot;bob&quot;)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The added benefit to doing it this way is that you have a plain old Ruby object that you can call independently and inline in your code elsewhere should the need arise. For instance, you may want to defer the processing to the background when handling a mass import of data, however, when being performed as a single HTTP request to your API, you could wish to do it inline.&lt;/p&gt;
&lt;p&gt;The downside to this is that we now, however, have had to create an additional class to provide this flexibility. That means, if we wish to keep this convention going, for each &lt;code&gt;Job&lt;/code&gt; class, there would be a corresponding &lt;code&gt;UseCase&lt;/code&gt; class. What if we could take this idea one step further? What if we could turn our Job class into a plain old Ruby object right from the start? Let's rewrite our &lt;code&gt;UpdateUserJob&lt;/code&gt; class.&lt;/p&gt;
&lt;h3 id=&quot;the-runner-way-with-poros-&quot;&gt;The &quot;runner&quot; way (with POROs)&lt;/h3&gt;
&lt;p&gt;Let's pretend you had never heard of Sidekiq or background queues, and you want to write some code to perform a specific task. For the sake of convention, I have ended the following class name with &quot;Job&quot; to match previous examples however by syntax definition, it is no longer what we would call a classic &quot;job&quot;. It is now just a Plain Old Ruby Object (PORO; identical to our previous &lt;code&gt;UpdateUserUseCase&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class UpdateUserJob
  def initialize(user_id, name)
    @user_id = user_id
    @name = name
  end

  def call
    user = User.find(@user_id)
    user.update!(name: @name)
    # .. additional business logic here
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that in place, we need to set up some sort of mechanism to have this code be run by Sidekiq in our background queue. We can introduce a simple class to wrap the sidekiq calls, allowing us to inject our custom job class with its arguments. To not conflict with the Sidekiq namespace, I have created the &lt;code&gt;Runners&lt;/code&gt; module which will run this code for me &lt;em&gt;(side note: no matter what project I've worked on in the past, I always seem to use these &quot;runner&quot; style classes for various things)&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt; # Cannot figure out why this code block fails to format correctly here on CoderLegion
module Runners
  class Sidekiq
    include ::Sidekiq::Job

    sidekiq_options(retry: 3, backtrace: true, queue: &quot;default&quot;)

    def perform(job_class_name, *args)
      Kernel.const_get(job_class_name).new(*args).call
    end

    def run(job_class, *args)
      self.class.perform_async(job_class.to_s, *args)
    end

    def run_in(delay_in_seconds, job_class, *args)
      self.class.perform_in(delay_in_seconds, job_class.to_s, *args)
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that, we have everything we need to be able to throw plain old Ruby classes into the background for processing. If we wish to run our business logic inline, we simply run&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;UpdateUserJob.new(1, &quot;bob&quot;).call
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;but if we want to delegate the processing to the background queue, we can send it off immediately or delay it&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;Runners::Sidekiq.new.run(UpdateUserJob, 1, &quot;bob&quot;) # perform the job as soon as possible
Runners::Sidekiq.new.run_in(5.minutes, UpdateUserJob, 1, &quot;bob&quot;) # delay the processing by 5 minutes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.brazenbraden.com/assets/images/sidekiq_classes/update_user_job.png&quot; alt=&quot;Sidekiq Web View&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;bells-amp-whistles&quot;&gt;Bells &amp;amp; Whistles&lt;/h3&gt;
&lt;p&gt;Now that we have our lovely Sidekiq runner class, we can embellish it with a couple final bells and whistles. We might want to add / override the sidekiq_options on demand instead of being stuck with the previous hard-coded options. Perhaps we prefer using keyword arguments in our method definitions for the added clarity and ability to insert the arguments in any order. And maybe, we would like the job names in the Sidekiq UI to be a little more descriptive as to the actual job they are running so that we don't have to go digging through the arguments list to find specific jobs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt; # Cannot figure out why this code block fails to format correctly here on CoderLegion
module Runners
  class Sidekiq
    include ::Sidekiq::Job

    DEFAULT_OPTIONS = { retry: 3, backtrace: true, queue: &quot;default&quot; }

    def perform(job_class_name, *args)
      if args in [Hash]
        # This handles the case of keyword arguments
        job_class_name.constantize.new(**args.first.symbolize_keys!).call
      else
        # This handles the case of basic arguments
        job_class_name.constantize.new(*args).call
      end
    end

    def initialize(options = {})
      self.class.sidekiq_options(DEFAULT_OPTIONS.merge(options))
    end

    def run(job_class, *args)
      update_display_class(job_class)
      self.class.perform_async(job_class.to_s, *args)
    end

    def run_in(delay_in_seconds, job_class, *args)
      update_display_class(job_class)
      self.class.perform_in(delay_in_seconds, job_class.to_s, *args)
    end

    private

    def update_display_class(job_class)
      self.class.sidekiq_options.merge!(&quot;display_class&quot; =&amp;gt; &quot;#{job_class} [#{self.class.name}]&quot;)
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In essence, you can now define your background job classes as basic Ruby objects, execute them inline, or transfer them to Sidekiq for background processing via our Runner. The main requirement is maintaining a consistent DSL with your job classes, such as implementing a mandatory &lt;code&gt;call&lt;/code&gt; (or equivalent: &lt;code&gt;perform&lt;/code&gt; or &lt;code&gt;execute&lt;/code&gt;) method. (Kudos to my friend &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/krzyczak&quot;&gt;Kris&lt;/a&gt; for the original idea.)&lt;/p&gt;
</description>
<category>Ruby on Rails</category>
<guid isPermaLink="true">https://coderlegion.com/5088/better-sidekiq-classes</guid>
<pubDate>Thu, 28 Aug 2025 23:31:09 +0000</pubDate>
</item>
<item>
<title>Scaling Sidekiq in Ruby on Rails: Best Practices, War Stories, and WTF Moments</title>
<link>https://coderlegion.com/4983/scaling-sidekiq-in-ruby-on-rails-best-practices-war-stories-and-wtf-moments</link>
<description>&lt;p&gt;Imagine that your Rails application is running smoothly. Users are satisfied, payments are processed, emails are sent out, and reports are produced. Sidekiq feels like magic.&lt;/p&gt;
&lt;p&gt;Then one Friday evening (because production issues always happen on Fridays), you check the Sidekiq dashboard ????&lt;/p&gt;
&lt;p&gt;Jobs are piling up faster than your interns can say, “Did we forget to add sidekiq_options retry: false again?”&lt;/p&gt;
&lt;p&gt;Welcome to Sidekiq at scale—where background jobs stop being background and start being your entire life.&lt;/p&gt;
&lt;p&gt;Here are some lessons I've learned from implementing Sidekiq at scale in production, along with metrics and real-world examples.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-keep-jobs-small-and-fast&quot;&gt;1. Keep Jobs Small and Fast&lt;/h3&gt;
&lt;p&gt;Jobs should be atomic and quick. A &lt;strong&gt;5–10 second job is already too long&lt;/strong&gt; in most production systems.&lt;/p&gt;
&lt;p&gt;Bad example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class ReportJob
  def perform(user_id)
    user = User.find(user_id)
    generate_pdf(user)          # CPU-heavy
    upload_to_s3(user)          # I/O-heavy
    send_email(user)            # External API
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this fails halfway, the entire job retries.&lt;/p&gt;
&lt;p&gt;✅ Better:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class GeneratePdfJob &amp;lt; ApplicationJob
  def perform(user_id)
    PdfGenerator.call(User.find(user_id))
  end
end

class UploadReportJob &amp;lt; ApplicationJob
  def perform(file_path)
    S3Uploader.call(file_path)
  end
end

class SendReportJob &amp;lt; ApplicationJob
  def perform(user_id, report_id)
    ReportMailer.with(user_id:, report_id:).deliver_now
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chain jobs with callbacks or Sidekiq batches.&lt;/p&gt;
&lt;h3 id=&quot;2-idempotency-is-non-negotiable&quot;&gt;2. Idempotency is Non-Negotiable&lt;/h3&gt;
&lt;p&gt;Jobs will run &lt;strong&gt;at least once&lt;/strong&gt;, sometimes more. If jobs aren’t idempotent, you’ll double-charge customers or send duplicate emails.&lt;/p&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;order.mark_as_paid!
send_receipt(order)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;order.update!(status: &quot;paid&quot;) unless order.paid?
ReceiptMailer.send(order).deliver_later unless order.receipt_sent?
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-use-queues-strategically&quot;&gt;3. Use Queues Strategically&lt;/h3&gt;
&lt;p&gt;Don’t dump everything into &lt;code&gt;default&lt;/code&gt;. Separate workloads:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;critical&lt;/code&gt; → user-facing (emails, webhooks, payments)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;default&lt;/code&gt; → normal jobs (notifications, syncs)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;low&lt;/code&gt; → heavy, non-urgent (reports, exports, ETL)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;sidekiq.yml&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;:queues:
  - [critical, 5]
  - [default, 3]
  - [low, 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-monitor-everything&quot;&gt;4. Monitor Everything&lt;/h3&gt;
&lt;p&gt;Visibility is critical at scale:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sidekiq Web UI&lt;/strong&gt; for queue depth &amp;amp; retries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SQL snippet to monitor retries:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT count(*)
FROM sidekiq_jobs
WHERE queue = 'default'
AND retry = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-control-concurrency&quot;&gt;5. Control Concurrency&lt;/h3&gt;
&lt;p&gt;More threads ≠ better throughput. High concurrency can overwhelm Postgres or external APIs.&lt;/p&gt;
&lt;p&gt;✅ Start with concurrency = &lt;strong&gt;2–5x your CPU cores&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;:concurrency: 15
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-redis-is-your-heartbeat&quot;&gt;6. Redis is Your Heartbeat&lt;/h3&gt;
&lt;p&gt;Redis is the single point of truth for Sidekiq.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;dedicated Redis&lt;/strong&gt; (not shared with cache/session store)&lt;/li&gt;
&lt;li&gt;Monitor memory &amp;amp; latency&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-smart-retries-only&quot;&gt;7. Smart Retries Only&lt;/h3&gt;
&lt;p&gt;Default retries (up to 25) can flood your system. Customize them.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class MyJob
  sidekiq_options retry: 3

  def perform
    ExternalService.call!
  rescue ExternalService::InvalidCredentials
    # Don’t retry, just fail fast
    raise Sidekiq::Shutdown
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-deploy-with-process-separation&quot;&gt;8. Deploy with Process Separation&lt;/h3&gt;
&lt;p&gt;Don’t run one mega Sidekiq instance. Separate by queue type.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bundle exec sidekiq -q critical,5 -q default,2
bundle exec sidekiq -q low
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-dead-jobs-are-zombie-failures&quot;&gt;9. Dead Jobs are Zombie Failures&lt;/h3&gt;
&lt;p&gt;Dead jobs = silent failures.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bundle exec sidekiqctl clean --all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or query them in Redis:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;Sidekiq::DeadSet.new.size
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;10-autoscale-or-suffer&quot;&gt;10. Autoscale or Suffer&lt;/h3&gt;
&lt;p&gt;In Kubernetes, ECS, or Heroku, autoscale workers on queue latency:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scale up if latency &amp;gt; 30s&lt;/li&gt;
&lt;li&gt;Scale down if latency &amp;lt; 5s&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;11-chain-jobs-for-external-api-pagination&quot;&gt;11. Chain Jobs for External API Pagination&lt;/h3&gt;
&lt;p&gt;Sometimes, the firehose comes from outside your app. A classic case: syncing a huge dataset from a third-party API that only gives you paginated results.&lt;/p&gt;
&lt;p&gt;If you naively fetch all pages in one job, you’ll hit rate limits, blow up memory, and make retries a nightmare.&lt;/p&gt;
&lt;p&gt;✅ Better: chain jobs page by page. Each job handles one page, then enqueues the next, until the API says you’re done. That way:&lt;br&gt;
Jobs stay tiny and idempotent.&lt;/p&gt;
&lt;p&gt;Failures retry gracefully without restarting the whole sync.&lt;br&gt;
You respect API rate limits with backoff and jitter.&lt;/p&gt;
&lt;p&gt;You can fan out each page’s items to dedicated jobs for massive throughput.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class SyncExternalPage
  include Sidekiq::Worker
  sidekiq_options queue: :sync, retry: 10

  def perform(cursor = nil)
    response = ExternalClient.fetch_page(cursor: cursor)

    response[:items].each do |raw|
      UpsertExternalItem.perform_async(raw) # idempotent per item
    end

    if response[:next_cursor]
      # Chain the next page
      self.class.perform_in(1, response[:next_cursor])
    else
      Rails.logger.info(&quot;Sync complete ✅&quot;)
    end
  end
end

class UpsertExternalItem
  include Sidekiq::Worker

  def perform(raw)
    ExternalRecord.upsert(
      { external_id: raw[&quot;id&quot;], name: raw[&quot;name&quot;] },
      unique_by: :index_external_records_on_external_id
    )
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern lets you chew through millions of external records without drowning your system in retries or hitting API bans.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Sidekiq is incredibly robust, but at scale, you need production-grade practices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep jobs &lt;strong&gt;small, fast, idempotent&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Prioritize with &lt;strong&gt;queues&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Monitor &lt;strong&gt;latency &amp;amp; retries&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Tune &lt;strong&gt;concurrency &amp;amp; Redis&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;autoscaling&lt;/strong&gt; to survive traffic spikes&lt;/li&gt;
&lt;li&gt;Chain jobs for external API pagination to handle massive datasets gracefully&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With these practices, you can confidently handle &lt;strong&gt;millions of jobs per day&lt;/strong&gt; without breaking a sweat.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;✍️ Your turn: what’s your worst Sidekiq horror story? Did you also accidentally email your entire user base at 3am?&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/4983/scaling-sidekiq-in-ruby-on-rails-best-practices-war-stories-and-wtf-moments</guid>
<pubDate>Mon, 25 Aug 2025 00:27:16 +0000</pubDate>
</item>
<item>
<title>JuggleBee’s Great Leap – Data Migration, ActiveStorage, and Production Readiness (Part 2)</title>
<link>https://coderlegion.com/4934/jugglebees-great-leap-data-migration-activestorage-and-production-readiness-part-2</link>
<description>&lt;p&gt;Now that we had a deployable app, I set up a staging server using my new favourite provider, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.hetzner.com/&quot;&gt;Hetzner&lt;/a&gt; (&lt;em&gt;not a sponsor, lol&lt;/em&gt;), spun it up, and witnessed a “fully functional” skeleton of JuggleBee. Stage 1 of the migration plan was complete.&lt;/p&gt;
&lt;p&gt;Now came the real meat and potatoes of the project — actually hydrating the new app with all of its production data. We’re talking over 50,000 images, countless database records, and security credentials… the works. This is the part where a missed step could mean broken listings, missing files, or downtime. No pressure.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;In case you missed &lt;strong&gt;Part 1&lt;/strong&gt;, we covered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why I started fresh on &lt;strong&gt;Rails 8&lt;/strong&gt; instead of doing incremental upgrades.&lt;/li&gt;
&lt;li&gt;Migrating core &lt;strong&gt;models/controllers/services&lt;/strong&gt; to modern Rails conventions.&lt;/li&gt;
&lt;li&gt;Swapping Sprockets for &lt;strong&gt;importmaps&lt;/strong&gt; and modernizing the JavaScript setup.&lt;/li&gt;
&lt;li&gt;Replacing &lt;strong&gt;Sidekiq + Redis + whenever&lt;/strong&gt; with &lt;strong&gt;ActiveJob + SolidQueue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Moving deployments to &lt;strong&gt;Kamal 2&lt;/strong&gt; with Traefik and Let’s Encrypt.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Catch up on it here:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.brazenbraden.com/posts/jugglebee_rails_upgrade_part1/&quot;&gt;JuggleBee’s Great Leap – Rebuilding a Rails 4 App in Rails 8 (Part 1)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now—back to the post.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-daunting-database-migration&quot;&gt;The Daunting Database Migration&lt;/h3&gt;
&lt;p&gt;Legacy JuggleBee had been coasting along happily on Postgres 9.6 — which, back in 2016, was the latest and greatest. Fast forward to today and it’s long past its sell-by date, with official support ending in November 2021. Upgrading wasn’t just about keeping up with the times; Postgres 17.5 brings some serious perks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Native JSON field type support.&lt;/li&gt;
&lt;li&gt;Incremental backups with &lt;code&gt;pg_basebackup --incremental&lt;/code&gt;, slashing backup time and storage usage.&lt;/li&gt;
&lt;li&gt;Noticeable performance gains from better memory management, indexing, and partitioning.&lt;/li&gt;
&lt;li&gt;Improved observability via enhanced &lt;code&gt;EXPLAIN&lt;/code&gt; options and richer stats views.&lt;/li&gt;
&lt;li&gt;…and more.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It was a no-brainer to upgrade, but the version gap meant one wrong move could corrupt thousands of user, auction, and invoice records. That made reliable backups non-negotiable before touching a single byte. Luckily, I already had a manual backup process baked into my deploy routine. Here’s the script (&lt;em&gt;feel free to steal any of the scripts in my posts&lt;/em&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

DB_CONTAINER=($(sudo docker ps | grep postgres | awk '{print $NF}'))
BACKUP_NAME=dump_`date +%d-%m-%Y&quot;_&quot;%H_%M_%S`.sql
BACKUP_FOLDER=db_backups
KEEP=10

# Dump the database into a backup sql file
echo &quot;Creating the database dump...&quot;
docker exec -t $DB_CONTAINER pg_dumpall -c -U postgres &amp;gt; $BACKUP_NAME
echo &quot;Database dump created.&quot;

echo &quot;Moving database backup to backup folder...&quot;
mkdir -p $BACKUP_FOLDER
mv $BACKUP_NAME $BACKUP_FOLDER
echo &quot;Backup moved to backup folder.&quot;

echo &quot;Deleting old releases...&quot;
(cd $BACKUP_FOLDER &amp;amp;&amp;amp; (ls -t | head -n $KEEP; ls) | sort | uniq -u | xargs rm -rf)
echo &quot;Backup stored.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One catch: &lt;code&gt;pg_dumpall&lt;/code&gt; (used above) tries to dump &lt;em&gt;everything&lt;/em&gt; — including Postgres cluster-wide settings — which can be version-specific and cause headaches on import. For a major version jump, you want &lt;code&gt;pg_dump&lt;/code&gt;, which targets a single database and avoids the version mismatch landmines.&lt;/p&gt;
&lt;p&gt;For the migration, I only needed to change the &lt;code&gt;docker&lt;/code&gt; command we executed - with the added ENV vars, that is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;DB_USER=db_user
DB_PASSWORD=db_password
DATABASE_NAME=database_name

# ...

docker exec -e PGPASSWORD=$DB_PASSWORD -t $DB_CONTAINER \
    pg_dump -U $DB_USER $DATABASE_NAME &amp;gt; $BACKUP_NAME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the new backup in hand, importing into Postgres 17.5 went off without a hitch — no edits, no drama. Honestly, I was braced for a fight given the version gap, but the data was simple enough (and avoided fancy Postgres features) that it just… worked. A rare and pleasant surprise in a project full of moving parts.&lt;/p&gt;
&lt;h3 id=&quot;listing-image-migration&quot;&gt;Listing Image Migration&lt;/h3&gt;
&lt;p&gt;Unlike the data migration before it, migrating all the listing images (just shy of 50,000!) came with its own set of headaches. The legacy setup used CarrierWave with a custom uploader and fixed S3 paths, which worked fine at the time but wasn’t exactly future-proof. Since I was already upgrading to Rails 8, it made sense to tackle this technical debt head-on and move to ActiveStorage — effectively retiring these gems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;carrierwave&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fog-aws&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mini_magick&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;… and replacing them with &lt;code&gt;image_processing&lt;/code&gt; for post-processing.&lt;/p&gt;
&lt;p&gt;In the old days, uploads required creating an “Uploader” class and mounting it on a model. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/image.rb
class Image &amp;lt; ActiveRecord::Base
  mount_uploader :aws, ImageUploader
end

# app/uploaders/image_uploader.rb
class ImageUploader &amp;lt; CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  storage :fog

  process resize_to_fill: [850, 850]

  version :thumb do
    process resize_to_fill: [280, 280]
  end

  def store_dir
    &quot;listings/#{model.listing_id}/#{model.id}&quot;
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With ActiveStorage, uploaders and hard-coded S3 paths are gone. You simply declare the attachment in your model (after setting up &lt;code&gt;config/storage.yml&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class Image &amp;lt; ApplicationRecord
  has_one_attached :file do |attachable|
    attachable.variant :thumb, resize_to_limit: THUMB_DIMENSIONS, preprocessed: true
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That takes care of &lt;em&gt;future&lt;/em&gt; uploads — but what about migrating the 50,000+ legacy images stored in the old bucket? The &lt;code&gt;image.aws&lt;/code&gt; field plus CarrierWave’s &lt;code&gt;store_dir&lt;/code&gt; gave me the exact file path to each image. I then wrote a script to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download each image from the legacy bucket.&lt;/li&gt;
&lt;li&gt;Re-upload it through ActiveStorage so it generates its own hashed storage path.&lt;/li&gt;
&lt;li&gt;Skip already-migrated images to allow safe re-runs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s the script in all its glory (run synchronously to avoid AWS rate limits — which meant a cool 4 hours of migration time):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;#!/usr/bin/env ruby

require_relative '../config/environment'
require 'aws-sdk-s3'
require 'stringio'

OLD_S3_BUCKET = Rails.application.credentials.dig(:old_aws, :bucket)
OLD_S3_REGION = Rails.application.credentials.dig(:old_aws, :region)
OLD_S3_ACCESS_KEY_ID = Rails.application.credentials.dig(:old_aws, :s3_key)
OLD_S3_SECRET_ACCESS_KEY = Rails.application.credentials.dig(:old_aws, :s3_secret)

# Optional: skip already migrated images
SKIP_EXISTING = true

s3 = Aws::S3::Client.new(
  region: OLD_S3_REGION,
  access_key_id: OLD_S3_ACCESS_KEY_ID,
  secret_access_key: OLD_S3_SECRET_ACCESS_KEY
)

puts &quot;Starting image migration...&quot;

Image.find_each do |image|
  begin
    if SKIP_EXISTING &amp;amp;&amp;amp; image.file.attached?
      puts &quot;Skipping Image #{image.id} (already attached)&quot;
      next
    end

    # Construct old S3 key from uploader logic - your path here
    key = &quot;listings/#{image.listing_id}/#{image.id}/#{File.basename(image[:aws].to_s)}&quot;

    obj = s3.get_object(bucket: OLD_S3_BUCKET, key: key)

    image.file.attach(
      io: StringIO.new(obj.body.read),
      filename: File.basename(key),
      content_type: obj.content_type || 'image/jpeg'
    )

    puts &quot;Migrated Image #{image.id} (Listing #{image.listing_id})&quot;
  rescue Aws::S3::Errors::NoSuchKey =&amp;gt; e
    warn &quot;Image #{image.id} missing in old bucket: #{key}&quot;
  rescue =&amp;gt; e
    warn &quot;Failed to migrate Image #{image.id}: #{e.class} - #{e.message}&quot;
  end
end

puts &quot;Migration script finished&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four hours later, we were in business — all images were now managed by ActiveStorage.&lt;br&gt;
Future uploads are handled automatically, thumbnails are generated on demand, and the whole setup is cleaner and easier to maintain.&lt;/p&gt;
&lt;h3 id=&quot;credentials-amp-security&quot;&gt;Credentials &amp;amp; Security&lt;/h3&gt;
&lt;p&gt;You might have noticed the use of &lt;code&gt;Rails.application.credentials&lt;/code&gt; in the previous script. That’s new territory — Rails encrypted credentials didn’t exist back in 4.2.&lt;/p&gt;
&lt;p&gt;Back then we had &lt;code&gt;secrets.yml&lt;/code&gt;, which stored everything in plain text and was meant to be excluded from Git. In theory, you’d inject it during deploy with whatever tooling you used. In practice… I was lazy. My deploys were manual, the repo was private, and I just checked them straight in. &lt;strong&gt;Big no-no.&lt;/strong&gt; Thankfully, Bitbucket never got hacked — but let’s just say this setup wouldn’t pass any kind of security audit.&lt;/p&gt;
&lt;p&gt;Rails 5.2 introduced encrypted credentials — finally, a way to store secrets in plain text &lt;em&gt;locally&lt;/em&gt; while Rails encrypts them with a master key. That master key stays out of Git and gets injected during deploy. Much better.&lt;/p&gt;
&lt;p&gt;Fast forward to today, and Kamal (my de facto deployment tool) takes it a step further with &lt;a rel=&quot;nofollow&quot; href=&quot;https://kamal-deploy.org/docs/commands/secrets/&quot;&gt;built-in support for pulling secrets&lt;/a&gt; straight from password managers like Bitwarden, 1Password, or LastPass. That means my Rails master key, database credentials, and other environment variables can live safely in Bitwarden, and Kamal just grabs them during deploy. No manual copying, no “where did I put that key?” moments, and no plain-text secrets hanging around in the repo.&lt;/p&gt;
&lt;p&gt;Here’s an example using Bitwarden via the CLI. This snippet fetches all secrets and assigns them to variables in &lt;code&gt;.kamal/secrets&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SECRETS=$(kamal secrets fetch --adapter bitwarden-sm all)

KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
POSTGRES_USER=$(kamal secrets extract POSTGRES_USER $SECRETS)
POSTGRES_PASSWORD=$(kamal secrets extract POSTGRES_PASSWORD $SECRETS)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only catch: you’ll need to have the Bitwarden CLI installed and authenticated on every machine you deploy from. It’s a one-time setup, but totally worth it for the peace of mind of never having to worry about leaked secrets again.&lt;/p&gt;
&lt;h3 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;/h3&gt;
&lt;p&gt;Looking back, a few big takeaways stood out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scripts are your best friend.&lt;/strong&gt; Whether it’s a database dump, image migration, or credentials setup, having a repeatable script means you can run it, tweak it, and run it again without reinventing the wheel. It also removes a ton of mental overhead when you’re juggling multiple moving parts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test migrations in staging — thoroughly.&lt;/strong&gt; Moving to PostgreSQL 17.5 could have been a disaster if I’d gone straight into production. Running the full migration end-to-end in a safe environment gave me confidence and caught subtle issues before they ever touched live data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expect the cleanup phase.&lt;/strong&gt; Even after the “big ticket” migrations were done, staging revealed a laundry list of smaller fixes — broken image links, JavaScript load order issues, outdated gem calls, and other gremlins. None of these were major individually, but together they took nearly two weeks to iron out. Build that buffer into your timeline.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Take the opportunity to ditch technical debt.&lt;/strong&gt; CarrierWave, fog-aws, Sidekiq, Redis… all of these had served me well for years, but replacing them with ActiveStorage and SolidQueue means fewer dependencies to maintain, fewer services to monitor, and a cleaner architecture.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security deserves equal billing with features.&lt;/strong&gt; Rails encrypted credentials plus Bitwarden CLI integration finally dragged my secrets management into the modern age. No more plain-text secrets in Git. Enough said.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modern Rails is worth the leap.&lt;/strong&gt; From importmaps to Kamal, the tooling around Rails 8 makes deployment, scaling, and maintenance dramatically simpler than what I was doing in 2015.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h3&gt;
&lt;p&gt;And with that, JuggleBee officially joined the modern Rails era.&lt;/p&gt;
&lt;p&gt;This wasn’t just a version bump — it was a full-stack rejuvenation.&lt;br&gt;
The app runs faster, the infrastructure is leaner, and I finally have a deployment pipeline I can trust at 2 a.m. without crossing my fingers.&lt;/p&gt;
&lt;p&gt;Was it worth skipping incremental upgrades? Absolutely. Starting fresh on Rails 8 gave me a clean slate to rebuild only what mattered, using tools that will keep JuggleBee humming for years to come.&lt;/p&gt;
&lt;p&gt;If you’ve been sitting on a legacy Rails app, staring down the upgrade path and wondering if it’s worth the effort — it is. Just plan your migrations, test them in staging, and embrace the opportunity to shed some old weight while you’re at it.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.jugglebee.com&quot;&gt;JuggleBee’s&lt;/a&gt; now live on &lt;strong&gt;Rails 8 / Ruby 3.4.3 / PostgreSQL 17.5&lt;/strong&gt;, with ActiveStorage handling thousands of images, Bitwarden keeping our secrets safe, and Kamal making deployments a one-command affair.&lt;/p&gt;
&lt;p&gt;Now it’s time to step away from the terminal, grab a cold beer, and actually enjoy the thing I’ve been rebuilding over the last month — until the next big upgrade comes along.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4934/jugglebees-great-leap-data-migration-activestorage-and-production-readiness-part-2</guid>
<pubDate>Sun, 24 Aug 2025 10:52:32 +0000</pubDate>
</item>
<item>
<title>JuggleBee’s Great Leap - Rebuilding a Rails 4 App in Rails 8 (Part 1)</title>
<link>https://coderlegion.com/4894/jugglebees-great-leap-rebuilding-a-rails-4-app-in-rails-8-part-1</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;http://www.jugglebee.com&quot;&gt;JuggleBee&lt;/a&gt; was born in 2015. It was Namibia's first online auction platform and is still one of the biggest today. I built it with Ruby on Rails 4.2 on Ruby 2.2. It sat there, rock-solid and stubbornly stable, only needing the occasional dust-off and minor tweak over the years. At various points in time, I thought about upgrading the various versions; however, I was stuck between a rock and a hard place — in order to upgrade Ruby, I had to upgrade Rails, and vice versa. The idea of incremental upgrades felt like a nightmare — two battlefronts to fight on, Ruby on one side and Rails on the other.&lt;/p&gt;
&lt;p&gt;Fast forward to 2025. With the arrival of my daughter, I acquired a smattering of free time interspersed with sleepless nights. The perfect time to finally bite the bullet, bring JuggleBee into the 21st century (you know what I mean), and get these upgrades done. After much pondering and planning, only one realistic option remained... incremental upgrades would take months with constant patching between every version bump; I would have to take the leap from Rails 4.2 straight to Rails 8, with Ruby hopping from 2.2 to 3.4.3. It was quite the journey, and what follows is a breakdown of the challenges and learnings I made along the way.&lt;/p&gt;
&lt;h3 id=&quot;starting-fresh&quot;&gt;Starting Fresh&lt;/h3&gt;
&lt;p&gt;Not to blow my own horn or anything, but I was in a pretty good place with this migration plan. The codebase of JuggleBee was intentionally built with self-contained business logic components, defining a clear separation of concerns. The models and controllers were not overly bloated, with liberal usage of &quot;service objects&quot; performing the more complicated logic, and an added separation of logic between my controllers and views using the &quot;view decorator&quot; pattern. This architecture made the copy-pasta approach almost… dare I say… fun. But let’s not kid ourselves — a jump from Rails 4.2 to 8 is no walk in the park.&lt;/p&gt;
&lt;p&gt;As part of the &lt;code&gt;rails new&lt;/code&gt; process (after installing the latest Ruby and necessary Rails gems), and before the true code migration could begin, I just needed an empty, runnable project with the necessary gems installed. I appended my old &lt;code&gt;Gemfile&lt;/code&gt; with the auto-generated one, removing all locked versions to ensure the latest compatible versions were installed. I also identified irrelevant and dead gems, removing those and replacing them with alternatives where necessary (more detail on this later).&lt;/p&gt;
&lt;p&gt;Reading into each of the default gems specified by the &lt;code&gt;rails new&lt;/code&gt; generator gave me a good understanding of what the current Rails stack looks like. I didn’t need all of it, so some were commented out, but I did take some of the new suggestions into account. &lt;code&gt;Puma&lt;/code&gt;, for example, replaced &lt;code&gt;Unicorn&lt;/code&gt;, which I was previously using. I could also get rid of &lt;code&gt;God&lt;/code&gt;, in the non-divine sense, as I no longer needed to monitor my server state manually. All in all, this whole process didn’t take more than an hour or so.&lt;/p&gt;
&lt;h3 id=&quot;models-controllers-services-amp-views-in-that-order-&quot;&gt;Models, Controllers, Services &amp;amp; Views (in that order)&lt;/h3&gt;
&lt;p&gt;These being purely Ruby code made it the easiest place to start. Even though we jumped up from Ruby 2.2 to 3.4.3, not all that much in the core language changed. Twas Rails that was the most behind. For example, JuggleBee was developed before the abstract &lt;code&gt;ApplicationRecord&lt;/code&gt; class made its way into the Rails architecture!&lt;/p&gt;
&lt;h4 id=&quot;models&quot;&gt;Models&lt;/h4&gt;
&lt;p&gt;The models were the easiest to migrate over, as they already had a smattering of unit tests behind them. First, though, we had to get the database up and running. Spoiler alert: I was also updating our Postgres version from &lt;strong&gt;9.6&lt;/strong&gt; to &lt;strong&gt;17.5&lt;/strong&gt; as part of this big update. The old migration files were fossils — brittle, outdated, and utterly unfit for a modern Rails 8 app. The simplest solution was to drop a &lt;code&gt;pg_dump&lt;/code&gt; of the current database (just on my local copy of the now legacy JuggleBee) and import it, generating the &lt;code&gt;schema.rb&lt;/code&gt;. I could now copy across my models and test them out in the &lt;code&gt;rails console&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Some of the updates required to get things up and running were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;belongs_to&lt;/code&gt; is now a required atribute. I have a home-grown version of polymorphism which allows a &lt;code&gt;Listing&lt;/code&gt; to be either a &lt;code&gt;Product&lt;/code&gt; or &lt;code&gt;Auction&lt;/code&gt; and the &lt;code&gt;belongs_to&lt;/code&gt; on these associations are optional, so I had to update these associations accordingly (*&lt;strong&gt;side note&lt;/strong&gt;: a refactor waiting to happen*). So,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;belongs_to :auction
belongs_to :product
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;becomes&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;belongs_to :auction, optional: true
belongs_to :product, optional: true
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I had a couple of instances of &lt;code&gt;update_attributes&lt;/code&gt; (very selective. Given they bypass validations, they were used with extreme caution) which is now deprecated and had to be changed to &lt;code&gt;update&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deprecated validation methods such as &lt;code&gt;validates_presence_of :name&lt;/code&gt; needed updating to &lt;code&gt;validates :name, presence: true&lt;/code&gt; sort of changes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As for the views, the gems I used (&lt;code&gt;haml-rails&lt;/code&gt;, &lt;code&gt;redcarpet&lt;/code&gt;, &lt;code&gt;simple_form&lt;/code&gt;, &lt;code&gt;cocoon&lt;/code&gt;, etc) are all still actively maintained and required no changes at all — a very pleasant surprise.&lt;/p&gt;
&lt;h4 id=&quot;controllers&quot;&gt;Controllers&lt;/h4&gt;
&lt;p&gt;Next up were the controllers. Thanks to my use of service objects for the bulk of complex logic, migrating controllers was mostly painless. Rails 8 is stricter, and that’s where most of my changes were required. While bringing over controllers, I also began migrating the service objects they called upon, which are POROs and required almost no changes.&lt;/p&gt;
&lt;p&gt;The main updates included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Params passed to background jobs now must be valid JSON. This was especially relevant for controllers triggering email or background tasks.&lt;/li&gt;
&lt;li&gt;Controller &lt;code&gt;filter&lt;/code&gt;s were renamed to &lt;code&gt;action&lt;/code&gt;s (&lt;code&gt;before_filter&lt;/code&gt; → &lt;code&gt;before_action&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Controller hooks like &lt;code&gt;before_action&lt;/code&gt; should come after the method they reference, not at the top of the file (a style guideline enforced in modern Rails apps).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redirect_to :back&lt;/code&gt; was replaced with &lt;code&gt;redirect_back fallback_location: root_path&lt;/code&gt; (or an appropriate fallback path).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;responders&lt;/code&gt; gem is no longer included in Rails, so &lt;code&gt;respond_with&lt;/code&gt; calls were rewritten using &lt;code&gt;respond_to&lt;/code&gt; blocks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All in all, the MVC migration was more about cleaning things up and embracing modern Rails conventions rather than fighting major incompatibilities.&lt;/p&gt;
&lt;h3 id=&quot;javascript-modernization&quot;&gt;JavaScript Modernization&lt;/h3&gt;
&lt;p&gt;This is where things started to get spicy — the kind of spicy where you wonder if you just bit off more than you can chew. So much has changed in the front-end assets side of things. JuggleBee leverages &lt;strong&gt;jQuery&lt;/strong&gt; (thankfully I dodged the &lt;em&gt;CoffeeScript&lt;/em&gt; bullet) with Sprockets as the asset pipeline. Modern Rails favours &lt;code&gt;importmaps&lt;/code&gt; as the default asset manager and pipeline, and keeping with the spirit of rejuvenating JuggleBee and bringing it up to speed, I signed up to it too. This allowed me to replace a bunch of gems which added various front-end features, such as &lt;code&gt;chartkick&lt;/code&gt;, &lt;code&gt;masonry&lt;/code&gt; and &lt;code&gt;bootstrap&lt;/code&gt; with the importmaps &lt;code&gt;pin&lt;/code&gt; alternative. Some libraries had to remain as CDN imports in my layout file, due to the order at which these JavaScripts got initialized and loaded by the DOM.&lt;/p&gt;
&lt;p&gt;My suite of JavaScript classes also needed a bit of love and attention. The Sprockets&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//= require
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;statements were replaced with&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and my classes now had to &lt;code&gt;export&lt;/code&gt; themselves so that they could be loaded as ES modules. All the files had to be relocated from &lt;code&gt;app/assets/javascripts/&lt;/code&gt; to &lt;code&gt;app/javascript/&lt;/code&gt;. The way we add our JavaScript base file into our application view layout also had to change slightly, going from&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-erb&quot;&gt;&amp;lt;%= javascript_include_tag &quot;application&quot; %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-erb&quot;&gt;&amp;lt;%= javascript_importmap_tags %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even though &lt;code&gt;jQuery&lt;/code&gt; is considered a bit long in the tooth, given the plethora of really solid UI frameworks like Vue and React, I opted to keep my reliance on it for the time being. It still works, is actively maintained, and trying to replace it now would be a whole new kettle of fish. An adventure for another day.&lt;/p&gt;
&lt;h3 id=&quot;background-jobs-amp-scheduling&quot;&gt;Background Jobs &amp;amp; Scheduling&lt;/h3&gt;
&lt;p&gt;This area was one of the biggest wins in the entire migration. JuggleBee had the “classic” background processing setup: &lt;strong&gt;Sidekiq + Redis + whenever&lt;/strong&gt; (for cron). It worked fine for years, but it always felt a little… heavy, like lugging around a toolbox just to tighten a single screw. Sidekiq required its own container, Redis was a hungry beast that always felt like it was asking for more memory, and &lt;code&gt;whenever&lt;/code&gt; meant fiddling with cron jobs and ensuring everything stayed in sync across deploys. It worked, but it was yet another moving part in an already aging stack.&lt;/p&gt;
&lt;p&gt;Enter &lt;strong&gt;SolidQueue&lt;/strong&gt; — a gem that ships with Rails 8, built to replace Sidekiq-like setups with something that is both simpler and more tightly integrated with ActiveJob. The real beauty? &lt;strong&gt;No Redis.&lt;/strong&gt; SolidQueue uses the same database you already have (Postgres in our case) to manage job queues, which meant I could drop an entire service and reclaim a chunk of server memory.&lt;/p&gt;
&lt;p&gt;Here’s what this meant for JuggleBee:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No more Sidekiq configuration, container management, or Redis headaches.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;whenever&lt;/code&gt; gem (used for cron scheduling) could be thrown out too, thanks to SolidQueue’s built-in &lt;code&gt;recurring.yml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Background jobs now feel like a natural part of Rails — no extra glue required.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To put it in perspective, my stack went from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Rails App + Sidekiq + Redis + whenever + cron
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;to&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Rails App + SolidQueue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. Beautiful, minimal, and a lot less to maintain.&lt;/p&gt;
&lt;p&gt;Another &lt;em&gt;chef’s kiss&lt;/em&gt; moment was how little code I had to change. Most jobs migrated over with minimal tweaks — the biggest adjustment being that job arguments &lt;strong&gt;must be JSON-serializable&lt;/strong&gt;. This meant replacing keyword arguments with hashes like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;EmailJob.perform(&quot;MyEmailClass&quot;, {&quot;from&quot; =&amp;gt; current_user.name, &quot;subject&quot; =&amp;gt; &quot;Hello there&quot;})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;EmailJob.perform(&quot;MyEmailClass&quot;, from: current_user.name, subject: &quot;Hello there&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It was one of those rare upgrades where less really is more. Dropping Redis and cron alone made this whole migration feel worth it.&lt;/p&gt;
&lt;h3 id=&quot;infrastructure-amp-deployment&quot;&gt;Infrastructure &amp;amp; Deployment&lt;/h3&gt;
&lt;p&gt;For years, deployment was powered by a custom shell script I wrote. It wasn’t flashy, but it was reliable and got the job done. It handled atomic releases, cleaned up old deployments, built Docker images, restarted containers, and even maintained a cache for faster updates. For a single-developer project like JuggleBee, it was the perfect balance of simplicity and control.&lt;/p&gt;
&lt;p&gt;Here’s the core of that script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

branch=${1:-master}
cwd=/home/ubuntu
repo_url=your_repo_here
revision=master
keep=5
timestamp=$(date +%s)
cache=$cwd/.git_cache
releases=$cwd/releases
mkdir -p $releases
previous=$releases/$(ls $releases/ -t | head -n 1)
release=$releases/$timestamp

if [ ! -d $cache ] ; then
        git clone $repo_url $cache
fi

echo &quot;downloading latest source code from $branch...&quot;
mkdir -p $release
(cd $cache &amp;amp;&amp;amp; git gc --prune=now &amp;amp;&amp;amp; git remote prune origin &amp;amp;&amp;amp; git fetch origin &amp;amp;&amp;amp; git reset --hard origin/$branch &amp;amp;&amp;amp; git archive $revision | tar -x -c $release)

echo &quot;release timestamp is: $timestamp ($release)&quot;

echo 'building docker images...'
/bin/bash -c &quot;(cd $release &amp;amp;&amp;amp; docker compose -f docker-production.yml build)&quot;

if [ $? -eq 0 ]
then
        if [[ $previous != &quot;&quot; ]] ; then
                echo 'stopping previous containers...'
                /bin/bash -c &quot;(cd $previous &amp;amp;&amp;amp; docker compose -f docker-production.yml stop &amp;amp;&amp;amp; docker ps -a -q --filter='status=exited' | xargs docker rm)&quot;
        fi

        echo 'restarting application...'
        /bin/bash -c &quot;(cd $release &amp;amp;&amp;amp; docker compose -f docker-production.yml up -d)&quot;

        echo 'deleting old releases...'
        (cd $releases &amp;amp;&amp;amp; (ls -t | head -n $keep; ls) | sort | uniq -u | xargs rm -rf)

        echo 'creating symlink.'
        rm $cwd/current-release
        ln -s $release $cwd/current-release

        echo 'deploy complete.'
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Honestly, this script could have kept soldiering on for years… until &lt;strong&gt;Kamal&lt;/strong&gt; walked in like the cool new kid who makes your old tricks look ancient. It wasn’t broken — far from it — but Kamal is like having a personal DevOps engineer in your terminal. It takes all the things I cared about (Dockerized deploys, atomic releases, rollbacks) and adds extra superpowers I didn’t even realize I needed.&lt;/p&gt;
&lt;h3 id=&quot;introducing-kamal-2&quot;&gt;Introducing Kamal 2&lt;/h3&gt;
&lt;p&gt;Kamal is a server provisioning and deployment tool built specifically (but not limited to) Rails apps. It automates a ton of tasks that my script didn’t — and one of the most significant changes? &lt;strong&gt;No more Nginx.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Kamal ships with a built-in Traefik proxy that handles all your routing and SSL certificates out of the box. No more tinkering with &lt;code&gt;nginx.conf&lt;/code&gt; files or restarting a separate webserver. One less dependency chewing through RAM and one less thing to break at 2 am.&lt;/p&gt;
&lt;p&gt;Here’s what Kamal brings to the table:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;kamal setup&lt;/code&gt;&lt;/strong&gt;: installs Docker, Git, and all required dependencies on your server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;kamal deploy&lt;/code&gt;&lt;/strong&gt;: builds your Rails app into a Docker image, pushes it to your registry, and runs it on the server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Traefik proxy with SSL (via Let’s Encrypt)&lt;/strong&gt;: you get HTTPS without lifting a finger.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessory management&lt;/strong&gt;: want Postgres or Redis? Kamal spins them up as Docker containers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaling&lt;/strong&gt;: need more capacity? add another container in deploy.yml and Kamal will route traffic automatically.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The impact of this was huge. I went from managing Docker builds, old release directories, and an Nginx webserver, to just running one command. There’s something almost unsettling about how smooth it is — I felt like I was missing steps at first, like forgetting my keys when leaving the house.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Wait, that’s it? Did I actually deploy? Did I forget something? Oh… it’s done already. Huh.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This wasn’t just a convenience win — it was a dependency detox. Between dropping Nginx here and killing Redis with SolidQueue earlier, my infrastructure got a lot lighter, faster, and cheaper to run. Kamal didn’t just evolve my old script — it rendered half of its logic unnecessary.&lt;/p&gt;
&lt;h3 id=&quot;half-way-there&quot;&gt;Half Way There&lt;/h3&gt;
&lt;p&gt;With the new Rails 8 foundation in place, JuggleBee has already shed a lot of its old skin. We’ve modernized the models and controllers, swapped out a creaky asset pipeline for importmaps, retired Sidekiq, Redis, and cron in favor of SolidQueue, and traded a homegrown deployment script (solid as it was) for the convenience and power of Kamal 2. The app is lighter, faster to deploy, and much easier to maintain — but we’re not done yet.&lt;/p&gt;
&lt;p&gt;Getting the Rails 8 app running and deployable was only &lt;strong&gt;half the battle&lt;/strong&gt;. Now comes the rest of the journey:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Migrating data from a legacy PostgreSQL 9.6 database to a fresh PostgreSQL 17.5 instance.&lt;/li&gt;
&lt;li&gt;Replacing CarrierWave with ActiveStorage and migrating all user-uploaded files into a new S3 bucket.&lt;/li&gt;
&lt;li&gt;Locking down credentials with Rails encrypted secrets and Bitwarden CLI.&lt;/li&gt;
&lt;li&gt;Cleaning up leftover code smells, finalizing test coverage, and polishing the overall performance of the new stack.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s some of what Part 2 will cover — all the finishing touches, the unexpected hurdles, and the final steps that took JuggleBee from a Rails 4 relic to a lean, modern, and production-ready Rails 8 app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stay tuned for Part 2, where the migration story continues.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Ruby on Rails</category>
<guid isPermaLink="true">https://coderlegion.com/4894/jugglebees-great-leap-rebuilding-a-rails-4-app-in-rails-8-part-1</guid>
<pubDate>Thu, 21 Aug 2025 08:06:05 +0000</pubDate>
</item>
<item>
<title>Getting Started with Ruby &amp; Rails on Windows</title>
<link>https://coderlegion.com/4390/getting-started-with-ruby-rails-on-windows</link>
<description>&lt;p&gt;If you're on &lt;strong&gt;Windows&lt;/strong&gt;, you can still develop with &lt;strong&gt;Ruby&lt;/strong&gt; and &lt;strong&gt;Rails&lt;/strong&gt; . Let's walk through your options and the steps involved.&lt;/p&gt;
&lt;p&gt;There are &lt;strong&gt;two main options&lt;/strong&gt;:&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-option-a-use-wsl-recommended-&quot;&gt;️ Option A: Use &lt;strong&gt;WSL&lt;/strong&gt; (Recommended)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;WSL&lt;/strong&gt; stands for &lt;strong&gt;Windows Subsystem for Linux&lt;/strong&gt; — it lets you run a full Linux environment on Windows without dual-booting or using a virtual machine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers who want a smooth Rails experience and compatibility with Linux tools.&lt;/p&gt;
&lt;hr&gt;
&lt;h5 id=&quot;steps-to-set-up-rails-with-wsl&quot;&gt;Steps to Set Up Rails with WSL&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Install WSL&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wsl --install
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;This will install Ubuntu by default.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update and install dependencies inside Ubuntu terminal&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade
sudo apt install curl git build-essential libssl-dev libreadline-dev zlib1g-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Install Ruby (using a version manager like rbenv or rvm)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://github.com/rbenv/rbenv-installer/raw/main/bin/rbenv-installer | bash
echo 'export PATH=&quot;$HOME/.rbenv/bin:$PATH&quot;' &amp;gt;&amp;gt; ~/.bashrc
echo 'eval &quot;$(rbenv init -)&quot;' &amp;gt;&amp;gt; ~/.bashrc
source ~/.bashrc

rbenv install 3.3.0
rbenv global 3.3.0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Install Rails&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gem install rails
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify installations&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ruby -v
rails -v
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-option-b-use-rubyinstaller-for-windows-native-ruby-&quot;&gt;️ Option B: Use &lt;strong&gt;RubyInstaller for Windows&lt;/strong&gt; (Native Ruby)&lt;/h4&gt;
&lt;p&gt;If you don’t want to use WSL, you can install Ruby directly in Windows.&lt;/p&gt;
&lt;h5 id=&quot;steps-for-native-windows-setup&quot;&gt;Steps for Native Windows Setup&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Download Ruby Installer&lt;/strong&gt;&lt;br&gt;
Go to: &lt;a rel=&quot;nofollow&quot; href=&quot;https://rubyinstaller.org&quot;&gt;https://rubyinstaller.org&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Download a version like &lt;code&gt;Ruby+Devkit 3.3.X&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run the Installer&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make sure you check the box that says &lt;strong&gt;“Add Ruby executables to your PATH”&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose to run &lt;code&gt;ridk install&lt;/code&gt; after installation (this installs DevKit, MSYS2).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Install Rails&lt;/strong&gt;&lt;br&gt;
Open Command Prompt or PowerShell and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gem install rails
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check Versions&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ruby -v
rails -v
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt; Note: Some gems (e.g., &lt;code&gt;pg&lt;/code&gt; for PostgreSQL) can be tricky to install on native Windows due to compilation issues. That’s why &lt;strong&gt;WSL is highly recommended&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;database-setup-for-windows&quot;&gt;Database Setup for Windows&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQLite&lt;/strong&gt; is supported out of the box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;: Download from &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/download/windows/&quot;&gt;https://www.postgresql.org/download/windows/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;You’ll need to install the dev libraries if using native Ruby (which can be difficult).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;common-tools&quot;&gt;Common Tools&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool           &lt;/th&gt;&lt;th&gt;Windows Option&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Ruby           &lt;/td&gt;&lt;td&gt;RubyInstaller OR rbenv via WSL&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Rails          &lt;/td&gt;&lt;td&gt;&lt;code&gt;gem install rails&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Git            &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://git-scm.com/downloads&quot;&gt;Git for Windows&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Code Editor    &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://code.visualstudio.com&quot;&gt;VS Code&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Terminal (WSL) &lt;/td&gt;&lt;td&gt;Ubuntu app from Microsoft Store&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;summary-which-setup-is-best-&quot;&gt;Summary: Which Setup is Best?&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Setup             &lt;/th&gt;&lt;th&gt;Pros                                    &lt;/th&gt;&lt;th&gt;Cons&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;WSL&lt;/strong&gt;           &lt;/td&gt;&lt;td&gt;Most compatible, Linux-like environment &lt;/td&gt;&lt;td&gt;Slightly more setup effort&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;RubyInstaller&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Simple to install, native Windows       &lt;/td&gt;&lt;td&gt;Compatibility issues with some gems/tools&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;&lt;p&gt; &lt;strong&gt;Recommendation&lt;/strong&gt;: Use &lt;strong&gt;WSL + Ubuntu&lt;/strong&gt; for the best Rails development experience on Windows.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
</description>
<category>Backend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/4390/getting-started-with-ruby-rails-on-windows</guid>
<pubDate>Sat, 02 Aug 2025 05:51:11 +0000</pubDate>
</item>
<item>
<title>Beyond rbenv and pyenv: Achieving Reproducible Dev Setups with Nix</title>
<link>https://coderlegion.com/4184/beyond-rbenv-and-pyenv-achieving-reproducible-dev-setups-with-nix</link>
<description>&lt;p&gt;Setting up a development environment is a painful process, as I &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/from-dotfile-hacks-to-open-source-my-development-environment-evolution-a771ea7bf8a8&quot;&gt;discussed previously&lt;/a&gt; when detailing how mine evolved to the combination of home-manager and Ubuntu Make. Little did I know, the article gained significant traction and likely resonated with many fellow developers. Coincidentally, I got into a new job earlier in the month, and this necessitated a revision to the setup on a MacOS environment. How did it go? Turbulent would be an appropriate adjective to describe the whole experience, as you may expect.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/from-dotfile-hacks-to-open-source-my-development-environment-evolution-1hce&quot;&gt;From Dotfile Hacks to Open Source: My Development Environment Evolution&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*t17Pg0TlocDfGtPxS8To3A.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;A cute illustration from Copilot&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-macos-catalyst-a-new-job-a-new-challenge&quot;&gt;The macOS Catalyst: A New Job, A New Challenge&lt;/h4&gt;
&lt;p&gt;Replicating a development setup over multiple machines can be a hard problem, and ensuring it works on different operating systems significantly increases that complexity. As we delve deeper into the topic, you will see how I shoot myself in the foot repeatedly, and the corresponding steps to recover.&lt;/p&gt;
&lt;p&gt;Why a Mac?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*9L-vGU5dIoVaFnew&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Claudio Schwarz on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Before starting my new role, I was asked if I would prefer a Linux machine or a Mac for work. Though I am relatively comfortable with a Linux machine, I picked Mac as I reckoned it is easier to get enterprise apps whenever necessary. I also reiterated this same reason when my coworker and I were pairing up for a task.&lt;/p&gt;
&lt;p&gt;Despite working mostly with Ubuntu, I do have a Intel iMac for video calls and functioning as a second screen for documentation and web project previews. A minimal home-manager setup worked mostly fine, and I assumed expansion of the setup for development would be as painless.&lt;/p&gt;
&lt;p&gt;Oh, how wrong I was.&lt;/p&gt;
&lt;p&gt;As the assigned MacBook Pro became my primary device for work, all the minor problems surfaced. First, my shells were not initialized properly in the terminal emulator. After getting that fixed, I got adventurous and decided to retire rbenv to manage Ruby versions. Coupled with the struggle to set up my code editor, I was amazed how my feet survived all the gunshots.&lt;/p&gt;
&lt;h4 id=&quot;diving-into-the-rabbit-holes-macos-and-interpreter-woes&quot;&gt;Diving into the Rabbit Holes: macOS and Interpreter Woes&lt;/h4&gt;
&lt;p&gt;How hard can it be?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*i3phby67nWZBNKAX&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Nachristos on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If one is looking for a statement to best represent underestimation, that would be it. Looking back, the problems look very trivial, but each was a great learning opportunity. Hang on tight, as we are going into a deep dive into all the rabbit holes I fell into with the revised setup, and to learn how to overcome them.&lt;/p&gt;
&lt;p&gt;Compared to Windows, macOS offers a more familiar environment for developers with experience in Linux. We can interact with the system through a similar command-line interface. With a package manager, we can even add different shells not shipped with the operating system, like my favourite &lt;a rel=&quot;nofollow&quot; href=&quot;https://fishshell.com/&quot;&gt;fish shell&lt;/a&gt;. Trying to configure an iTerm2 profile for fish is exactly the first hole I fell into.&lt;/p&gt;
&lt;p&gt;Why would macOS not source .profile in a graphical session?&lt;/p&gt;
&lt;p&gt;Unlike Ubuntu, macOS &lt;a rel=&quot;nofollow&quot; href=&quot;https://ghostty.org/docs/help/macos-login-shells&quot;&gt;does not source &lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ghostty.org/docs/help/macos-login-shells&quot;&gt;.profile in a graphical session&lt;/a&gt;. Most of the time, the script updates the PATH variable, making locally installed applications discoverable. Subsequently, shell initialization scripts (like .bashrc for bash and config.fish for fish) then set up individual application based on the environment defined in .profile.&lt;/p&gt;
&lt;p&gt;Multiple variations was attempted, until I stumbled upon &lt;a rel=&quot;nofollow&quot; href=&quot;https://superuser.com/questions/446925/re-use-profile-for-fish&quot;&gt;a Q&amp;amp;A on superuser&lt;/a&gt;. Thinking it would be useful, I sent the proposed answer to Gemini and asked if it would be applicable for my case. Turns out I only needed a little tweak.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/bin/sh -lc &quot;exec -l /path/to/fish&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a breakdown of the command:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;/bin/sh: This specifies that the command should be executed by the Bourne shell, launching a new instance of this basic shell.&lt;/li&gt;
&lt;li&gt;/bin/sh -l: this tells the shell to launch as a login shell, ensuring it sources standard login files like .profile.&lt;/li&gt;
&lt;li&gt;/bin/sh -c: This tells the shell to execute the command provided in the string that follows.&lt;/li&gt;
&lt;li&gt;exec: This &lt;a rel=&quot;nofollow&quot; href=&quot;https://devconnected.com/understanding-processes-on-linux/#Execute_operation&quot;&gt;crucial command&lt;/a&gt; tells the shell to replace the current /bin/sh process with a new process (in this specific case, the fish shell).&lt;/li&gt;
&lt;li&gt;exec -l: This tells the new shell (e.g., fish) to launch as a login shell itself.&lt;/li&gt;
&lt;li&gt;/path/to/fish: This is the full path to your fish shell executable. With the -l flag in the exec command, this fish shell will then source its own login initialization configuration file (e.g., config.fish).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Similarly, the command can be adapted to work with byobu, my preferred terminal multiplexer. The -l flag is omitted for the exec command as byobu is not a shell.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/bin/sh -lc &quot;exec /path/to/byobu new-session&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For completeness’ sake, the setup for a bash profile is a lot simpler, as it sources .profile and .bashrc properly when launched as a login shell -l itself.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/path/to/bash -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What a ride.&lt;/p&gt;
&lt;p&gt;Shifting from Python to Ruby as my main development language presented an opportunity to review the toolings involved. Previously I relied on pyenv to provide isolated Python interpreters for individual projects. Eventually, the functionality was replaced by uv, which also manages pre-built CPython distributions as well. Unfortunately, I wasn’t aware of an alternative for Ruby, and had to continue relying on rbenv for the purpose.&lt;/p&gt;
&lt;p&gt;One of the main goals of using home-manager, as discussed in the previous article, was to achieve idempotency and to isolate dev packages from the system package manager (such as apt in Ubuntu). Thus, we need to first define the packages in the home-manager flake file, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;packages = with pkgs; [
    ...
    # dev libraries
    openssl
    zlib-ng
    bzip2
    readline
    sqlite
    ncurses
    xz
    tcl
    tk
    xml2
    xmlsec
    libffi
    libtool
    libyaml
    libxml2
    libxslt
  ];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Essentially, this yields an isolated filesystem namespace that is unrecognized by the operating system. In order to get the compiler to utilize the pulled development package, we need to tell &lt;code&gt;rbenv&lt;/code&gt; where to look for them, as shown in the shell script below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env sh

export CPPFLAGS=&quot;-I$HOME/.nix-profile/include $CPPFLAGS&quot;
export CFLAGS=&quot;-I$HOME/.nix-profile/include $CFLAGS&quot;
export LDFLAGS=&quot;-L$HOME/.nix-profile/lib $LDFLAGS&quot;
export PKG_CONFIG_PATH=$HOME/.nix-profile/lib/pkgconfig:$HOME/.nix-profile/share/pkgconfig:$PKG_CONFIG_PATH
export C_INCLUDE_PATH=$HOME/.nix-profile/include:$C_INCLUDE_PATH
export INCLUDE_PATH=$HOME/.nix-profile/include:$INCLUDE_PATH
export LIBRARY_PATH=$HOME/.nix-profile/lib:$LIBRARY_PATH
export LD_LIBRARY_PATH=$HOME/.nix-profile/lib:$LD_LIBRARY_PATH

$HOME/.nix-profile/bin/rbenv install -vf 3.3.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It worked fine, though I always found this a dirty hack, as it required explicitly defining the flags and include paths. There has to be a better way.&lt;/p&gt;
&lt;h4 id=&quot;the-nix-pivot-discovering-a-declarative-path&quot;&gt;The Nix Pivot: Discovering a Declarative Path&lt;/h4&gt;
&lt;p&gt;If not rbenv, then how do we approach Ruby version management? Turns out we can use another feature provided by Nix to build a development environment through a declarative manner. So how is this relevant to the problem we are looking at though?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*uqrvCw46GE479P0z&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Jonathon B. Carreño on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A friend of mine had been hinting for a while that I could completely replace rbenv with Nix but I didn’t explore deeper. That changed when I saw the announcement of the gemini-cli app. In the installation guide, it called for npx, but the tool was not available as a Nix package. After some exchanges with Gemini, it suggested I look into nix shell, and the conversation quickly evolved into an introductory session on nix develop.&lt;/p&gt;
&lt;p&gt;Just a side note, gemini-cli was already packaged not long after the announcement, no need for npx. If I were to follow the installation guide, npx could be made available through a nix shell -p nodejs session. In case you missed how this could be related to our original problem, the equivalent for Ruby would be nix shell -p ruby.&lt;/p&gt;
&lt;p&gt;What if I need to install gems that require some special dev libraries?&lt;/p&gt;
&lt;p&gt;You just unlocked the reason the conversation turned into an introduction to nix develop. Like home-manager, the input for the command is a flake file, written in the Nix programming language, declaring the development environment setup. Naturally, this added complexity made me hesitant to commit to this strategy. I was still trying to get more comfortable with home-manager and wanted to take more conservative baby steps in adopting additional features offered by Nix.&lt;/p&gt;
&lt;p&gt;Essentially, there are two important components to be declared in the flake: the list of packages and an initialization shell script. For example, we can define a development environment sporting Ruby 3.3 with the list of packages like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;buildInputs = with pkgs; [
  # fish is kept so that fish-specific completions from
  # packages can be made available.
  fish
  # Git is needed for fetching gems from git repositorie
  git
  # The Ruby interpreter (3.3.8 as the time of writing)
  ruby
  # Bundler for managing Ruby gems.
  bundler
  # Common dependencies for building native extensions.
  cmake
  gcc
  gnumake
  # Libraries often required by gems.
  openssl
  pkg-config
  libmysqlclient
  shared-mime-info
  re2
  readline
  zlib
] ++ lib.optionals stdenv.isDarwin [
  # Add macOS-specific dependencies.
  libiconv
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Secondly, the initialization script definition&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shellHook = ''
  # Ruby-specific setup
  export FREEDESKTOP_MIME_TYPES_PATH=&quot;${pkgs.shared-mime-info}/share/mime/packages/freedesktop.org.xml&quot;

  echo &quot;Ruby dev environment activated for ${system}!&quot;
'';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Incorporating these into the flake file makes nix develop a superior method to set up a development environment. Compared to just listing packages in nix shell, the shellHook component allows more control and further manipulation, enabling us to set up proper environment variables and run initialization commands. Additionally, the declarative nature of a flake file also means it is highly reproducible and can be easily shared.&lt;/p&gt;
&lt;h4 id=&quot;a-harmonious-setup-validation-and-reflection&quot;&gt;A Harmonious Setup: Validation and Reflection&lt;/h4&gt;
&lt;p&gt;Remember I needed to redefine library path and compiler flags to compile Ruby interpreters? That problem eventually came back while I was installing gems for work projects. Long story short, I gave in and adopted nix develop. Does it work out of the box? Does the adoption of nix develop stop me from shooting myself in the foot?&lt;/p&gt;
&lt;p&gt;In order to be cautious, I replicated and adapted the flake for my bigmeow side project, which is still managed by Poetry, and hence relied on pyenv. Fortunately, a satisfactory outcome was achieved after a few iterations. On the other hand, direnv also offers some integration with nix develop. Automated activation of a development environment can be achieved by adding one line to the respective .envrc:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use flake /path/to/folder # the folder that contains the flake.nix declaration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*axJYZtjNEhYB998Q&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Sigmund on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Applying the Ruby counterpart also went rather smoothly. There were some problems in the earlier iterations, but they were mostly trivial, and all it took was a regeneration by Gemini. Compared to the relatively smooth sailing in environment setup, taming my code editor to work with it was a lot more frustrating.&lt;/p&gt;
&lt;p&gt;In recent years, most extensions bridging the code and development tools would often bundle the tool together. For instance, if I install an extension to format Python code with ruff formatter in Visual Studio Code, it would bundle ruff itself in the package. This saves the developer from needing to declare ruff as a dependency of the project.&lt;/p&gt;
&lt;p&gt;Likely due to the highly modular design, the experience was a lot more complicated in the Ruby world. One of my work projects defined an ancient version of the Rubocop formatter, and none of the editor extensions offered in the marketplace would work with it. Forcing the extension to use the bundled gem would fail, because it wouldn’t work with the equally outdated rubocop extension gems (rubocop-rails, rubocop-minitest) installed in the project.&lt;/p&gt;
&lt;p&gt;Updating the lock file is not a practical solution and I was stuck there for a while. Thankfully a coworker shared that I could explore the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Shopify/ruby-lsp&quot;&gt;ruby-lsp extension&lt;/a&gt;. Apparently, this extension makes it possible to refer to an alternative Gemfile, i.e. I could have a Gemfile for all the development tools saved outside of the project. In this particular case, I could have a Gemfile like this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source &quot;https://rubygems.org&quot;

gem &quot;ruby-lsp&quot;
gem &quot;rubocop&quot;, &quot;1.26.1&quot;
gem &quot;rubocop-rails&quot;
gem &quot;rubocop-minitest&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Solving that yields an functional setup that I was happy enough for work. Looking back, everything seemed rather trivial, but in reality I was stuck for a couple of days. Despite the added complexity, getting all these parts working together felt like putting together puzzle pieces. Getting them configured correctly is giving the same satisfaction as solving a complex jigsaw puzzle.&lt;/p&gt;
&lt;p&gt;Is this increased complexity worth the effort? Would there be a better alternative strategy out there? I am curious to know about them and am open to recommendations and corrections. Meanwhile, I do find the revision to retire version managers a fruitful learning experience. Another part I liked about the current implementation is the elimination of explicit declaration of dev packages to build the interpreters. If you ask me, I would say replacing the version managers and removing dev packages for interpreter compilation with nix develop flake files is totally worth the effort.&lt;/p&gt;
&lt;p&gt;Throughout this journey, Gemini served as a valuable editorial companion, helping refine my prose. However, the voice and code remain entirely my own. I welcome your feedback and recommendations in the comments below, and invite you to subscribe to my &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/&quot;&gt;Medium&lt;/a&gt; for more content on my development adventures!&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4184/beyond-rbenv-and-pyenv-achieving-reproducible-dev-setups-with-nix</guid>
<pubDate>Sun, 20 Jul 2025 18:59:08 +0000</pubDate>
</item>
<item>
<title>Rails releases security updates for ReDoS vulnerabilities—upgrade now to the latest versions for optimal protection!</title>
<link>https://coderlegion.com/438/releases-security-updates-vulnerabilities-upgrade-latest-versions-optimal-protection</link>
<description>&lt;h2&gt;Critical Rails Security Updates: Patch Your Apps Now!&lt;/h2&gt;
&lt;p&gt;Attention all Ruby on Rails developers! The Rails team has released &lt;strong&gt;Rails versions 6.1.7.9, 7.0.8.5, 7.1.4.1, and 7.2.1.1&lt;/strong&gt;, which address several critical security vulnerabilities, most notably ReDoS (Regular Expression Denial of Service) risks. If your application is running on &lt;strong&gt;Ruby 3.1&lt;/strong&gt; or earlier, it’s crucial to take action now to keep your app secure and performing optimally.&lt;/p&gt;
&lt;h3&gt;Key Security Vulnerabilities Addressed:&lt;/h3&gt;
&lt;h4&gt;1. CVE-2024-47887: ReDoS Vulnerability in HTTP Token Authentication&lt;/h4&gt;
&lt;p&gt;This vulnerability affects HTTP token authentication in &lt;strong&gt;Action Controller&lt;/strong&gt;. It allows malicious users to perform Denial of Service (DoS) attacks by exploiting regular expressions used in token validation.&lt;/p&gt;
&lt;h4&gt;2. CVE-2024-41128: ReDoS Vulnerability in Query Parameter Filtering&lt;/h4&gt;
&lt;p&gt;Action Dispatch is vulnerable due to a regular expression flaw in query parameter filtering. Attackers can send crafted requests, leading to performance degradation.&lt;/p&gt;
&lt;h4&gt;3. CVE-2024-47888: ReDoS Vulnerability in `plain_text_for_blockquote_node` in Action Text&lt;/h4&gt;
&lt;p&gt;A regular expression flaw in the &lt;strong&gt;Action Text&lt;/strong&gt; `plain_text_for_blockquote_node` method could allow attackers to slow down or crash an application.&lt;/p&gt;
&lt;h4&gt;4. CVE-2024-47889: ReDoS Vulnerability in `block_format` in Action Mailer&lt;/h4&gt;
&lt;p&gt;The `block_format` method in &lt;strong&gt;Action Mailer&lt;/strong&gt; has a regular expression vulnerability that can expose applications to ReDoS attacks under certain conditions.&lt;/p&gt;
&lt;h3&gt;Why You Should Upgrade Immediately&lt;/h3&gt;
&lt;h4&gt;1. Ruby 3.1 Nearing End of Life for Security Updates&lt;/h4&gt;
&lt;p&gt;If your application runs on &lt;strong&gt;Ruby 3.1 or below&lt;/strong&gt;, these versions will no longer receive security updates. To avoid the risk of new vulnerabilities and performance issues, it is essential to upgrade to &lt;strong&gt;Ruby 3.2&lt;/strong&gt; or later.&lt;/p&gt;
&lt;h4&gt;2. Avoid Performance Degradation and Downtime&lt;/h4&gt;
&lt;p&gt;Without these updates, your application could suffer from slowdowns or potential outages. ReDoS attacks can significantly slow down your app by exhausting server resources with malicious input patterns. Preventive updates keep your app running smoothly.&lt;/p&gt;
&lt;h4&gt;3. Security and Compliance&lt;/h4&gt;
&lt;p&gt;Staying updated ensures your app meets security standards and compliance requirements. Running outdated software increases the risk of security breaches, which can lead to significant legal and financial repercussions.&lt;/p&gt;
&lt;h3&gt;What This Means for Your Project&lt;/h3&gt;
&lt;h4&gt;1. Older Ruby Versions Are Vulnerable&lt;/h4&gt;
&lt;p&gt;If you're still using &lt;strong&gt;Ruby 3.1 or below&lt;/strong&gt;, it’s highly recommended to upgrade to &lt;strong&gt;Ruby 3.2&lt;/strong&gt; or later. These newer versions offer improved security, performance, and long-term stability.&lt;/p&gt;
&lt;h4&gt;2. Extended Support for Rails 6.1&lt;/h4&gt;
&lt;p&gt;For projects running &lt;strong&gt;Rails 6.1&lt;/strong&gt;, even though it’s officially past its maintenance window, version &lt;strong&gt;6.1.7.9&lt;/strong&gt; is part of an extended support release. This gives you a grace period to safely migrate your app to newer versions of Rails.&lt;/p&gt;
&lt;h3&gt;Code Example: Implementing Secure HTTP Token Authentication&lt;/h3&gt;
&lt;p&gt;Here’s a snippet demonstrating a more secure way to handle HTTP Token Authentication in &lt;strong&gt;Action Controller&lt;/strong&gt; after upgrading Rails:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class ApplicationController &amp;lt; ActionController::Base
  before_action :authenticate

  def authenticate
    authenticate_or_request_with_http_token do |token, options|
      # Ensure the token follows a safe and secure pattern to avoid ReDoS risks
      return false unless token.match(/\A[A-Za-z0-9\-_]+\z/)
      User.find_by(token: token)
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Explanation:&lt;/strong&gt; In this example, we use a safer regular expression to validate the token, ensuring that it only contains alphanumeric characters, dashes, and underscores. This minimizes the risk of malicious patterns being used in an attack.&lt;/p&gt;
&lt;h3&gt;Steps to Upgrade Your Rails Application&lt;/h3&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Update Rails and Ruby Versions&lt;/strong&gt;&lt;br&gt;
  Update your &lt;code&gt;Gemfile&lt;/code&gt; to specify the latest versions of Rails and Ruby:
  &lt;pre&gt;&lt;code&gt;gem 'rails', '7.2.1.1'&lt;/code&gt;&lt;/pre&gt;
  Then run:
  &lt;pre&gt;&lt;code&gt;bundle update rails&lt;/code&gt;&lt;/pre&gt;
  &lt;/li&gt;

  &lt;li&gt;&lt;strong&gt;Test for Compatibility&lt;/strong&gt;&lt;br&gt;
  Run your test suite to ensure your application works correctly with the updated versions. You can use:
  &lt;pre&gt;&lt;code&gt;bundle exec rspec&lt;/code&gt;&lt;/pre&gt;
  Or:
  &lt;pre&gt;&lt;code&gt;bundle exec rails test&lt;/code&gt;&lt;/pre&gt;
  &lt;/li&gt;

  &lt;li&gt;&lt;strong&gt;Check for Deprecations&lt;/strong&gt;&lt;br&gt;
  Review deprecation warnings to anticipate any breaking changes in future versions.
  &lt;/li&gt;

  &lt;li&gt;&lt;strong&gt;Deploy&lt;/strong&gt;&lt;br&gt;
  Once the tests pass, deploy the updated app to your staging environment first, monitor for issues, and then deploy to production.
  &lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Upgrading to the latest Rails and Ruby versions is essential for keeping your application secure and stable. With critical vulnerabilities like &lt;strong&gt;ReDoS&lt;/strong&gt; affecting core components such as &lt;strong&gt;Action Controller&lt;/strong&gt;, &lt;strong&gt;Action Dispatch&lt;/strong&gt;, &lt;strong&gt;Action Text&lt;/strong&gt;, and &lt;strong&gt;Action Mailer&lt;/strong&gt;, failing to update can expose your app to attacks that compromise performance and security.&lt;/p&gt;
&lt;p&gt;If your app is still running on &lt;strong&gt;Ruby 3.1 or earlier&lt;/strong&gt;, migrating to &lt;strong&gt;Ruby 3.2&lt;/strong&gt; or later is highly recommended. The latest versions bring improvements in both performance and security, giving you peace of mind as you continue to develop and scale your application.&lt;/p&gt;
&lt;p&gt;If you're not sure how to proceed with the upgrade or need help navigating the transition, feel free to &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rubyonrails.org/community&quot; target=&quot;_blank&quot;&gt;contact us&lt;/a&gt;. We’re here to assist you in keeping your app secure and high-performing.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://rubyonrails.org/security&quot; target=&quot;_blank&quot;&gt;Ruby on Rails Official Security Advisories&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.ruby-lang.org/en/news/&quot; target=&quot;_blank&quot;&gt;Ruby Language Security Updates&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>HTML</category>
<guid isPermaLink="true">https://coderlegion.com/438/releases-security-updates-vulnerabilities-upgrade-latest-versions-optimal-protection</guid>
<pubDate>Tue, 15 Oct 2024 09:14:00 +0000</pubDate>
</item>
</channel>
</rss>