We have developed 100s of Rails application over the last few years. When we started, there was no AI coding agents. So our developers had to do everything by hand. While working on such projects, we saw some recurring patterns that were worth abstracting into independent gems.
These were very simple problems but repeating enough across projects, warranting a public gem. For example: A page transition leaves an old event listener attached. A canonical URL is missing from one template. A form needs validations but has no reason to create a database record. Each problem looks too small to demand architectural attention.
Repeated across features and releases, however, these gaps become a tax. Engineers spend time remembering where cleanup belongs, checking whether metadata stayed consistent, or bending a persistent model around a temporary interaction.
We maintain three open-source projects focused on those recurring problems: Achilles, PageStructuredData and PassiveModel. The RocketApex application repository includes all three, but none is intended as a universal prescription. Each represents a narrow boundary that we wanted to make explicit.
Our test for extracting a library
Code does not deserve a separate project simply because it might be reusable.
We look for a recurring responsibility that can be named without explaining the entire application around it. The interface should make ownership clearer, not move the same confusion into a gem. We also need to be willing to maintain its documentation, compatibility and release path.
Open sourcing raises the standard further. An internal shortcut can survive on team memory. A public library needs a contract that another developer can inspect and question.
Achilles: browser behavior needs a lifecycle owner
Rails and Turbo make it possible to keep most interface rendering on the server. Many applications still need small JavaScript components for forms, menus, filters, editors, notifications and other interaction islands.
Attaching that behavior is usually easy. Removing it at the correct moment is where trouble begins.
A component may own listeners, timers, observers or third-party widgets. If its DOM element is replaced while those resources remain active, the visible page can look correct while old behavior continues in the background.
Achilles gives that behavior an explicit lifecycle. A DOM node identifies a registered JavaScript class. The class implements setup() and teardown(), while Achilles coordinates those methods with Turbo navigation and dynamically inserted markup. Parent components are set up before their children, and children are torn down before their parents.
A recent Turbo Frame issue sharpened the underlying lesson. Achilles had relied on its DOM observer to discover that registered component roots had been removed. By then, Turbo had already replaced the outgoing frame content. Cleanup still happened, but the component element was gone, so the lifecycle ran late and produced a misleading missing-element error.
The correction was not to hide the error. Achilles now wraps Turbo’s frame renderer when turbo:before-frame-render fires. Immediately before Turbo invokes the original renderer, Achilles tears down and deregisters components inside the outgoing frame. Cleanup runs from the deepest component upward and includes the frame root when it is registered. The normal observer can then register components in the incoming content.
The important boundary is timing. Cleanup must happen while the outgoing DOM still exists, at the point where replacement actually occurs.
Stimulus remains a strong default for many Rails applications. Its targets, values, actions, conventions and controller ecosystem are useful. Achilles is for teams that prefer a smaller layer with explicit class mapping and standard DOM APIs. The choice is about fit, not framework superiority.
PageStructuredData: metadata is a page-level contract
Metadata drift rarely appears as one dramatic failure.
The title may live in a helper, Open Graph tags in a layout, canonical URLs in individual templates and JSON-LD in another partial. A page can look correct in the browser while search engines and social platforms receive incomplete or inconsistent information.
PageStructuredData keeps that responsibility in one page object and one rendering partial. A page can declare its title, description, canonical URL, robots instructions, social description, sharing image, breadcrumbs and supported Schema.org data together.
That does not make content useful or guarantee how a search engine will treat it. It makes the application’s intent easier to inspect, render consistently and test. Its warnings and valid? helpers let applications inspect incomplete supported structured data without blocking rendering.
The broader lesson is simple: metadata is application behavior. It deserves an owner and a testable contract, just like the visible page.
PassiveModel: not every valid object is a database row
Some inputs need model-like behavior without persistence.
A contact form, service input or request object may need naming, translation, conversion, validations and callbacks. Creating a database-backed model for that work can confuse validation with storage.
PassiveModel provides a small ActiveModel-based object for this case. It supports validations and validation callbacks, while save validates and runs registered before_save callbacks without writing to a database.
Its scope is deliberately narrow. It is not an ActiveRecord replacement. Current attribute assignment writes directly to instance variables, does not call custom setters and does not reject unknown names. persisted? also remains false unless the instance manages that state itself. Those constraints matter when deciding whether the abstraction fits.
A small utility earns trust by documenting where it stops.
The shared lesson
These projects operate in different parts of a Rails application, but the judgment behind them is the same: recurring work becomes safer when one boundary clearly owns it.
Before extracting your own library, ask:
Which review question or failure keeps returning?
Can the responsibility be explained without the whole application?
Will extraction reduce ambiguity, or merely relocate complexity?
Are you prepared to support the resulting contract?
If you want to inspect the code, tests and documented limits, visit the repositories for Achilles, PageStructuredData and PassiveModel. You can also see the wider context behind RocketApex’s open-source work.

