A Phoenix Channel implementing the Tiptapex collaboration wire protocol.
defmodule MyAppWeb.DocChannel do
use Tiptapex.Collab.Channel
@impl true
def authorize("doc:" <> slug, _params, socket) do
case MyApp.Docs.get_by_slug(slug) do
nil -> {:error, %{reason: "not_found"}}
doc -> {:ok, Phoenix.Socket.assign(socket, :doc_id, doc.id)}
end
end
endMount it on your socket with a wildcard route matching the topics your
editors use (channel "doc:*", MyAppWeb.DocChannel).
Wire protocol
Matches PhoenixCollabProvider in assets/js/tiptapex/collaboration.js:
"client_sync"(binary) — a Yjs sync message from one peer, rebroadcast to the others as"server_sync"."client_awareness"(binary) — Yjs awareness (cursors, names), rebroadcast as"server_awareness"."request_state"— a freshly-joined peer asks the room for the current state; rebroadcast so an existing peer answers with a sync message.
The server never interprets the CRDT payloads — it is a relay. Your regular "save" flow remains the source of truth for persistence.
Optional persistence hooks
load_state/2 and persist_update/3 (both default to no-ops) let you
snapshot the opaque Yjs state: return {:ok, binary} from load_state/2
to seed late joiners, and store the binaries you receive in
persist_update/3. Merging Yjs updates server-side would require a
Yjs-compatible runtime, so the recommended strategy is peer-relay plus a
snapshot written by your save action.
Summary
Callbacks
Authorizes a join. Return {:ok, socket}, {:ok, socket, reply} (the
reply map is sent to the client), or {:error, reply}.
Loads a previously-persisted Yjs state binary to push to a joining peer
as an initial "server_sync". Default: {:ok, nil} (no stored state).
Called with every binary sync update relayed through the channel.
Default: :ok (no persistence).
Callbacks
@callback authorize(topic :: String.t(), params :: map(), socket :: Phoenix.Socket.t()) :: {:ok, Phoenix.Socket.t()} | {:ok, Phoenix.Socket.t(), map()} | {:error, map()}
Authorizes a join. Return {:ok, socket}, {:ok, socket, reply} (the
reply map is sent to the client), or {:error, reply}.
@callback load_state(topic :: String.t(), socket :: Phoenix.Socket.t()) :: {:ok, binary() | nil}
Loads a previously-persisted Yjs state binary to push to a joining peer
as an initial "server_sync". Default: {:ok, nil} (no stored state).
@callback persist_update( topic :: String.t(), update :: binary(), socket :: Phoenix.Socket.t() ) :: :ok
Called with every binary sync update relayed through the channel.
Default: :ok (no persistence).