Cloudflare Durable Objects, Ported to Rails

necrodome2 pts0 comments

Solid Objects — Cloudflare Durable Objects for Rails (Ruby gem)

Ruby gem · Orleans-style virtual actors · v0.12.0<br>Cloudflare Durable Objects,<br>ported to Rails.

Solid Objects gives Rails the easiest reactive ERB there has ever been . Define a<br>solid object , drop it in an ERB template, and it live-updates in every open browser the moment<br>state changes. No channels, no broadcasts, no Stimulus, no JavaScript. It's powered by durable, ordered virtual<br>actors (Cloudflare Durable Objects, ported to Rails) running on the database you already have .

Get started →<br>View source

terminalshellcopy

bundle add solid_objects<br>bin/rails generate solid_objects:install<br>bin/rails db:migrate<br>bin/rails solid_objects:doctor

The same object, two runtimes<br>A distributed counter — Workers on the left, Rails on the right.

Durable Objects give a program an object it can address by name, whose state survives, and whose methods run one at a time. Solid Objects gives you that exact shape in Ruby.

Cloudflare Durable Objects · TypeScript

counter.tstypescript

import { DurableObject } from "cloudflare:workers"

export class Counter extends DurableObject {<br>async increment(amount = 1) {<br>const value = (await this.ctx.storage.get("value")) ?? 0<br>await this.ctx.storage.put("value", value + amount)<br>return value + amount

// from your Worker — addressed by name:<br>const id = env.COUNTER.idFromName("global")<br>const stub = env.COUNTER.get(id)<br>await stub.increment(5)

Solid Objects · Ruby on Rails

app/actors/counter.rbruby

class Counter 5

…and in a Rails view, the same object renders itself — live

app/views/counters/show.html.erberb

Live count:

The part you won't find anywhere else<br>Durable Objects that render themselves.

You just saw the counter own its state. Now watch it own its view. A Durable Object can hold a<br>WebSocket and push updates; Solid Objects does the Rails-native version — but it's declarative in your<br>template. Wrap ERB in solid_object, render an observable as a method, and the<br>object's committed state streams itself to every connected browser. No channel classes. No manual broadcasts.<br>No Stimulus controller. This is the easiest reactivity Rails has ever had.

1 · Declare state and observables

app/actors/shopping_cart.rbruby

class ShoppingCart { [] }

def add_item(sku:, quantity: 1)<br>self.items sku, "quantity" => quantity }<br>end

observable :items_count do<br>self.items.sum { |item| item["quantity"] }<br>end

observable :subtotal_cents do<br>self.items.sum { |item| item["quantity"] * 2_350 }<br>end<br>end

2 · Use it in a plain ERB template

app/views/carts/show.html.erberb

Items in cart:<br>Subtotal:

Repeatable, keyed, batched live components

app/views/matches/show.html.erberb

app/views/actors/match/_scoreboard.html.erberb

Repeat a component with a signed key: and JSON-compatible<br>locals:, and opt into refresh: :morph for in-place<br>Turbo morphs with superseded-request cancellation and browser-side revision fencing. On any change the frame<br>re-renders on the server, through request-time authorization — keys and locals included — so<br>Solid Objects never broadcasts personalized HTML. Refreshes coalesce; reconnects converge to the current revision.<br>Give components a shared batch: and they collapse into a single request per revision —<br>the whole scoreboard refreshes in one round trip, and unchanged components are never requested.

add_item(…)<br>→turn commits<br>→observable changed<br>→Turbo Stream replace<br>→every browser updates

3 · Mutate from anywhere

the trigger — a controller, an API, another actorruby

# no broadcast call, no channel, no JS:<br>ShoppingCart.ref(current_user.id).add_item(sku: "SKU-9")

# solid_object already rendered a .<br># the committed change replaces just that — nothing else.

4 · Authorize calls, reads & the stream

config/initializers/solid_objects.rbruby

SolidObjects.configure do |config|<br># every policy denies by default. one ownership predicate gates<br># mutations, reads, and the Cable stream:<br>owns_cart = lambda do |actor_type:, actor_id:, authorization_context:, **|<br>user = authorization_context&.current_user<br>actor_type == ShoppingCart.actor_type &&<br>user.present? &&<br>actor_id == user.id.to_s<br>end

config.authorize_message = owns_cart # add_item + other mutations<br>config.authorize_query = owns_cart # initial server render<br>config.authorize_subscription = owns_cart # Action Cable stream<br>end

No lost-update divergence. The signed token proves integrity, not authority — the channel still<br>runs your authorize_subscription policy before streaming, and a reconnect refreshes<br>from current actor state, so a missed broadcast is only temporary staleness. Solid Objects signs the stream with a<br>key derived from Rails.application.key_generator (set stream_signing_secret<br>only to pin a separate stable secret), and bundle exec solid_objects start runs the<br>broadcast worker that delivers the updates.

New in 0.6.0 · personalized state payloads, for JS-driven UIs

app/actors/room.rbruby

class Room { {} }<br>attribute :turn, default: 1

# runs once per...

objects rails durable solid counter state

Related Articles