AOCyber
Abstract illustration of converging rails reaching an architectural limit

From Fork to Framework, Part 3: Why Rails Almost Won

March 19, 2026

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.

AODex Rails home screen with conversation list and quick actions
The AODex Rails interface: dark mode, conversation sidebar, quick-action cards, and the gold brand identity that carried through to Flutter.

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.

AOSentry Rails admin dashboard showing request metrics and spend tracking
The AOSentry Rails admin dashboard: real-time request metrics, spend tracking, and the management interface built with Eden UI components.
AOSentry Model Hub showing 257 available AI models across providers
The AOSentry Model Hub: 257 models from every major provider, browsable and configurable through the Rails admin.
AOSentry guardrail log showing PII detection and token masking
Guardrail execution logs showing PII detection in action: tokens identified, masked, and tracked with full audit trail.

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.

graph TB subgraph AODex A1[Rails 8 API + Hotwire] --> A2[Eden UI Gem] A1 --> A3[Eden App Platform Engine] A1 --> A4[Solid Queue] A1 --> A5[Solid Cache] A1 --> A6[Solid Cable] end subgraph AOSentryRails B1[Rails 8 API] --> B2[eden_llm Gem] B1 --> B3[Devise + Pundit] B1 --> B4[Eden App Platform Engine] B1 --> B5[Solid Queue] end A4 --> PG[(PostgreSQL)] A5 --> PG A6 --> PG A3 --> PG B4 --> PG B5 --> PG REDIS[Redis] -. eliminated .-> PG style REDIS fill:#4a2020,stroke:#d4a853,stroke-dasharray: 5 5,color:#888 style PG fill:#1a3a1a,stroke:#d4a853,color:#c9a84c

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 jobsSidekiq + Redis, or Active Job's default adapterSolid Queue, backed by PostgreSQL, enabled by default
CachingRedis or Memcached cache storeSolid Cache, backed by PostgreSQL, enabled by default
WebSocketsAction Cable with a Redis adapterSolid Cable, backed by PostgreSQL polling
DeploymentCapistrano, Heroku, or a custom Docker pipelineKamal 2 and Thruster, bundled by default
AuthenticationDevise or a hand-rolled solutionNative generator, or Devise for advanced needs
Asset pipelineSprockets or esbuild/webpackPropshaft, simplified and default
Production databaseSQLite discouraged in productionSQLite 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.

  1. Audit your Ruby version first. Rails 8 requires a modern Ruby runtime, so check your .ruby-version file and your CI configuration, and upgrade to Ruby 3.2 or higher before you touch the Gemfile at all.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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 patternsLLM 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 rotationMobile 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 interfacesThread-per-request model: Ruby's GIL limits true parallelism to one thread per process
Solid Queue, Solid Cache, and Solid Cable: zero-Redis infrastructureType 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 daysMemory 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-documentedDeployment density: 600MB containers on shared infrastructure limit how many instances fit on a single node
AODex family-safe personas with age-appropriate guardrails
Family-safe AI personas with age-appropriate content filters, capabilities, and guardrails — built during the Rails 8 phase and carried forward into Flutter.

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.

Add to Preferred Sources
blog author avatar

Justin Donnaruma

Justin Donnaruma is the founder and Chief Innovation Officer of AOCyber, where he sets the technical direction for a product line built on one constraint: the customer pays, so the customer is never the product. He founded Don't Panic Consulting in 2016 — a managed service provider working with Fortune 500 companies, hedge funds and national brands, with zero customer churn across five years of operations — and founded AOCyber in 2025 to build AI-native, privacy-first platforms. He writes here about AI governance, the economics of "free" AI, and what actually holds up when a model reaches production.

Back to Blog
AOCyber

You don't have to be the product anymore. Building the next generation of applications.

© 2026 AOCyber. All rights reserved.