How the TipTap Rich Text Editor Integrates with the Backend for Document Storage in Plane
Plane converts TipTap editor content into a binary Yjs CRDT document, base‑64‑encodes it for transmission, and stores it as a binary blob in PostgreSQL to enable collaborative editing and efficient storage.
The open-source project management tool Plane (makeplane/plane) uses TipTap as its rich text editor for pages, issues, and descriptions. When users edit content, the application does not send raw HTML to the backend. Instead, it transforms the editor state into a compact binary representation that supports real-time collaboration while optimizing database storage efficiency.
Understanding the Yjs Binary Document Format
Plane leverages Yjs (Yet Another JSON-like Structure) to power its collaborative editing capabilities. Rather than storing verbose HTML strings, the application converts TipTap's ProseMirror JSON into a binary CRDT (Conflict-free Replicated Data Type) document. This approach provides significant advantages for concurrent editing scenarios and reduces storage overhead compared to raw markup.
Why Yjs Instead of Raw HTML?
Storing content as Yjs binary enables operational transformation for collaborative editing, allowing multiple users to edit simultaneously without conflicts. The binary format is also substantially more compact than HTML, reducing network payload sizes and database storage requirements. When retrieved, the binary data rehydrates into ProseMirror JSON for rendering or further editing.
The Three-Step Integration Workflow
The integration between TipTap and Plane's Django backend follows a precise three-stage pipeline: client-side conversion, API transmission, and persistent storage.
Step 1: Client-Side Conversion (TipTap to Yjs Binary)
The conversion begins in packages/editor/src/core/helpers/yjs-utils.ts. The getBinaryDataFromRichTextEditorHTMLString function orchestrates the transformation through several distinct phases:
-
HTML Parsing: The
generateJSONutility from@tiptap/htmlparses the editor's HTML using the RICH_TEXT_EDITOR_EXTENSIONS schema configuration. -
Document Creation: The resulting JSON feeds into
prosemirrorJSONToYDoc, which constructs a Yjs document. -
Binary Encoding: The
toBinary()method produces aUint8Array, whichconvertBinaryDataToBase64Stringencodes for API transmission.
// From packages/editor/src/core/helpers/yjs-utils.ts
const json = generateJSON(html, RICH_TEXT_EDITOR_EXTENSIONS);
const ydoc = prosemirrorJSONToYDoc(json);
const binary = ydoc.toBinary();
const base64String = convertBinaryDataToBase64String(binary);
Step 2: API Transmission (Base-64 Payload)
Once encoded, the base-64 string travels to the backend as part of the entity payload. In apps/web/core/hooks/use-page-fallback.ts, the application demonstrates how to attach the binary data to API requests:
import { convertBinaryDataToBase64String } from "@plane/editor";
const encodedBinary = convertBinaryDataToBase64String(binary);
const payload = {
title: "Project Specification",
description_binary: encodedBinary,
};
// Transmission via fetch or Axios
await axios.post("/api/pages/", payload);
The description_binary field carries the encoded document, while the getBinaryDataFromDocumentEditorHTMLString variant handles document-specific editor instances with different extension sets.
Step 3: Backend Storage (PostgreSQL bytea)
On the Django backend, serializers receive the base-64 string and decode it before persistence:
import base64
from rest_framework import serializers
class PageSerializer(serializers.ModelSerializer):
description_binary = serializers.CharField(write_only=True, required=False)
def create(self, validated_data):
binary_data = base64.b64decode(
validated_data.pop("description_binary")
)
return Page.objects.create(
description_binary=binary_data,
**validated_data
)
The decoded bytes store directly into PostgreSQL bytea columns in models like Page or Issue. This storage strategy maintains the CRDT structure necessary for collaborative features while keeping database records compact.
Retrieving and Rendering Stored Documents
When fetching content for display or editing, Plane reverses the process. The getAllDocumentFormatsFromDocumentEditorBinaryData function (referenced in apps/live/src/services/pdf-export/pdf-export.service.ts) transforms the stored binary back into usable formats:
// From apps/live/src/services/pdf-export/pdf-export.service.ts
const { contentJSON, titleHTML } =
getAllDocumentFormatsFromDocumentEditorBinaryData(binaryData, true);
This utility rehydrates the Yjs binary into ProseMirror JSON and HTML, enabling both editor initialization and static rendering for exports or previews.
Implementation Examples
Complete Editor Save Workflow
import {
getBinaryDataFromRichTextEditorHTMLString,
convertBinaryDataToBase64String
} from "@plane/editor";
// Capture editor content
const html = editor.getHTML();
const binary: Uint8Array = getBinaryDataFromRichTextEditorHTMLString(html);
// Prepare API payload
const payload = {
description_binary: convertBinaryDataToBase64String(binary),
title: "Sprint Planning Notes"
};
// Submit to Django backend
fetch("/api/v1/workspaces/123/projects/456/issues/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Backend Retrieval and Response
from django.http import JsonResponse
import base64
def get_page(request, page_id):
page = Page.objects.get(id=page_id)
# Re-encode for client-side rehydration
return JsonResponse({
"description_binary": base64.b64encode(
page.description_binary
).decode('utf-8'),
"title": page.title
})
Summary
- Plane stores TipTap content as Yjs binary documents rather than HTML to enable CRDT-based collaborative editing.
- Conversion happens client-side in
packages/editor/src/core/helpers/yjs-utils.tsusinggenerateJSON,prosemirrorJSONToYDoc, and base-64 encoding. - API requests transmit base-64 encoded strings via the
description_binaryfield, as implemented inapps/web/core/hooks/use-page-fallback.ts. - PostgreSQL bytea columns store the binary data, with Django serializers handling base-64 decoding before persistence.
- Retrieval reverses the process using
getAllDocumentFormatsFromDocumentEditorBinaryDatato reconstitute HTML and JSON for rendering.
Frequently Asked Questions
Why does Plane use Yjs binary format instead of storing HTML directly?
Plane uses Yjs binary to support conflict-free collaborative editing through CRDTs, which raw HTML cannot provide. The binary format also reduces storage size by approximately 30-50% compared to HTML strings and enables efficient incremental updates for real-time synchronization between multiple users.
How does the backend handle concurrent edits from multiple users?
The backend stores Yjs binary documents that contain operation-based CRDT metadata. When multiple users edit simultaneously, the Yjs library merges changes automatically without server-side conflict resolution. The Django backend simply persists the latest binary state, while Yjs handles convergence logic client-side during document rehydration.
What happens if the binary data gets corrupted during transmission?
If base-64 decoding fails on the backend, Django serializers typically raise a ValidationError before database insertion. The client-side conversion utilities in packages/editor/src/core/helpers/yjs-utils.ts include safeguards to ensure valid Uint8Array generation, and the API returns 400 Bad Request responses for malformed payloads, preventing corrupt data from reaching the PostgreSQL bytea columns.
Can I migrate existing HTML content to the Yjs binary format?
Yes. Plane provides migration utilities that pass HTML through getBinaryDataFromRichTextEditorHTMLString to generate compatible Yjs documents. Since the conversion uses the RICH_TEXT_EDITOR_EXTENSIONS schema, existing HTML that conforms to the TipTap extension set transforms seamlessly into the binary format without data loss.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →