This is a simple CRDT text collaboration demo powered by Yjs and Tiptap which uses PowerSync as both the persistence and connection provider. This means that the Yjs CRDT data structures are stored in Postgres. Conflicts are automatically resolved using CRDTs.
295104919-97c6bec0-d0ec-4719-8df1-797264b9b789.mov
This demo is built using the PowerSync JS web SDK.
Switch into the demo's directory:
cd demos/yjs-react-supabase-text-collabUse pnpm to install dependencies:
pnpm installThis demo can be started with local PowerSync and Supabase services.
Follow the instructions for configuring Supabase locally.
Copy the environment variables template file
cp .env.local.template .env.localStart the Supabase project
supabase startCopy the anon key and JWT secret into the .env.local file.
Run the PowerSync service with
docker run \
-p 8080:8080 \
-e POWERSYNC_CONFIG_B64=$(base64 -i ./powersync.yaml) \
-e POWERSYNC_SYNC_RULES_B64=$(base64 -i ./sync-config.yaml) \
--env-file ./.env.local \
--network supabase_network_yjs-react-supabase-text-collab \
--name my-powersync journeyapps/powersync-service:latestThis demo app uses Supabase as its Postgres database and backend:
- Create a new project on the Supabase dashboard.
- Go to the Supabase SQL Editor for your new project and execute the SQL statements in
database.sqlto create the database schema, database functions, and publication needed for PowerSync. - Enable "anonymous sign-ins" for the project here.
If you don't have a PowerSync account yet, sign up here.
Then, in the PowerSync Dashboard, create a new PowerSync instance:
- Right-click on 'PowerSync Project' in the project tree on the left and click "Create new instance"
- Pick a name for the instance e.g. "Yjs Demo Test" and proceed.
- In the "Edit Instance" dialog that follows, click on the "Connections" tab.
- Click on the "+" button to create a new database connection.
- Input the credentials from the project you created in Supabase. In the Supabase dashboard, under your project you can go to "Project Settings" and then "Database" and choose "URI" under "Connection string", untick the "Use connection pooling" option, and then copy & paste the connection string into the PowerSync dashboard "URI" field, and then enter your database password at the "Password" field.
- Click the "Test connection" button and you should see "Connection success!"
- Click on the "Credentials" tab of the "Edit Instance" dialog.
- Tick the "Use Supabase Auth" checkbox and configure the JWT secret.
- Click "Save" to save all the changes to your PowerSync instance. The instance will now be deployed — this may take a minute or two.
- Open the
sync-config.yamlin this repo and copy the contents. - In the PowerSync Dashboard, paste that into the sync streams editor panel.
- Click the "Deploy sync streams" button and select your PowerSync instance from the drop-down list.
To set up the environment variables for the demo app:
- Copy the
.env.local.templatefile to.env.local:
cp .env.local.template .env.local- Edit
.env.localand populate the relevant values:- Set
VITE_SUPABASE_URLto your Supabase project URL. You can find this by going to the main page for the project on the Supabase dashboard and then look for "Project URL" in the "Project API" panel. - Set
VITE_SUPABASE_ANON_KEYto your Supabase API key. This can be found right below the Project URL on the Supabase dashboard. - Set
VITE_POWERSYNC_URLto your PowerSync instance URL (this is the same URL from step 3)
- Set
In this directory, run the following to start the development server:
pnpm devOpen http://localhost:5173 with your browser to try out the demo.
To try out the collaborative editing, copy and paste the URL of the page and open it in another browser (or another browser window).
The more edits are made to a document, the longer the Yjs CRDT update history becomes. There is currently a very basic edge function available to merge updates into a single update row.
You can deploy it using the following command:
supabase functions deploy merge-document-updatesAnd invoke it using the Supabase CLI:
curl -L -X POST 'https://<project-ref>.supabase.co/functions/v1/merge-document-updates' -H 'Authorization: Bearer [anon-key]' --data '{"document_id":"[document-id]"}'Replace <project-ref> with your Supabase project ref, [anon-key] with your Supabase API key, and [document-id] with the UUID of the document (found in the URL of the specific document you're editing the demo app).
Note that this is not a production-grade implementation of merging updates – the current implementation will have race conditions and is only a PoC for development/testing.
To-do
- Add user sessions. For ease of demoing, still allow anonymously signing in (perhaps using this Supabase workaround), but keep track of session data so that each user has a unique
user_idwhich we can associate with edits to the document. - Improve sync streams: Use a many-to-many relationship between users and documents, so that all documents and their updates are not synced to all users. Dependent on user sessions.
- Add suggested RLS rules for Supabase. Dependent on user sessions.
- Add live cursor support; allow user to set their name, prepopulate with auto-generated name if none set. Dependent on user sessions.
- Show PowerSync connection status; allow user to toggle offline/online for testing purposes
- Add button to the UI allowing the user to merge the Yjs edits i.e.
document_updaterows. Invokemerge-document-updatesedge function in Supabase. - Prepopulate sample text into newly created documents.
- Improve performance / rework inefficient parts of implementation:
- [] Optimize the 'seen updates' approach to filter the
SELECTquery for updates that have not yet been seen — perhaps based oncreated_attimestamp generated on the Postgres side. For the watch query — watch for certain tables instead of watching a query. This will allow queryingdocument_updateswith a dynamic parameter. - Flush 'seen updates' when
document_updatesare merged.
- [] Optimize the 'seen updates' approach to filter the
Done
- Show number of edits (rows in
document_updates) on the document - For ease of demoing, when the user hits the root page, either generate a new random document UUID and redirect the user to that document, or redirect to last viewed document ID if any (ID stored in local storage).
- Add function to merge document updates; can be invoked if number of
document_updatesbecomes too large (currently a row is created indocument_updatesfor every edit/update from the Yjs document)