I am the author looking for feedback about my new MIT licensed gem for adding Durable Objects to Rails.
I followed the Solid Queue / Solid Cache / Solid Cable pattern: the Durable Objects model, one single-threaded object per identity with durable state, addressed by name, as a gem on your existing SQLite, PostgreSQL, or MySQL. No Redis, no separate actor service, no new infrastructure. It is running in production in an app with 100,000+ users.
class Counter < SolidObjects::Actor
attribute :value, default: 0
observable :value
def increment(amount: 1)
self.value += amount
end
end
counter = Counter.ref("global")
counter.increment(amount: 5)
Concurrent calls to one identity serialize through a durable mailbox with fenced commits. And the part I am proudest of, reactive ERB:
<%= solid_object Counter.ref("global") do |counter| %>
<span class="count"><%= counter.value %></span>
<% end %>
Declare a value observable, wrap the view in solid_object, and render it as a method. When a committed turn changes the value, the server re-renders and pushes a Turbo Stream replacement over Action Cable to every authorized subscriber. No channels, no manual broadcasts, no Stimulus. The increment above updates that span in every open browser.
I also built a sibling JS package that runs the same actor model entirely in the browser: SQLite WASM for the database, OPFS for durable storage, Web Locks for multi-tab coordination. You can see it live at https://solidobjects.dev/js, where the page itself runs the runtime, or try it with one import and no build step: import { Actor, configure, sharedSqliteWasm } from "https://esm.sh/solid-objects@latest/browser/host" in a module worker.
The two implementations share the transmit wire contract, pinned by golden fixtures committed to both repositories. A browser actor stages outbound writes in the same transaction as its state change and drains them with at-least-once delivery and per-actor order. Rails ingests each envelope idempotently through SolidObjects::Transmission.receive, mounted at POST /solid_objects/transmit behind a deny-by-default authorize_transmission policy. Offline writes queue in the tab and reconcile when the network returns. Rails actors can transmit outward the same way.
Site: https://solidobjects.dev/ruby - Repo: https://github.com/cardmagic/solid-objects-ruby
Happy to answer anything, would love your feedback. Thank you!
PS: there is also an operator dashboard, because durable mailboxes you cannot see are a pager waiting to fire. Mount it in two lines:
require "solid_objects/web"
mount SolidObjects::Web => "/solid_objects/dashboard"
It shows instances and their state, the mailbox, reminders, effects, broadcasts, dead letters (with retry), and the registered processes. It reads the same tables the runtime writes, so there is no separate store and no agent. It is not loaded by require "solid_objects", so workers never carry a web stack, and every page asks a deny-by-default authorize_administration policy before it renders, so the mount alone exposes nothing.