# How the TipTap Rich Text Editor Integrates with the Backend for Document Storage in Plane

> Discover how Plane's TipTap rich text editor integrates with PostgreSQL for efficient, collaborative document storage using Yjs CRDT.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: internals
- Published: 2026-06-23

---

**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`](https://github.com/makeplane/plane/blob/main/packages/editor/src/core/helpers/yjs-utils.ts). The `getBinaryDataFromRichTextEditorHTMLString` function orchestrates the transformation through several distinct phases:

1. **HTML Parsing**: The `generateJSON` utility from `@tiptap/html` parses the editor's HTML using the **RICH_TEXT_EDITOR_EXTENSIONS** schema configuration.

2. **Document Creation**: The resulting JSON feeds into `prosemirrorJSONToYDoc`, which constructs a Yjs document.

3. **Binary Encoding**: The `toBinary()` method produces a `Uint8Array`, which `convertBinaryDataToBase64String` encodes for API transmission.

```typescript
// 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`](https://github.com/makeplane/plane/blob/main/apps/web/core/hooks/use-page-fallback.ts), the application demonstrates how to attach the binary data to API requests:

```typescript
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:

```python
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`](https://github.com/makeplane/plane/blob/main/apps/live/src/services/pdf-export/pdf-export.service.ts)) transforms the stored binary back into usable formats:

```typescript
// 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

```typescript
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

```python
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.ts`](https://github.com/makeplane/plane/blob/main/packages/editor/src/core/helpers/yjs-utils.ts) using `generateJSON`, `prosemirrorJSONToYDoc`, and base-64 encoding.
- **API requests transmit base-64 encoded strings** via the `description_binary` field, as implemented in [`apps/web/core/hooks/use-page-fallback.ts`](https://github.com/makeplane/plane/blob/main/apps/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 `getAllDocumentFormatsFromDocumentEditorBinaryData` to 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`](https://github.com/makeplane/plane/blob/main/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.