DEV Community

Cover image for Real-Time Rails Without Turbo: Modern Reactive UIs with Inertia and DexieCable
Stefan Buhrmester
Stefan Buhrmester

Posted on

Real-Time Rails Without Turbo: Modern Reactive UIs with Inertia and DexieCable

Hotwire and Turbo Streams have become the default answer for real-time applications in the Rails ecosystem. They are great for HTML-over-the-wire setups, but what if you prefer building your frontend with component frameworks like Svelte, paired with Inertia.js for server-driven routing?

When you step outside the Turbo ecosystem, real-time sync often gets cumbersome. Do you re-fetch Inertia page props on every WebSocket event? Do you write custom ActionCable subscribers and manually mutate complex client-side stores?

There’s a much cleaner way to handle real-time sync without Turbo—by placing a local database in the middle.

Meet DexieCable.


The Problem: Real-Time in Inertia Apps

Inertia.js gives us monolith productivity with the rich UI component model of Svelte, Vue, or React.

However, handling real-time updates over WebSockets in an Inertia app typically leads to one of two awkward patterns:

  1. Inertia Reloads (router.reload({ only: ['todos'] })): Every WebSocket event triggers a network request back to Rails to fetch updated props. It works, but it causes unnecessary server load and adds latency to UI updates.
  2. Manual Component State Sync: You listen to ActionCable events and manually splice arrays or update objects inside frontend state stores. This scales poorly and quickly leads to brittle client-side logic.

The Solution: Local-First Synchronization with DexieCable

DexieCable bridges the gap between Rails (via ActionCable) and client-side IndexedDB (via Dexie.js).

Instead of pushing WebSocket updates directly into your UI components, DexieCable allows Rails to execute Dexie.js write operations straight into IndexedDB over ActionCable. Your UI components then subscribe to Dexie using reactive liveQueries.

[ Rails / ActionCable ]
         |
   [ DexieCable ]
         |
 [ Dexie (IndexedDB) ]
         |
   [ LiveQuery ]
Enter fullscreen mode Exit fullscreen mode

This architecture gives you some significant benefits:

  • Zero Sync Boilerplate: Components don't care how data arrived in IndexedDB; they simply observe local tables.
  • Instant UI Updates: UI rendering runs off browser memory/IndexedDB—eliminating network render delays.
  • Decoupled Architecture: ActionCable updates IndexedDB in the background, regardless of which page or component is currently mounted.
  • Declarative Rails Macros: You can automate model broadcasting on the backend using simple ActiveRecord macros.

How It Works in Practice

Let’s look at a complete example using Rails, ActionCable, DexieCable, and Svelte.

1. Setting Up the Client (Dexie + DexieCable)

Configure your Dexie database and point DexieCable to your database instance.

// db.js
import Dexie from "dexie";
import DexieCable from "dexiecable";

export const db = new Dexie("MyAppDB");

db.version(1).stores({
  todos: "id, title, completed, updated_at"
});

// Pass your Dexie database instance to DexieCable and subscribe to your channel
DexieCable.db = db;
DexieCable.subscribe("UserChannel");

Enter fullscreen mode Exit fullscreen mode

2. Setting Up Rails (Channel & Model)

First, include DexieCable in your ActionCable channel:

# app/channels/user_channel.rb
class UserChannel < ApplicationCable::Channel
  include DexieCable

  def subscribed
    stream_for current_user
  end
end

Enter fullscreen mode Exit fullscreen mode

Next, use the syncs_to_dexie macro on your model:

# app/models/todo.rb
class Todo < ApplicationRecord
  belongs_to :user

  # Automatically syncs create (add), update (put), and destroy (delete) events[cite: 1]
  syncs_to_dexie via: UserChannel, to: :user
end

Enter fullscreen mode Exit fullscreen mode

Internally, syncs_to_dexie creates after_commit callbacks, that broadcast the corresponding Dexie operation. In this case, it would broadcast using UserChannel.broadcast_to todo.user.

3. Reactive Rendering in Svelte

In your Svelte + Inertia view, query Dexie using liveQuery. When ActionCable pushes changes, Dexie updates IndexedDB, and Svelte reactively re-renders the UI automatically.

<!-- Todos.svelte -->
<script>
  import { db } from './db';
  import { liveQuery } from 'dexie';

  // Observe the local IndexedDB table reactively
  let todos = liveQuery(() => db.todos.toArray());
</script>

<div class="todo-list">
  <h1>Real-Time Todos</h1>

  {#if $todos}
    <ul>
      {#each $todos as todo (todo.id)}
        <li class:completed={todo.completed}>
          {todo.title}
        </li>
      {/each}
    </ul>
  {/if}
</div>

Enter fullscreen mode Exit fullscreen mode

Advanced Query Chaining from Rails

syncs_to_dexie covers standard CRUD synchronization, but DexieCable also lets you chain arbitrary Dexie operations directly from Rails controllers or background jobs:

# Single item insert[cite: 1]
UserChannel[current_user].table("todos").add(id: 1, title: "Buy milk") #[cite: 1]

# Modify matching records[cite: 1]
UserChannel[current_user]
  .table("todos")
  .where(:completed).equals(false)
  .modify(completed: true) #[cite: 1]

# Delete specific scopes[cite: 1]
UserChannel[current_user]
  .table("todos")
  .where(:project_id).equals(project.id)
  .delete() #[cite: 1]

Enter fullscreen mode Exit fullscreen mode

DexieCable serializes the method chain into JSON, sends it across ActionCable, and replays the exact operation chain against the local IndexedDB database in the browser.


Why Choose This Over Turbo Streams?

Turbo Streams couple your backend directly to HTML fragment generation or DOM manipulation.

By choosing the DexieCable + Inertia + Svelte approach:

  1. You keep complete control over your frontend state inside Svelte components.
  2. Your backend serves pure data rather than rendering HTML partials over WebSockets.
  3. Your UI feels immediate because reads occur locally against IndexedDB.

Wrapping Up

If you prefer the Rails + Inertia stack but want reactive real-time updates without Turbo, DexieCable provides a lightweight pattern that bridges the gap. Rails manages data and business logic, ActionCable handles transport, Dexie manages local browser storage, and Svelte delivers the UI.

Check out the repository on GitHub:

👉 github.com/buhrmi/dexiecable

Top comments (0)