Real-Time Rails Without Turbo: Modern Reactive UIs with Inertia and 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:
- 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. - 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 liveQuery.
[ Rails / ActionCable ] | [ DexieCable ] | [ Dexie (IndexedDB) ] | [ LiveQuery ]
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");
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
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
syncs_to_dexie via: UserChannel, to: :user
end
With syncs_to_dexie, whenever a Todo is created, updated, or deleted, DexieCable automatically broadcasts the corresponding Dexie operation targeting the user's channel stream.
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>
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
UserChannel[current_user].table("todos").add(id: 1, title: "Buy milk") #[cite: 1]
# Modify matching records
UserChannel[current_user]
.table("todos")
.where(:completed).equals(false)
.modify(completed: true) #[cite: 1]
# Delete specific scopes
UserChannel[current_user]
.table("todos")
.where(:project_id).equals(project.id)
.delete() #[cite: 1]
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:
- You keep complete control over your frontend state inside Svelte components.
- Your backend serves pure data rather than rendering HTML partials over WebSockets.
- 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)
Comments
No comments yet. Start the discussion.