> Add real-time collaborative editing to a Rails app with yrby. Install the gems, run the generator, render one tag, and bind an editor in the browser.

Canonical: https://yrby.example.com/docs/getting-started
Source (authoritative): https://github.com/jpcamara/yrby/blob/main/README.md#install

---

# Getting started

yrby lets several people edit the same document in your Rails app at once. It
wraps [y-crdt](https://github.com/y-crdt/y-crdt), the Rust version of Yjs, for
Ruby. On top of that it adds a sync channel for Action Cable and AnyCable, a
browser client, and tools to read and render documents in Ruby. You don't need
to run Node.

## Install

Add both gems, then install the browser package and its peer dependencies:

```ruby
# Core CRDT and protocol primitives
gem "yrby"

# The Rails side: the sync channel, the document models, the generator
gem "yrby-rails"
```

```
npm install yrby-client yjs y-protocols @rails/actioncable
```

yrby needs Ruby 3.4 or newer. Releases include precompiled gems for Ruby 3.4
and 4.0. You only need [Rust](https://rustup.rs) when no precompiled gem
matches your platform and the gem builds from source.

## Install the storage

The generator adds one migration. The models and the channel come with the gem,
so there's nothing else to generate.

```bash
bin/rails generate yrby:install
bin/rails db:migrate
```

## The server side

You don't need to write a channel. Render the tag in a view that only users who
can edit the record can see:

```erb
<%= collaborative_document_tag @post, :body %>
```

The tag includes a signed token for that record and attribute. The browser
subscribes to the gem's `Y::DocumentChannel` with it, and the channel looks up
the record. The channel saves each change before it confirms it. It uses
`Y::Document`, or `Y::EncryptedDocument` when the attribute is declared with
`encrypted: true`.

The browser only ever sends that token. The channel rejects it if it's missing,
tampered with, expired, signed for a different attribute, or points at a
deleted record. Only your server can sign a token, so a valid one means your
controller already let this user see the page. To also check the user's
current permissions when they subscribe, see
[The document channel](/docs/document-channel).

The document is stored as rows in your database, and you can read it back in
Ruby:

```ruby
doc = @post.collaborative_document(:body).y_doc
doc.read_text("content")  # for rich text, use Y::Lexxy.new(doc).to_html
```

If you need documents keyed by room, a different store, or your own
authorization, generate your own channel with
`bin/rails generate yrby:install --channel`. It's a few lines long and uses the
same concern as `Y::DocumentChannel`.
[The document channel](/docs/document-channel) covers it.

## The browser side

The tag renders a `<yrby-document>` element. Import it once and it connects by
itself. When the document has synced, your listener gets it and hands it to
your editor:

```js
import "yrby-client/element"

document.addEventListener("yrby:synced", ({ target, detail }) => {
  const editor = bindYourEditor(target, detail.doc, detail.provider)
  detail.signal.addEventListener("abort", () => editor.destroy(), { once: true })
})
```

`yrby:synced` fires once the document has caught up with the server. Wait for
it before you attach the editor. Most editor bindings add an empty paragraph when
they start. If two people attach before the server's copy arrives, the document
ends up with both paragraphs.

When the signal aborts, remove the editor and its listeners. Leave the
document, the provider, and the cable consumer alone, because yrby manages
those. The event bubbles, so one listener on `document` also covers elements
added to the page later.

If the user leaves the page before their edits are sent, yrby keeps sending
them as long as the tab stays open. The next visit loads the saved content from
Rails, with a fresh undo history. The
[JavaScript client](/docs/javascript-client#document-sessions-and-navigation)
page has the details, and the [record-backed editor](/examples/document) lets
you try it in two windows.

On AnyCable, set the consumer before any element connects:

```js
import { YrbyDocumentElement } from "yrby-client/element"
import { createConsumer } from "@anycable/web"

YrbyDocumentElement.consumer = createConsumer()
```

See [The JavaScript client](/docs/javascript-client) for the rest of the API.

## What yrby covers

`yrby` binds the parts of `y-crdt` you need to sync and persist collaborative
documents: a `Doc`, awareness, and the y-websocket protocol primitives. The
server doesn't need to know what's in a document to sync it. It applies
updates, answers sync handshakes, and saves changes without reading them. The
editor in the browser defines the document's structure. When you want to read
the contents in Ruby, use `Doc#read_text` or `Doc#read_map`.

The API is small. Most of the gem's code deals with saving changes, delivering
them reliably, and thread safety.

## Editors

yrby doesn't look inside Yjs updates, so any editor with a Yjs binding works.
The demo app in the repo runs four of them, and CI types into each one in real
Chrome.

| Editor | Yjs binding |
|---|---|
| [Tiptap](https://tiptap.dev) (v2) | `@tiptap/extension-collaboration` |
| [Lexxy](https://github.com/basecamp/lexxy) (Lexical) | [`lexxy-realtime`](https://www.npmjs.com/package/lexxy-realtime) |
| [Rhino Editor](https://github.com/KonnorRogers/rhino-editor) (Tiptap 3) | `@tiptap/extension-collaboration` + `-caret` |
| [CodeMirror 6](https://codemirror.net) | `y-codemirror.next` |

The same channel also syncs documents that aren't text, such as a whiteboard
on a `Y.Map`, a kanban board on a `Y.Array`, or a spreadsheet on a `Y.Array` of
nested `Y.Map`s. This site has six [live demos](/demos). In the Lexxy one,
built on [lexxy-realtime](https://github.com/jpcamara/lexxy-realtime), the
server renders the document to HTML with `Y::Lexxy` after every change and
saves it to a plain column.

## Reading a document in Ruby

You can read a document on the server for search, exports, or emails:

```ruby
doc.read_text("prosemirror")  # => plain text of a Y.Text root, or nil
doc.read_xml("root")          # => text of an XML root, one block per line
doc.read_map("state")         # => a Y.Map root as a JSON string
doc.read_array("cards")       # => a Y.Array root as a JSON string
```

For HTML that matches what the editor produces, see
[Server-side rendering](/docs/rendering).

## Thread safety

You can share a `Doc` across Ruby threads. Puma threads, Action Cable
connection threads, and background jobs can all use the same one at once, and
your code doesn't need to lock it.

Methods that do real CRDT work release Ruby's Global VM Lock while the native
code runs. So on MRI, several threads can do CRDT work in parallel, and a big
update on one thread doesn't hold up the others.
