Our product has been through many changes over the years. Both from a market standpoint, but also technically. Over the last year weâve simplified our architecture and moved away from a traditional Javascript single page application (SPA) and gone back to our Rails roots. Hereâs the story of why we chose Hotwire, what itâs allowed us to do, and where we hope to see it in the future.
How our tech stack evolved
A bit of Bearer history. We started as a Ruby on Rails heavy team, but as our original product grew, so did the needs of our architecture. Pretty soon, it was incredibly reliant on microservices. For good reasons, too. We were handling over a billion requests a month, we required resiliency as we sat in between our users requests and the outside world. The stack was very diverse to put it lightly. We ran a Rails API, GraphQL, ReactJS on the front-end, and a handful of other services running Elixir, NodeJS, and Rust to name a few. This was compounded by the in-app agents that our customers installed in their applicationsâwhich at the time we transitioned away from the product covered PHP, Go, Ruby, NodeJS, and Python. It was quite the engineering burden for a small team. Weâve spoken in the past about how we processed all that data effectively.
This stack quickly became fragile. The front end never had a true owner, we ran into caching and loading issues, and the development time went rogue. It became very tricky to deploy changes to the React app and the Rails app at the same time without one side getting out of sync. We ended up with something close to this process:
- Develop a dummy endpoint on the backend. This meant generating graphql schemas that the front-end developers could utilize immediately, without waiting for backend features to finish. We used graphql-code-generator to make schema validation with typescript easier.
- Develop the feature on the frontend.
- Develop the feature on the backend (in parallel with the frontend).
This added unneeded complexity and context switchingânot just the language differences between Ruby and Javascript, but more so the paradigm differences. There was just enough friction that prototyping became difficult.
We were essentially maintaining two applications where one should have been enough. The big learning was that this architecture might have made sense for large, stable applications, but it didnât make sense for a company iterating fast to find a good product market fit.
Time for a change
Since our above system wasnât working well for prototyping, our lead engineers were building a proof of concept for a new product in pure Rails. It let them work quickly and keep it really simple. Quick feedback loopâmeaning quick iterationâand no context switching.
Right around this time (late December 2020), Basecamp announced Hotwire.
It seemed like the timing lined up perfectly to seriously consider a change in architecture. The new product was very differentâand is even more different todayâand it was less complicated than the old product. It was much closer to a pure SaaS application than a mix of services and embeddable agents. Our engineering team sat down and looked at the current state of the app, where we wanted to go, and the technical choices we had. It was really important to get everyoneâs input, as this was going to be a big departure from the past product. Everyone was on board to move forward with a Rails and Hotwire-centric approach.
How we use Hotwire
With all that said, letâs look at how we build with Hotwire. To give a brief introduction to Hotwire, it is a collection of technologies that allow us to send HTML over the wire. The main technology behind Hotwire is Turbo, which is a variety of techniques to handle these HTML responses. Notably, Turbo allows us to send HTML from the server (whether in response to user interaction or a server-side event) that will dynamically change parts of the page without any custom JavaScript. In the rare cases where we do need some JS, we have Stimulus which is a lightweight JavaScript framework. It breaks down like this:
- Hotwire = HTML-over-the-wire = Turbo + Stimulus (for now)
- Turbo: techniques to handle HTML responses (Turbo Frames, Turbo Streams, âŠ)
- Stimulus: Lightweight JS Framework
It began with Stimulus
For us, our Hotwire journey actually began with the idea to use Rails with Stimulus. Coming from a place where we had Rails and React, we knew we needed some JS and Stimulus seemed like an appropriate choice. This did get us quite far, but we found that the Stimulus controllers we were using became large and complex. It turned out that this was a pretty big code smell in Hotwire, as the intention is to write less custom JS and instead rely on partial page updates to handle complex UI changes.
Turbo
To better understand Turbo, letâs look at two use cases for how we use Turbo at Bearer.
Turbo streams for reducing custom JavaScript
Using Turbo streams allowed us to greatly reduce JS usage for common UI behaviors, like modal popups, adding and removing items from a list, and responsively disclosing form elements. Streams allows us to send a request to the server, and receive back multiple Turbo streams that update multiple blocks in the UI. Weâre also using streams to keep the application state on the server, and then enforcing any updates on the client.
In the example below, when the âXâ button for an item was clicked, we were using custom JS to hide the item and update a hidden form field to delete the item on form submission.

This led to an edge case where all items could be deleted but the state of the âNextâ button would not be updated, meaning that we could submit the form with zero items selected. We addressed this using Turbo. Now, when the âXâ button for an item is clicked, it sends a DELETE request to the server which returns two Turbo Streams, one to remove the HTML element containing the deleted item, and another to update the âNextâ button in order to reflect any state change.
Lazy-loaded Turbo frames for a quick performance win
For many of our original components, we were loading everything at page load and then showing/hiding components as needed. This was resulting in a large page load, and really quite a waste for instances where not many components are seen by the user. In this example, we responsively disclose more content when a user selects hide/show contentâa pretty common interaction for most applications.

Instead of that heavy direct load, we  lazy-load a Turbo frame with very little code. For example:
Hotwire pros
The big pro: Built for a Rails ecosystem. As a team with tons of Rails fluency, it was low risk to move over to Hotwire. This meant it was quick to implement, and easy to maintain, especially since most Turbo use cases involve âregularâ Rails routes, controller actions and partials.
We also found that it allowed us to quickly iterate. We could build a product feature using standard HTML pages for our proof of concept, and then gradually introduce Turbo as needed to optimize flow and improve the rendering performance.
The other main advantage, especially compared to our old stack, was that data and validation can all be kept in once placeâthe server. This avoids logic duplication and typing duplicationâweâre looking at you Typescript and GraphQL.
Hotwire cons
The primary con is that Hotwire is still, relatively, new. The documentation is good, but not extensive.
For those coming from Rails, you are accustomed to an opinionated set of âBest practices.â Right now, many of the Hotwire best practices are still up for debate. For example, is it acceptable to return multiple Turbo Streams in a single response and if so, whatâs the preferred way to do this? How should we approach dynamic forms in Hotwire? Itâs never a great experience drilling through GitHub issues for the best approach to a problem. There is good news here. It seems like Hotwire will be a core, default part of Rails 7 in the future. This should greatly improve the lack of solid conventions that weâve seen so far.
The other thing to keep in mind is that it is still a primarily server-side solution. This requires some discipline to ensure that there arenât too many requests moving over the pipe. If every user interaction fires off a request and some of those are slower request, we need to think about implementing some kind of optimistic rendering in JavaScript while we wait for responses. You also need to know that it wonât be quite as slick as a single page application. You can get really far with Hotwire, but it really depends on the needs of your application. For us, it was the right level of detail for the interactivity our app required.
Where we go from here
If you couldnât tell, weâre pretty happy with the pace, performance, and quality weâve been able to achieve with Hotwire. On top of that, in our recent hiring round we kept hearing how attractive the stack was to prospective Rails developers. It's normally easy to find a "hot new tech stack", but it's hard to find one that scales, works for your product cadence, and is useful for hiring.
We look forward to the community stabilizing further as best practices become solidified. To help with that, we have some new Rails and Hotwire articles in the pipeline so keep an eye out for them. The team is always happy to share their findings, so reach out to us @BearerSH on Twitter if youâd like to know more.
â