/rushlight

Real-time collaborative code editing on your own infrastructure

Primary LanguageTypeScriptMIT LicenseMIT

🕯️ Rushlight

Make collaborative code editors that run on your own infrastructure: just Redis and a database.

Supports multiple real-time documents, with live cursors. Based on CodeMirror 6 and operational transformation, so all changes are resolved by server code. It's designed to be as easy to integrate as possible (read: boring). The backend is stateless, and you can bring your own transport; even a single HTTP handler is enough.

Unlike most toy examples, Rushlight supports persistence in any durable database you choose. Real-time updates are replicated in-memory by Redis, with automatic log compaction.

An experiment by Eric Zhang, author of Rustpad.

Motivation

Let's say you're writing a web application. You already have a database, and you want to add real-time collaborative editing. However, most libraries are unsuitable because they:

  • Require proprietary gadgets
  • Are not flexible enough, e.g., to customize appearance
  • Make you subscribe to a cloud service where you can't control the data
  • Use decentralized algorithms like CRDTs that are hard to reason about
  • Make it difficult to authenticate users or apply rate limits
  • Rely on a single stateful server, which breaks with replication / horizontal autoscaling
  • Need WebSockets or other protocols that aren't supported by some providers
  • Are just generally too opinionated

This library tries to take a more practical approach.

Usage

Install the client and server packages.

# client
npm install rushlight

# server
npm install rushlight-server

On the frontend, create a CollabClient object and attach it to your CodeMirror instance via extensions.

import { EditorView } from "codemirror";
import { CollabClient } from "rushlight";

const rushlight = await CollabClient.of({
  async connection(message) {
    // You can use any method to send messages to the server. This example
    // executes a simple POST request.
    const resp = await fetch(`/doc/${id}`, {
      method: "POST",
      body: JSON.stringify(message),
      headers: { "Content-Type": "application/json" },
    });
    if (resp.status !== 200) {
      throw new Error(`Request failed with status ${resp.status}`);
    }
    return await resp.json();
  },
});

const view = new EditorView({
  extensions: [
    // ...
    rushlight,
    rushlight.presence, // Optional, if you want to show remote cursors.
  ],
  // ...
});

Then, on the server, we need to write a corresponding handler for the POST request. Create a CollabServer object, which requires a Redis connection string and a persistent database for document storage.

The example below is with express, but you can use any framework.

import express from "express";
import { Checkpoint, CollabServer } from "rushlight-server";

const rushlight = await CollabServer.of({
  redisUrl: process.env.REDIS_URL || "redis://localhost:6473",
  async loadCheckpoint(id: string): Promise<Checkpoint> {
    // ... Load the document from your database.
    return { version, doc };
  },
  async saveCheckpoint(id: string, { version, doc }: Checkpoint) {
    // ... Save the new version of the document to your database.
  },
});

rushlight.compactionTask(); // Run this in the background.

const app = express();

app.post("/doc/:id", express.json(), async (req, res) => {
  const id = req.params.id;
  try {
    res.json(await rushlight.handle(id, req.body));
  } catch (e: any) {
    console.log("Failed to handle user message:", e.toString());
    res.status(400).send(e.toString());
  }
});

app.listen(8080);

That's it! See the ClientOptions and ServerOptions types for more configuration options.

To view a full demo application, a collaborative Markdown editor using Postgres to store documents, see the app/ folder in this repository.

Development

These are instructions for developing the library itself and running the demo application. Clone the repository, which is an NPM workspace. To build the TypeScript files, just run:

npm install
npm run lint
npm run build

The demo application requires Node.js version 18 or higher and Docker Compose.

docker compose up
npm run dev

Visit http://localhost:6480 in your browser.

Deployment

For the demo application:

npm ci
npm run build

export REDIS_URL=redis://...
export DATABASE_URL=postgres://...
npm start -w=app

Listens on port 6471 by default, or the PORT environment variable if set.

Why the name?

It comes from this quote, and the fact that rushlights are a type of makeshift candle; you make do with what you have.

“Early Sunday morning, Natasha and I lit a candle, looked in the mirror … They say you can see your future in the long row of candles, stretching back and back and back, into the depths of the mirror.”

”I see nothing but the candle in the mirror. No visions of the future. So lost and alone.”

―Dave Malloy, Natasha, Pierre & The Great Comet of 1812