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.
In the repo directory, use pnpm to install dependencies:
pnpm install
pnpm build:packages
This 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.sql
to create the database schema, database functions, and publication needed for PowerSync.
For ease of demoing, this demo app uses anonymous authentication. The below instructions are derived from the powersync-jwks-example README:
- Install Deno and Supabase CLI if you don't have them already.
- Clone the powersync-jwks-example repo.
- In the powersync-jwks-example repo directory, run the script to generate a keypair:
deno run generate-keys.ts
- Then use
supabase secrets set
as shown in the terminal output to update the generated keys on Supabase (POWERSYNC_PUBLIC_KEY
andPOWERSYNC_PRIVATE_KEY
). You will need the project ref of the Supabase project you created previously. - Switch back to the
powersync-supabase-yjs-text-collab-demo
repo directory and deploy thepowersync-jwks
andpowersync-auth-anonymous
edge functions to Supabase: (Note that the Supabase CLI requires Docker Desktop to be installed for this step)
supabase functions deploy --no-verify-jwt powersync-jwks
supabase functions deploy powersync-auth-anonymous
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
- Enter the URL of your
powersync-jwks
edge function at the "JWKS URI" field. It should be in the formathttps://<supabase-project-ref>.supabase.co/functions/v1/powersync-jwks
. If needed, you can find the URL in the Supabase dashboard by going to your project and then to "Edge Functions". - Click "Save" to save all the changes to your PowerSync instance. The instance will now be deployed — this may take a minute or two.
- Now that your PowerSync instance is created, you should see a URL for it on the right side of the PowerSync dashboard in the "Deploy logs" panel. Click the button next to the URL to copy it.
- Use the copied value to set the
POWERSYNC_URL
secret to be used by your Supabase edge functions: (make sure there is no trailing slash at the end of the URL)
supabase secrets set POWERSYNC_URL=https://<powersync-instance-id>.powersync.journeyapps.com
- Open the
sync-rules.yaml
in this repo and copy the contents. - In the PowerSync dashboard, paste that into the 'sync-rules.yaml' editor panel.
- Click the "Deploy sync rules" 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.template
file to.env.local
:
cp .env.local.template .env.local
- Edit
.env.local
and populate the relevant values:- Set
VITE_SUPABASE_URL
to 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_KEY
to your Supabase API key. This can be found right below the Project URL on the Supabase dashboard. - Set
VITE_POWERSYNC_URL
to your PowerSync instance URL (this is the same URL from step 5)
- Set
In this directory, run the following to start the development server:
pnpm dev
Open 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-updates
And 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_id
which we can associate with edits to the document. - Improve sync rules: 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_update
rows. Invokemerge-document-updates
edge 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
SELECT
query for updates that have not yet been seen — perhaps based oncreated_at
timestamp generated on the Postgres side. For the watch query — watch for certain tables instead of watching a query. This will allow queryingdocument_updates
with a dynamic parameter. - Flush 'seen updates' when
document_updates
are 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_updates
becomes too large (currently a row is created indocument_updates
for every edit/update from the Yjs document)