How I implemented "Google-docs-like" collaboration with Hono, Hocuspocus and Bun (opens in new tab)

From the get-go, when I started working on Lydie, an open-source document editor and writing workspace, I knew I’d eventually have to implement real-time collaboration. I wanted multiple team members to be able to work on the same document, on par with tools such as Google Docs and Notion.

Despite never having worked on this before, I had heard good things about Hocuspocus, a WebSocket server that handles merging data into single source-of-truth documents using CRDTs via the Y.js library. Using CRDTs as the backbone for collaborative editing is one of the most widely used approaches, and is also used by large companies such as Google and Notion for their collaborative features.

Hocuspocus is also conveniently developed by the same team who is behind TipTap, which is the headless WYSIWYG library that Lydie uses to power its dynamic editor. And with their first-party extension, is it incredibly easy to set up the client-side communication between the editor and backend server.

Using Hocuspocus with Hono

Lydie uses Hono, a lightweight web framework for building backend APIs, and I wanted to keep our collaborative features close to our other backend features. Luckily, integrating the Hocuspocus server with Hono wasn’t difficult, as Hono provides utilities for setting up WebSocket connections.

import { logger } from "hono/logger";
import { ExternalApi } from "./external";
import { InternalApi } from "./internal";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { hocuspocus } from "../hocuspocus-server";
import { createNodeWebSocket } from "@hono/node-ws";

export const app = new Hono()
.use(
cors({
origin: [
"https://app.lydie.co",
"https://lydie.co",
"http://localhost:3000",
],
credentials: true,
})
)
.use(logger())
.get("/", async (c) => {
return c.text("ok");
})
.route("/internal", InternalApi)
.route("/v1/:idOrSlug", ExternalApi);

export const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({
app,
});

app.get(
"/yjs/:documentId",
upgradeWebSocket((c) => {
const documentId = c.req.param("documentId");

return {
onOpen(_evt, ws) {
if (!ws.raw) {
throw new Error("WebSocket not available");
}
hocuspocus.handleConnection(ws.raw, c.req.raw as any, documentId);
},
};
})
);

export type AppType = typeof app;
import { app, injectWebSocket } from "./api";
import { serve } from "@hono/node-server";
import { hocuspocus } from "./hocuspocus-server";

const port = 3001;

// Start server
const server = serve(
{
fetch: app.fetch,
port,
},
(info) => {
hocuspocus.hooks("onListen", {
instance: hocuspocus,
configuration: hocuspocus.configuration,
port: info.port,
});
}
);

// Setup WebSocket support (Node.js specific)
injectWebSocket(server);
Loading more...

Keyboard Shortcuts

Navigation
Next / previous item
j/k
Open post
oorEnter
Preview post
v
Post Actions
Love post
a
Like post
l
Dislike post
d
Undo reaction
u
Save / unsave
s
Recommendations
Add interest / feed
Enter
Not interested
x
Go to
Home
gh
Interests
gi
Feeds
gf
Likes
gl
History
gy
Changelog
gc
Settings
gs
Browse
gb
Search
/
General
Show this help
?
Submit feedback
!
Close modal / unfocus
Esc

Press ? anytime to show this help