<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged rails</title>
<link>https://coderlegion.com/tag/rails</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>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>Hotwire vs. React: A Guide for the &quot;One-Person Framework&quot;</title>
<link>https://coderlegion.com/13050/hotwire-vs-react-a-guide-for-the-one-person-framework</link>
<description>&lt;h3 id=&quot;the-default-choice&quot;&gt;The Default Choice&lt;/h3&gt;
&lt;p&gt;For the last five years, the &quot;Standard Web Stack&quot; has been undeniable:&lt;br&gt;
&lt;strong&gt;Backend API (Rails/Node) + SPA Frontend (React/Vue).&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It is the architecture taught in bootcamps. It is the architecture demanded by recruiters. And for a long time, if you wanted a &quot;modern&quot; feeling application—no page reloads, snappy transitions, real-time updates—it was the only way.&lt;/p&gt;
&lt;p&gt;But it comes with a cost. I call it &lt;strong&gt;The React Tax.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With the maturity of Rails 7 (and now Rails 8) and &lt;strong&gt;Hotwire&lt;/strong&gt;, that tax is no longer mandatory. Here is why I believe Hotwire is the superior choice for 95% of Rails projects.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-two-apps-one-product&quot;&gt;The Problem: Two Apps, One Product&lt;/h3&gt;
&lt;p&gt;When you choose React, you aren't building &quot;an app.&quot; You are building &lt;strong&gt;two apps.&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Backend:&lt;/strong&gt; Dealing with serialization, JWTs, CORS, and versioning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Frontend:&lt;/strong&gt; Dealing with route management, global state (Redux/Context), effect hooks, and package.json hell.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Suddenly, a simple feature like &quot;Rename a Project&quot; requires:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frontend: Update UI state optimistically.&lt;/li&gt;
&lt;li&gt;Frontend: Dispatch API call.&lt;/li&gt;
&lt;li&gt;Backend: Handle auth.&lt;/li&gt;
&lt;li&gt;Backend: Update DB.&lt;/li&gt;
&lt;li&gt;Backend: Serialize JSON response.&lt;/li&gt;
&lt;li&gt;Frontend: Handle success/error.&lt;/li&gt;
&lt;li&gt;Frontend: Re-sync local state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;With Hotwire, the flow is:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller: Update DB.&lt;/li&gt;
&lt;li&gt;View: Render HTML.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;1-the-single-source-of-truth-myth&quot;&gt;1. The &quot;Single Source of Truth&quot; Myth&lt;/h3&gt;
&lt;p&gt;The hardest problem in Computer Science is naming things. The second hardest is &lt;strong&gt;Cache Invalidation.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When you duplicate your database state into a Redux store or React Query cache, you are inviting bugs. &lt;em&gt;Is the user list stale? Did the socket update the count? Why does the UI say &quot;Premium&quot; but the API returned 403?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Hotwire rejects this premise.&lt;br&gt;
With &lt;strong&gt;Turbo&lt;/strong&gt;, the server is the single source of truth. When data changes, you don't update a client-side JSON cache; you simply replace the HTML chunk on the screen with fresh HTML from the server.&lt;/p&gt;
&lt;p&gt;You eliminate an entire class of &quot;State desync&quot; bugs because &lt;strong&gt;you have no client-side state.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-velocity-the-one-person-framework-&quot;&gt;2. Velocity: The &quot;One Person Framework&quot;&lt;/h3&gt;
&lt;p&gt;Rails 8 is pushing the concept of the &quot;One Person Framework.&quot; The idea is that a single developer should be able to build a startup that used to require a team of 5.&lt;/p&gt;
&lt;p&gt;If you are a solo dev or a small team using React, you are spending 50% of your time managing the &lt;strong&gt;glue&lt;/strong&gt; between your backend and frontend.&lt;/p&gt;
&lt;p&gt;With Hotwire (Turbo Drive + Turbo Frames + Stimulus), you stay in Ruby/ERB land for 90% of the work. You write standard Rails views. You get SPA-speed navigation for free via Turbo Drive. You get dynamic partial updates via Frames.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Context Switch Cost:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;React:&lt;/strong&gt; Switch from Ruby to JS. Switch from ERB to JSX. Switch from RSpec to Jest.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hotwire:&lt;/strong&gt; Stay in the flow.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-but-react-feels-better-&quot;&gt;3. &quot;But React feels better&quot;&lt;/h3&gt;
&lt;p&gt;This was true in 2016. It is not true today.&lt;/p&gt;
&lt;p&gt;Turbo Drive (which intercepts link clicks and form submissions) makes a standard Rails app feel instant. It merges the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and replaces the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; without a full browser reload.&lt;/p&gt;
&lt;p&gt;Need real-time? &lt;strong&gt;Turbo Streams&lt;/strong&gt; over ActionCable allows you to broadcast updates to millions of users with a few lines of Ruby.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/message.rb
after_create_commit { broadcast_append_to &quot;chat_room&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That one line creates a WebSocket connection that appends the new message to the DOM of every active user. Doing this in React requires setting up a socket context, listeners, state reducers, and safety checks.&lt;/p&gt;
&lt;h3 id=&quot;when-should-you-use-react-&quot;&gt;When SHOULD you use React?&lt;/h3&gt;
&lt;p&gt;I am not a React hater. It is an incredible piece of engineering. You &lt;em&gt;should&lt;/em&gt; use React (or Vue/Svelte) if:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;You are building a high-fidelity tool:&lt;/strong&gt; e.g., Figma, an in-browser video editor, or a complex drag-and-drop Trello clone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You need &quot;Offline First&quot;:&lt;/strong&gt; If the app must function 100% without an internet connection (PWA with heavy local sync).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your API serves multiple clients:&lt;/strong&gt; If you have a Mobile App, a Web App, and a Desktop App that all need the exact same API, maybe a decoupled frontend makes sense (though native apps are moving to Server-Driven UI too!).&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;the-verdict&quot;&gt;The Verdict&lt;/h3&gt;
&lt;p&gt;For the vast majority of SaaS products—dashboards, marketplaces, social networks, project management tools—the &quot;React Tax&quot; is a waste of resources.&lt;/p&gt;
&lt;p&gt;Hotwire gives you 80% of React's interactivity with 10% of the complexity.&lt;/p&gt;
&lt;p&gt;If you want to ship features, use Hotwire. If you want to configure Webpack, use React.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Are you sticking with the React ecosystem, or have you tried the Hotwire life? Let’s discuss below.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13050/hotwire-vs-react-a-guide-for-the-one-person-framework</guid>
<pubDate>Tue, 17 Mar 2026 12:47:49 +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>
</channel>
</rss>