
From Fork to Framework, Part 3: Why Rails Almost Won
Rails 8 is the framework we chose in February 2026 when we rebuilt both AODex and AOSentry from the ground up, and the appeal was immediate. After two earlier iterations — one stitched together from open-source forks, another built on Python and Svelte during our second rewrite — we knew exactly what we needed next: fewer dependencies, stronger conventions, and a framework that made the common patterns disappear into the background. Rails 8 delivered on every count. For four weeks, it genuinely felt like we had found the answer.
This is the complete story of that Rails 8 build: nearly 300 commits, a shared Rails engine, a genuinely excellent security platform, and the architectural ceiling we eventually hit. Along the way, we'll also cover what's actually new in Rails 8, how it compares to Rails 7, how to approach an upgrade, and where the framework's own documentation leaves questions unanswered.
Why We Picked Rails 8 Over Everything Else
Before committing to Rails 8, we evaluated what each previous architecture had cost us. Our fork-based stack inherited whatever dependencies the upstream projects demanded, and our Python and Svelte stack required PostgreSQL, Redis, Celery workers, and Socket.io just to run locally. In contrast, Rails 8, running on Ruby 3.4, promised a single-database architecture with conventions mature enough that a small team could move fast without reinventing authentication, background jobs, or admin tooling from scratch.
Specifically, three things mattered most. First, Rails 8 ships with sensible defaults instead of a blank slate. Second, its ecosystem — Devise for authentication, Pundit for authorization, ActiveRecord for the database layer — is battle-tested and extensively documented. Third, and most importantly for infrastructure cost, Rails 8 introduced a way to eliminate Redis entirely. That last point deserves its own section, because it changed our entire deployment story.
What's Actually New in Rails 8
Most upgrade guides list Rails 8's changes as a bullet-point changelog. We'd rather explain what each one means in practice, because we lived with every one of them in production.
The Solid Trifecta: Solid Queue, Solid Cache, Solid Cable
Rails 8.1.2 introduced what the Rails team calls the Solid trifecta: Solid Queue for background jobs, Solid Cache for application caching, and Solid Cable for WebSocket pub/sub (the mechanism Action Cable uses to push real-time updates to connected browsers). All three back directly onto PostgreSQL instead of Redis. No separate cache cluster. No Sidekiq. No message broker to patch and monitor.
This was a significant architectural simplification. One database. One connection pool. One thing to back up, monitor, and scale. For the full technical rationale, the official Rails Guides walk through each adapter in detail, though they don't tell you what it actually feels like to run it under production load — we cover that later in this article.
Kamal 2 and Thruster: Rails 8's Deployment Story
Rails 8 also bundles Kamal 2 by default, a deployment tool that packages your app as a Docker container and ships it to any server with SSH access, no Kubernetes cluster required. Alongside it sits Thruster, a lightweight HTTP proxy that adds automatic HTTPS, asset compression, and X-Sendfile caching in front of Puma, the application server Rails uses by default. Together, these two tools mean a solo developer can deploy a production-grade Rails 8 application to a single DigitalOcean droplet without touching a YAML-heavy orchestration platform. We'll return to the deployment tradeoffs we actually experienced further down.
The Built-In Authentication Generator
For the first time, Rails 8 ships a native authentication generator — rails generate authentication — that scaffolds sessions, password resets, and secure cookie handling without installing a gem. We still chose Devise for our build because we needed JWKS-backed JWT validation and SAML support for enterprise single sign-on, but for simpler applications, the built-in generator genuinely removes a dependency that used to be mandatory.
Propshaft and SQLite in Production
Rails 8 also finalizes the switch from Sprockets to Propshaft, a far simpler asset pipeline that skips bundling and transpilation in favor of serving fingerprinted files directly. In addition, Rails 8 officially supports SQLite in production for the first time, backed by improvements to its concurrent write handling. We didn't use SQLite ourselves — our multi-tenant workload needed PostgreSQL's row-level security — but for smaller applications, it removes an entire database server from the stack.
The Zero-Redis Stack in Practice
The operational benefits of the Solid trifecta showed up immediately. Deployment configurations shrank. Local development went from a multi-service Docker Compose file to a single rails server command. Because Solid Cache and PostgreSQL were now the same system, the cognitive overhead of reasoning about cache invalidation across two separate stores simply disappeared.
For a small team building fast, this mattered more than any synthetic benchmark. Fewer moving parts meant fewer 3am pages. As a result, we spent our on-call hours building features instead of debugging a Redis eviction policy.
Eden App Platform: Shared Rails 8 Infrastructure in Two Commits
On February 22, we created eden-app-platform, a shared Rails engine that provided the foundation for both AODex and AOSentry. In just two commits, we had multi-tenancy with tenant isolation enforced at the database query level, role-based access control with hierarchical permissions, immutable audit logging, and Stripe subscription synchronization.
This was possible because of how Rails engines work: a Rails engine is a miniature Rails application that mounts inside a host application, sharing models, controllers, and views while keeping its own namespace. Eden App Platform shipped as a gem. Both products declared it as a dependency, ran the install generator, and instantly inherited a complete multi-tenant SaaS foundation.
We also built Eden UI, a Rails gem providing Stimulus and Tailwind-based components (Stimulus being the lightweight JavaScript framework that ships with Rails by default). The design language drew from Flowbite-Svelte concepts used in the earlier Python iteration, but we rebuilt it as server-rendered partials with progressive enhancement. There was no JavaScript build pipeline and no node_modules folder. Components rendered on the server and were enhanced in the browser.
AODex: Building on Rails 8 From Day One
AODex started life as aocodex-rails with 9 commits across three days, February 14 through 17. It began as a straightforward port of the Python AODex feature set into Rails conventions: ActiveRecord models, Devise authentication, Pundit authorization, and Hotwire for real-time updates.
After that initial sprint, we renamed it to aodex and kept building, eventually accumulating 166 commits. Hotwire and Turbo Frames gave us instant admin navigation without writing custom JavaScript. Form submissions, table filters, and modal dialogs all happened over the wire, with the server rendering HTML fragments and Turbo swapping them into the page. The developer experience was remarkably fast — changes to a view appeared on the next request with no compilation step, no hot-reload lag, and no client-side state to debug.

AOSentry: Rails 8's Deepest Test
AOSentry is where Rails 8 truly demonstrated its strengths. Over 120 commits, we built a comprehensive security and compliance platform that would have taken significantly longer in almost any other framework.
ActiveRecord encryption with per-tenant PII keys gave us field-level encryption with key rotation built in. Devise handled authentication, but we extended it with JWKS-backed JWT validation for API consumers and SAML crypto verification for enterprise SSO. Meanwhile, Pundit policies enforced authorization at every single controller action.
The compliance features came together quickly, too. GDPR (the EU's General Data Protection Regulation) data export generated complete tenant data packages on demand. Immutable audit logs used SHA-256 hash chains, where each entry's hash incorporated the previous entry's hash, making tampering mathematically detectable. Anomaly detection flagged unusual access patterns, and compliance reports aggregated audit data into formats enterprise customers could hand directly to their auditors.
We also built IP allowlisting, consent management with versioned consent records, and dual-approval change workflows, where sensitive configuration changes required sign-off from two authorized users before taking effect. If you want to see this in action, our documentation covers both how to configure a guardrail and how to investigate a guardrail decision after the fact. The eden_llm gem, which we also built during this phase, abstracted LLM provider interactions behind a unified interface, handling streaming responses, token counting, and provider-specific error mapping.
Every one of these features benefited from Rails conventions. Migrations kept the schema versioned. Callbacks kept the audit trail consistent. ActiveJob dispatched background work through Solid Queue without any additional configuration. In short, the framework never fought us on any of it.



The Rails 8 Architecture Behind AODex and AOSentry
Both applications backed onto a single PostgreSQL instance. Solid Queue, Solid Cache, and Solid Cable all stored their state in PostgreSQL tables. Redis was gone entirely — the only infrastructure dependency was the database itself, as the diagram below shows.
Rails 8 vs Rails 7: What Actually Changed for Us
Most "Rails 7 to Rails 8" comparisons focus on the changelog. We'd rather tell you what each change meant operationally, since that's what actually determines whether an upgrade is worth your team's time.
| Area | Rails 7 | Rails 8 |
|---|---|---|
| Background jobs | Sidekiq + Redis, or Active Job's default adapter | Solid Queue, backed by PostgreSQL, enabled by default |
| Caching | Redis or Memcached cache store | Solid Cache, backed by PostgreSQL, enabled by default |
| WebSockets | Action Cable with a Redis adapter | Solid Cable, backed by PostgreSQL polling |
| Deployment | Capistrano, Heroku, or a custom Docker pipeline | Kamal 2 and Thruster, bundled by default |
| Authentication | Devise or a hand-rolled solution | Native generator, or Devise for advanced needs |
| Asset pipeline | Sprockets or esbuild/webpack | Propshaft, simplified and default |
| Production database | SQLite discouraged in production | SQLite officially production-ready |
In our case, the jump from a Rails 7 mental model to Rails 8 was really a jump from "assemble your own infrastructure" to "accept the defaults and move faster." For a team our size, that trade was overwhelmingly positive.
How to Upgrade From Rails 7 to Rails 8
Although we built our Rails 8 applications from scratch, several teams around us upgraded existing Rails 7 codebases. Based on what we learned debugging their issues, here's the process we'd recommend.
- Audit your Ruby version first. Rails 8 requires a modern Ruby runtime, so check your
.ruby-versionfile and your CI configuration, and upgrade to Ruby 3.2 or higher before you touch the Gemfile at all. - Update the Gemfile and run bundle update rails. Bump the rails gem to the 8.x line, resolve any dependency conflicts Bundler flags, and re-lock your Gemfile.lock before moving to the next step.
- Run bin/rails app:update to merge configuration defaults. Review each generated diff carefully and accept new initializers selectively, rather than overwriting your existing configuration files blindly.
- Migrate jobs, caching, and WebSockets to the Solid trifecta. Replace Sidekiq, your Redis-backed cache store, and Action Cable's Redis adapter with Solid Queue, Solid Cache, and Solid Cable respectively, one subsystem at a time.
- Swap Sprockets for Propshaft. Update any direct asset references, recompile your assets in a staging environment, and confirm every stylesheet and script still loads before deploying further.
- Run your full test suite and load-test critical paths. Pay particular attention to long-lived connections or streaming endpoints, since those behave differently under Rails 8's new default server settings.
What Rails 8 Excelled At (and Where It Didn't Fit)
After months in production, the pattern became clear. Rails 8 is outstanding at web application problems and structurally mismatched with a small set of specific workloads. Here's the honest breakdown.
| What Rails 8 Excelled At | Where Rails 8 Did Not Fit |
|---|---|
| Convention-driven development: authentication, authorization, mailers, jobs, and admin interfaces all had established patterns | LLM streaming concurrency: Puma workers at roughly 150MB each cannot sustain thousands of simultaneous streaming connections |
| ActiveRecord encryption with per-tenant PII keys and built-in key rotation | Mobile targets: Rails produces HTML and cannot compile to iOS and Android without a separate frontend |
| Shared engines for multi-tenant SaaS foundations (eden-app-platform shipped in 2 commits) | Container image size: 600MB+ images meant slow deploys and high cold-start latency on DigitalOcean App Platform |
| Hotwire and Turbo Frames for zero-JavaScript interactive admin interfaces | Thread-per-request model: Ruby's GIL limits true parallelism to one thread per process |
| Solid Queue, Solid Cache, and Solid Cable: zero-Redis infrastructure | Type safety: runtime method resolution means entire classes of errors surface only in production |
| Rapid compliance feature development: GDPR export, audit logs, and consent management built in days | Memory per connection: each Puma worker reserves substantial memory whether or not it is actively serving a request |
| Mature ecosystem: Devise, Pundit, and ActiveRecord encryption are battle-tested and well-documented | Deployment density: 600MB containers on shared infrastructure limit how many instances fit on a single node |

The Concurrency Ceiling: Rails 8 and LLM Streaming
The core tension was straightforward. An LLM gateway proxies streaming responses from upstream providers to downstream clients, and each request holds an open connection for seconds to minutes while tokens stream through. Under load, we were effectively running a process per connection for streaming, which is extremely memory-hungry, with each worker consuming roughly 150 megabytes.
At a thousand concurrent streaming connections, that works out to 150 gigabytes of memory for the application layer alone. This is not a Rails bug — it's a direct consequence of the thread-per-request model that Ruby's runtime enforces. Puma can run multiple threads per worker, but the Global Interpreter Lock (GIL) means only one thread executes Ruby code at a time per process. For CPU-light, IO-heavy streaming workloads, that's the worst possible tradeoff: you pay the memory cost of many workers but still can't actually parallelize the work.
We confirmed this through load testing. At moderate concurrency, Rails 8 handled streaming responses without issue. However, as concurrent connections climbed, memory consumption grew linearly and response latency degraded. Similarly, the Solid Cable WebSocket layer, backed by PostgreSQL polling, added latency compared to dedicated WebSocket servers. None of this was surprising — it was the expected behavior of a framework designed for request-response web applications handling long-lived streaming connections.
Key takeaway: Rails 8 is not slow. It's simply optimized for a different shape of workload than a streaming LLM gateway. If your product serves short request-response cycles, this ceiling will likely never matter to you.
The Mobile Gap: Why Rails 8 Alone Wasn't Enough
The second limitation was mobile. Rails renders HTML. It can serve a JSON API that a mobile application consumes, but it cannot produce the mobile application itself. Our product roadmap included iOS and Android clients, and building those in Swift and Kotlin while maintaining a Rails web frontend and a Rails API would have meant three separate UI codebases, three sets of components, and three deployment pipelines.
We had already experienced this exact fragmentation during the fork phase, where the React frontend and Python backend lived as separate worlds connected only by a JSON contract. Rails 8 improved the web experience considerably with Hotwire, but it did not solve the cross-platform problem. Ultimately, we needed a single UI framework that could target web, iOS, Android, macOS, and Linux from one codebase — a decision that reshaped our entire development workflow going forward.
The Deployment Reality: Container Size and Cold Starts
Container images for our Rails 8 applications exceeded 600 megabytes. Ruby, Bundler, native gem extensions, asset precompilation, and the Rails framework itself all contributed to that figure. On DigitalOcean App Platform, this meant slow image pulls, slow scaling events, and meaningful cold-start latency whenever new instances launched.
Deployment was not broken — it worked, and Kamal 2 made the process itself painless. But every scaling event carried a time penalty that compounded under load, precisely when fast scaling mattered most. For a product that needed to handle bursty LLM traffic patterns, where a single enterprise customer could spike concurrent connections by an order of magnitude, slow scaling became a real operational constraint rather than a theoretical one.
Frequently Asked Questions About Rails 8
What is Rails 8?
Rails 8 is the current major version of Ruby on Rails, a web application framework for Ruby. Its headline change is the Solid trifecta — Solid Queue, Solid Cache, and Solid Cable — which lets a production application run background jobs, caching, and WebSockets entirely on PostgreSQL, without Redis.
Do you still need Redis with Rails 8?
No. The Solid trifecta eliminates the need for Redis in most applications by moving background jobs, caching, and WebSocket pub/sub onto PostgreSQL tables. We ran both AODex and AOSentry entirely Redis-free for the duration of the Rails 8 build.
Can Rails 8 handle LLM streaming at scale?
Not efficiently at high concurrency. Because Puma uses a thread-per-request model constrained by Ruby's Global Interpreter Lock, each streaming connection consumes a full worker's memory. At roughly 150MB per worker, a thousand concurrent streams require around 150GB of memory for the application layer alone.
How do you upgrade from Rails 7 to Rails 8?
Upgrade Ruby to 3.2 or higher first, then run bundle update rails, run bin/rails app:update to merge configuration defaults, migrate jobs and caching to the Solid trifecta, swap Sprockets for Propshaft, and finally run a full test suite with load testing on any streaming endpoints.
What is the Solid Trifecta in Rails 8?
It's the collective name for Solid Queue, Solid Cache, and Solid Cable — three Rails 8 libraries that replace Redis-dependent tools for background jobs, caching, and WebSocket pub/sub, respectively, all backed by PostgreSQL.
Does Rails 8 support SQLite in production?
Yes. Rails 8 officially supports SQLite in production, thanks to improvements in concurrent write handling. We chose PostgreSQL instead because our multi-tenant architecture needed row-level security, but smaller applications can now skip a separate database server entirely.
Is Rails 8 a good choice for mobile app backends?
Yes, as a JSON API backend. Rails 8 cannot generate the mobile application itself, since it renders HTML, so teams building iOS and Android clients typically pair a Rails 8 API with a separate cross-platform framework for the client layer.
The Lesson: When Rails 8 Is (and Isn't) the Right Choice
Rails 8 is a genuinely excellent framework for building web applications. The four weeks we spent with it were among the most productive of the entire project. Eden App Platform, the security features in AOSentry, the compliance infrastructure, and the audit logging system all came together faster in Rails 8 than they would have in any other framework we evaluated.
The decision to move on was not a judgment on Rails 8. Rather, it was a recognition that our product was not purely a web application — it was an LLM streaming gateway that needed to sustain thousands of concurrent connections at low memory cost, packaged as a cross-platform application that needed to run on phones and desktops alongside the browser. Rails 8 excels at the web application pattern. LLM streaming gateways and cross-platform mobile clients simply are not web applications in the traditional sense.
In the end, every line of business logic we wrote in Rails 8 — every migration, every policy class, every audit log schema — transferred directly to the next iteration. Rails 8 taught us exactly what our architecture needed to look like, and Go and Flutter went on to provide the runtime characteristics to make it work at scale. If you're evaluating Rails 8 today, trust it completely for your web layer, plan around its concurrency model for anything streaming-heavy, and you'll make the same informed choice we did.
