Plant Together uses a WebSocket-based architecture to enable real-time collaboration. The system consists of multiple layers working together:
WebSocket Server - Handles real-time connections
Y-Redis - Yjs-compatible WebSocket server with Redis persistence
Redis - In-memory message broker for real-time updates
PostgreSQL - Persistent storage for document history
WebSockets provide a full-duplex communication channel over a single TCP connection, enabling real-time bidirectional data transfer between clients and servers.
Initially, Plant Together used the basic y-websocket server (started with npx y-websocket), but this had a critical limitation: rooms were persisted only in memory, making it difficult to control data retention after users disconnected.The solution was Y-Redis, created by Kevin Jahns (the creator of Yjs). Y-Redis extends y-websocket with:
Persistent storage - Documents are stored in PostgreSQL
Redis streaming - Real-time updates via Redis Streams
Scalability - Multiple server instances can share state via Redis
Y-Redis allows Plant Together to persist collaborative sessions even after all users have disconnected, enabling “pick up where you left off” functionality.
Browsers enforce a security requirement: HTTPS websites can only connect to secure WebSocket servers (WSS). Plant Together needed to upgrade from ws:// to wss://.
Instead of using NGINX as a TLS terminator (which is complex to configure), Plant Together uses a fork of y-websocket by Andreas Rozek that natively supports TLS certificates.
This fork eliminates the need for NGINX, simplifying deployment and reducing potential points of failure.
The awareness protocol broadcasts user information:
provider.awareness.setLocalStateField('user', { name: username, color: userColor.current,})// Listen for other users joining/leavingprovider.awareness.on('change', () => { const statesArray = Array.from(provider.awareness.getStates()) // Update UI to show active collaborators})
import { io, Socket } from 'socket.io-client'const authToken = await retrieveToken()const socket = io(serverHttpUrl, { extraHeaders: { 'room-id': roomId, Authorization: `Bearer ${authToken}`, },})// Listen for document creationsocket.on('/document', ({ code, documentName, id }: any) => { if (code === 200) { setRoomDocuments(docs => [...docs, { id, name: documentName }]) }})// Listen for document renamessocket.on('/document/rename', ({ code, newDocumentName, documentId }: any) => { if (code === 200) { // Update document name in UI }})// Listen for document deletionssocket.on('/document/delete', ({ code, documentId }: any) => { if (code === 200) { // Remove document from UI }})
Plant Together uses two WebSocket connections: Y-Redis for document content synchronization and Socket.io for metadata operations like creating, renaming, and deleting documents.
# Client connects to this WebSocket serverVITE_SERVER_WS_URL=wss://plant-together-yredis.example.com# Y-Redis server portYREDIS_PORT=3003# Redis connectionREDIS_PORT=6379REDIS=redis://redis:6379# PostgreSQL connection for document persistenceDB_NAME=postgresDB_HOST=database-psqlDB_PORT=5432DB_USER=postgresDB_PASS=secretpassword# Or use a full connection stringPOSTGRES=postgres://postgres:secretpassword@database-psql/postgres