Show HN: Pg_CRDT – an experimental CRDT extension for Postgres (supabase.com)
This is an experimental extension for CRDTs, pg_crdt: GitHub repo[0]. It supports Yjs/Yrs and Automerge.
The linked blog post [1] describes how we're thinking about this extension in a Supabase context. I want to emphasise this part: "pg_crdt has not been released onto the Supabase platform (and it may never be). We’re considering many options for offline-sync/support and, while CRDTs will undoubtedly factor in, we’re not sure if this is the right approach."
[0] GitHub repo: https://github.com/supabase/pg_crdt
[1] Blog post: https://supabase.com/blog/postgres-crdt
38 comments
[ 6.3 ms ] story [ 75.7 ms ] thread[0] Yjs: https://github.com/yjs/yjs
[1] Automerge: https://github.com/automerge/automerge
[3] pgx: https://github.com/tcdi/pgx
I'm going to enjoy getting stuck into this! (When I'm not stuck driving for six hours... just a quick coffee break reading this)
Support for CRDT columns within a db to enable JSON like indexing/querying capabilities is fundamental to then building some exciting further developments. Supporting both Yjs and Automerge is exactly the right route forward.
Without the ability to index properties, or do ad-hock queries, on the documents there is little reason to to the merging in-DB. It then makes more sense to merge outside the DB and save both the raw CRDT object and a serialised JSON next to it for querying. I assume that if you had found it's performance better, indexing+querying would have been the next step?
However I'm not surprised you found performance not ideal, I suspect a layer in-front of PG throttling writes is the only way forward for a real-time use case.
Personally I find the offline + eventual consistency the most interesting use case. The ability to sync a users document set to a local db (SQLite or a future mini-pg/supabase) with local querying and then merge them back later. This could be as basic as a three column table (guid, doc, clock), the clock being a global Lamport like clock to enable syncing only changed documents.
This would be achievable, but also perhaps quite strict - we're dictating a table structure rather than working with existing CRDT libs. That said, we could probably do this today, without any mods/extensions
> querying/indexing
Yeah, there is nothing in this implementation for that
> Without the ability to index properties, or do ad-hock queries, on the documents there is little reason to to the merging in-DB
One benefit is the ability to send "directly" to the database, but I agree that lack of additional features in the current form outweighs this benefit
> a serialised JSON
This is how we might do it with with "Realtime as an authority" approach (and is the most likely approach tbh).
Unfortunately, v8 basically is a bloated mess and postgres just stopped supporting it cause it's build process is horrendous.
At the moment, every tool in supabase is decoupled from the other tools. The only thing that each tool depends on is Postgres. This is beneficial, because now we don't need to build interfaces - Postgres is the interface.
Putting this implementation into Realtime would introduce a second dependency for some tools or create a burden for the developer. For example, this query goes directly to PostgREST:
If the "content" part of this is a CRDT, then suddenly the developer needs to know that that particular field needs to be sent through realtime (psuedo-code): Or we'd need to build some automatic-routing logic into the libraries (or PostgREST), which is also complex.In my view, CRDTs are a data problem, and therefore probably a database problem. If we can make Postgres act like "just another peer", then it simplifies the architecture.
> isn't it faster to iterate on your solution if you implement the merge semantics within the central source of truth that you have full control over?
We have full control over both Realtime and Postgres (at least the extension), so this is less of a concern. We're also less worried about fastest iteration than finding the correct solution long-term
Like the example with the changed fruits in the list, maybe one or the other should end up in the final result, maybe both, maybe none. The correct conflict resolution strategy is not obvious and highly dependent on the use case, that is why I said that implementations are usually not general or they are only general in the sense that you can plug in any behavior you like. Compare this with a real CRDT like a counter, there there is absolutely no doubt about the desired outcome.
In other words, the “conflict-free” part of the term refers to the fact that all replicas converge on the same value when all updates have been exchanged. What you’re discussing seems to have more to do with concerns around intention preservation when using CRDTs. In other words, an LWW register on a text field is still a CRDT, but in many applications it is potentially a poor choice in terms of preserving the intent of user-generated operations, but determining this is something done in a case-by-case basis.
If the content is a long document which users collaborate on over time, then a register is most certainly a poor choice. If it is a short text field akin to a label, an LWW register isn’t necessarily bad, because user intent is likely to replace the entire value, rather than to perform minute edits on the value.
that's... a very reasonable response. Nice one.
I think the fuzz comes from the more esoteric CRDTs, which solve actually hard problems (traditionally solved by Operational Transforms). But I don't think I could have created a simple example in the blog post for one of these.
As you correctly observe, very few applications that exist today have data models with operations that satisfy the requirements of CRDTs, including e.g. commutativity. But CRDTs were never meant to be a "swappable" state layer for existing applications. The idea has always been that applications would need to change how they interact with state, to a model which does satisfy those requirements. Such models can absolutely generalize, it just takes a bit of thinking.
In general, Sequence CRDTs seem to be the most difficult and most interesting area of continuous research. In my own work, I've implemented a sequence CRDT to create a distributed log of events across users that then (because the order is guaranteed to eventually be the same for each device) can just be reduced to whatever data structure you need. It means, we end up with one CRDT to rule them all and then can just write the "conflict resolution" in simple term with the business logic.
I'm not sure from the article, but I'm guessing the difference here is that it will apply the updates in the correct order (using last writer wins) regardless of when the yjs doc hits the server. I guess the idea here is you can get decent offline-ish support without directly storing timestamps, version numbers, soft-deletes etc and associated logic.
In the same way some operations are embarrassingly parallelizable (think adding elements to a set), some operations are inherently conflicting: editing elements of the same text, image, directory,... More than trying to decide in all cases which modification wins, we should detect when there are multiple legitimate options and ask the user to choose. Like a merge conflict in Git for example.
The application state feels so clean when conflicts can't arise or are automatically dealt with. But we have to acknowledge there are only so much datastructures that can merge automatically and still be intuitive for the user.
This is very nice and comes handy to me as I've been working for months on a couple projects using Yjs for which I adapted a Postgres persistence layer.
My only issue with this right now is that the code doesn't seem to have a license so please add (an open source ;)) one so I know I can deploy it with no worries :)
Can you explain more about your persistence layer and how it works?
In all honestly, to say it was a "persistence layer" is a bit of a stretch since it was not an additional layer per se, but here's what we did.
Context: I was the CTO for a sort-of text editor SaaS that allowed auditors to write reports collaboratively, these guys usually work together in teams that visit companies for a couple days/weeks, so our solution made everything much easier for them.
Anyway we tried many different CRDT(-like) solutions (automerge, etc...) and we eventually settled for yjs. All clients were coordinated and synced through a centralized server, with websockets providing the transport; we didn't make use of y-websocket though, we rolled our own thing since we needed a lot of custom functionality (see ahead).
Now on to persistence, we were already using Postgres in our backend so we wanted to store this data in there as well. Every "document" was internally a CRDT structure to which all collaborators connected to. Our solution was to create an extra yjs client for each of these documents (i.e. another hidden collaborator), whose role was to listen to all changes from the CRDT and storing them to the db, and viceversa, akin to what a data historian does, running inside our custom websocket server.
So, each time a new document had to be instanced, we first enabled the historian and recovered the relevant CRDT log before sending it to the user, and when all connections were closed this historian made sure everything was committed and deallocated itself to free server resources.
It was a bit complex but it worked quite well in production. It also had some sort of a nice composability element in the sense that we could just "add" an historian client to any existing pool of yjs peers and enable persistence for them transparently. These historians were easy to adapt/extend themselves so, for instance, we could make them use MySQL instead or any other db stack; then again, just by making them join an existing pool of peers, that smallish network gained a particular function. It was an interesting paradigm. I hope my explanation made sense.
Unfortunately that startup went kaput so I'm back at consulting, but some clients request this sort of functionality from time to time so I'll give this (pg_crdt) a try and add it to my stack of tools. Btw, Supabase is great, I've been following you for a couple years, you're really building a great product. Congrats!