Sign inSign up

opencloudeu/yjs

By opencloudeu

Updated 5 days ago

Yjs collaboration server for OpenCloud

Image
0

1.2K

opencloudeu/yjs repository overview

opencloud-yjs-server

The Yjs server for OpenCloud. It runs a Hocuspocus server that relays Yjs updates between clients editing the same file.

The service persists nothing. Documents are file-backed via WebDAV and hydrated from the client. Rooms still live in process memory, so all peers of a document must reach the same instance: run a single instance, or shard by document with sticky routing. Peers split across instances silently stop seeing each other, and their saves collide instead of merging.

Every connection is authenticated and authorized against OpenCloud:

  • the bearer token is validated against /graph/v1.0/me
  • write access is derived from the effective permission actions on the file
  • awareness states are re-stamped with the authenticated identity, so users cannot spoof each other
  • on connect, each client receives its own stamped identity over a stateless message, because Yjs never echoes a client's own awareness back to it

Seeding an empty room

The first client in a room writes the file body into the Y.Doc. Exactly one client may do that, or the room ends up holding the body twice. The yjs server decides which, because it is the only party that sees every connection to a room at once. A client asks over a stateless message, and gets a grant or a refusal. Read-only connections are always refused.

Grants live in process memory and last as long as the holder's connection. If the holder leaves before it seeds, the grant passes to another writer in the room. A grant for a room that already has content is harmless: it is permission, not an instruction, and the client checks its own document first.

Configuration

VariableDefaultDescription
OPENCLOUD_URL-Required. Base URL of the OpenCloud server, e.g. https://cloud.example.com
PORT1234Port to listen on
SHUTDOWN_GRACE_PERIOD_MS15000Grace period for graceful shutdown before the process exits with code 1

Routing

The service listens for plain HTTP on PORT and upgrades to WebSocket. Where it sits is up to the deployment: behind a reverse proxy on the OpenCloud host, on its own hostname, or reachable directly.

Clients connect to the URL configured as options.yjsServerUrl in the web config. Collaborative editing is off while that option is unset.

Whatever sits in front must:

  • forward WebSocket upgrades
  • not require authentication of its own

The second point is easy to get wrong. The bearer token does not travel in an Authorization header: browsers cannot set headers on a WebSocket handshake, so it arrives in Hocuspocus' own first message once the socket is already open. A proxy that demands an Authorization header therefore rejects every connection before the service ever sees it - and the service validates the token itself regardless.

The dev stack is one example: OpenCloud's own proxy forwards /yjs to the service, with the route marked unprotected for exactly that reason (see dev/docker/opencloud/proxy.yaml).

Running

The sources are TypeScript and are run directly by Node's type stripping, so there is no build step. Node 22.18 or newer is required.

The dev stack in docker-compose.yml builds and runs it as the yjs service, and mounts src/ read-only - so editing the server needs a docker restart web-yjs-1, not a rebuild. Running it on the host instead needs an OPENCLOUD_URL whose TLS certificate the host trusts; the dev setup's self-signed Traefik certificate does not qualify, which is why the container gets NODE_TLS_REJECT_UNAUTHORIZED=0.

As a container, built from the repository root:

docker build -f services/yjs/Dockerfile -t opencloud-yjs-server .

Healthchecks and readiness

The image defines a Docker HEALTHCHECK that verifies:

  1. the readiness endpoint (GET /healthz/ready) returns HTTP 200
  2. a WebSocket handshake to ws://127.0.0.1:$PORT succeeds

During normal operation /healthz/ready returns 200 ok. Once shutdown starts, it flips to 503 immediately.

Graceful shutdown

The service installs a SIGTERM/SIGINT/SIGQUIT handler and performs graceful shutdown:

  1. mark the instance as shutting down (readiness becomes 503)
  2. stop accepting new upgrade attempts
  3. close existing WebSocket connections and let Hocuspocus unload/flush open documents
  4. exit successfully when done

If shutdown does not complete within SHUTDOWN_GRACE_PERIOD_MS, the process exits with code 1.

Docker Compose defaults

In the repository docker-compose.yml:

  • yjs uses restart: unless-stopped
  • yjs uses stop_grace_period: 20s (5s buffer beyond the default 15s shutdown grace period)

Keep stop_grace_period higher than SHUTDOWN_GRACE_PERIOD_MS to allow the service time to flush and close cleanly before Docker sends SIGKILL.

Logging

The service writes operational logs to standard output/error (stdout/stderr) via Node's console methods. No file logger is used.

Tag summary

Content type

Image

Digest

sha256:2beddf0cc

Size

57.2 MB

Last updated

5 days ago

docker pull opencloudeu/yjs