A Yjs provider that enables real-time collaboration and persistence through Supabase.
- Real-time sync - Sync document changes across clients using Supabase Realtime broadcast
- Persistence - Persist document state to a Supabase Postgres table and restore on load
- Awareness - Track user presence, cursors, and selections with
y-protocols/awareness - Lightweight - Minimal dependencies, works with any Yjs-compatible editor
- TypeScript - Full TypeScript support with type definitions
npm install @supabase-labs/y-supabase yjs @supabase/supabase-jsimport * as Y from 'yjs'
import { createClient } from '@supabase/supabase-js'
import { SupabaseProvider } from '@supabase-labs/y-supabase'
// Create a Yjs document
const doc = new Y.Doc()
// Create Supabase client
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
)
// Create the provider
const provider = new SupabaseProvider('my-room', doc, supabase)
// Listen to connection events
provider.on('connect', () => {
console.log('Connected to Supabase Realtime')
})
provider.on('error', (error) => {
console.error('Provider error:', error)
})
// Use with any Yjs-compatible editor (Tiptap, Lexical, Monaco, etc.)
const yText = doc.getText('content')SupabasePersistence saves the full Yjs document state to a Supabase Postgres table and restores it when the document is opened. This works independently of SupabaseProvider — you can use either or both.
Create a table to store document state:
create table yjs_documents (
room text primary key,
state text not null
);import * as Y from 'yjs'
import { createClient } from '@supabase/supabase-js'
import { SupabasePersistence } from '@supabase-labs/y-supabase'
const doc = new Y.Doc()
const supabase = createClient('https://your-project.supabase.co', 'your-anon-key')
const persistence = new SupabasePersistence('my-room', doc, supabase)
persistence.on('synced', () => {
console.log('Document state loaded from Supabase')
})
persistence.on('error', (error) => {
console.error('Persistence error:', error)
})For real-time collaboration with persistence, pass persistence as a provider option:
const provider = new SupabaseProvider('my-room', doc, supabase, {
persistence: true
})
// Access persistence events via getPersistence()
provider.getPersistence()?.on('synced', () => {
console.log('Document state loaded')
})You can also pass persistence options directly:
const provider = new SupabaseProvider('my-room', doc, supabase, {
persistence: { table: 'custom_docs', storeTimeout: 2000 }
})The provider handles live sync between connected clients, while persistence ensures the document state survives across sessions. The persistence instance is automatically destroyed when the provider is destroyed.
type SupabasePersistenceOptions = {
// Table name to store document state (default: 'yjs_documents')
table?: string
// Schema name (default: 'public')
schema?: string
// Column name for the room/document identifier (default: 'room')
roomColumn?: string
// Column name for the binary state (default: 'state')
stateColumn?: string
// Debounce timeout in ms before persisting updates (default: 1000)
storeTimeout?: number
}| Event | Payload | Description |
|---|---|---|
synced |
persistence |
Initial state loaded from database |
error |
Error |
An error occurred (fetch, persist, or flush failure) |
Creates a new persistence instance. Immediately fetches existing state from the database and applies it to the document.
name- Room/document identifier (used as the primary key)doc- Yjs document instancesupabase- Supabase client instanceoptions- Optional configuration (see above)
destroy()- Stop listening and flush any pending writesclearData()- Destroy and delete the persisted state from the databaseon(event, listener)- Subscribe to eventsoff(event, listener)- Unsubscribe from events
type SupabaseProviderOptions = {
// Throttle broadcast updates (ms)
broadcastThrottleMs?: number
// Enable automatic reconnection on disconnect (default: true)
autoReconnect?: boolean
// Maximum reconnection attempts (default: Infinity)
maxReconnectAttempts?: number
// Initial reconnection delay in ms (default: 1000)
reconnectDelay?: number
// Maximum reconnection delay in ms (default: 30000)
// Uses exponential backoff: 1s, 2s, 4s, 8s
maxReconnectDelay?: number
// Enable awareness for user presence (cursors, selections, etc.)
// Pass `true` to create a new Awareness instance, or pass an existing one
awareness?: boolean | Awareness
// Enable persistence. Pass `true` for defaults, or pass SupabasePersistenceOptions
persistence?: boolean | SupabasePersistenceOptions
}Example with custom reconnection:
const provider = new SupabaseProvider('my-room', doc, supabase, {
autoReconnect: true,
maxReconnectAttempts: 5,
reconnectDelay: 2000,
maxReconnectDelay: 60000
})| Event | Payload | Description |
|---|---|---|
connect |
provider |
Connected to Supabase Realtime |
disconnect |
provider |
Disconnected from channel |
status |
'connecting' | 'connected' | 'disconnected' |
Connection status changed |
message |
Uint8Array |
Received update from peer |
awareness |
Uint8Array |
Received awareness update from peer |
error |
Error |
An error occurred (e.g., failed to decode update) |
Creates a new provider instance.
channelName- Unique identifier for the collaboration roomdoc- Yjs document instancesupabase- Supabase client instanceoptions- Optional configuration options (see above)
connect()- Connect to the channel (called automatically)destroy()- Disconnect and clean up resourcesgetStatus()- Get current connection statusgetAwareness()- Get the Awareness instance (ornullif not enabled)getPersistence()- Get the SupabasePersistence instance (ornullif not enabled)on(event, listener)- Subscribe to eventsoff(event, listener)- Unsubscribe from events
Awareness enables real-time presence features like user cursors, selections, and online status. It uses the standard y-protocols/awareness protocol, making it compatible with all Yjs editor bindings.
const provider = new SupabaseProvider('my-room', doc, supabase, {
awareness: true
})
// Set local user presence
const awareness = provider.getAwareness()!
awareness.setLocalStateField('user', {
name: 'Alice',
color: '#ff0000',
cursor: { line: 10, column: 5 }
})
// Listen for remote awareness changes
provider.on('awareness', (update) => {
console.log('Remote presence updated')
})
// Get all connected users
const states = awareness.getStates()
states.forEach((state, clientId) => {
console.log(`User ${state?.user?.name} is online`)
})import { Awareness } from 'y-protocols/awareness'
const awareness = new Awareness(doc)
const provider = new SupabaseProvider('my-room', doc, supabase, {
awareness: awareness
})Awareness states are automatically cleaned up when:
provider.destroy()is called- The user closes the browser tab (via
beforeunloadevent)
import * as Y from 'yjs'
import { MonacoBinding } from 'y-monaco'
import * as monaco from 'monaco-editor'
import { createClient } from '@supabase/supabase-js'
import { SupabaseProvider } from '@supabase-labs/y-supabase'
const supabase = createClient('https://...', 'your-key')
const doc = new Y.Doc()
const provider = new SupabaseProvider('my-room', doc, supabase, {
awareness: true,
persistence: true
})
provider.getPersistence()?.on('synced', () => console.log('Document state loaded'))
// Set user info for cursor display
const awareness = provider.getAwareness()!
awareness.setLocalStateField('user', {
name: 'User ' + Math.floor(Math.random() * 100),
color: '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')
})
const ytext = doc.getText('monaco')
const editor = monaco.editor.create(document.getElementById('editor')!, {
value: '',
language: 'javascript',
})
// Pass awareness for cursor/selection sync
new MonacoBinding(ytext, editor.getModel()!, new Set([editor]), awareness)MIT