Transcript
James Arthur: There's a quote from the racing driver Ayrton Senna, probably the greatest racing driver ever. "If you see a gap that exists and you no longer go for it, then you are no longer a racing driver." I think this applies to us, to developers. We share this need for speed. The whole evolution of web development has been about making better, faster apps. That's what this track that Dan has put together is about, is how we do that. I was listening at a conference we organized called Sync Conf, here in SF, to Sunil Pai from Cloudflare. He's quite a deep React geek. You may have seen him on Twitter. He's been in the ecosystem a long time. His words were, it used to be fun talking about React.
What he was saying is no one's talking about it anymore, and all the React geeks are now the local-first geeks, or the sync geeks, or in his case, the agent geeks, given he's now building the agent's SDK for Cloudflare. What he was alluding to is that the alpha in reactivity frameworks, in client-side reactivity, is just not there anymore. We've had all this era optimizing reactivity loops on the client, with actions and reducers and signals and whatnot. It's come to the point where it is hard nowadays to still see a gap. Where is the remaining alpha? What I would like to argue in this talk is that it lies in extending reactivity to the server. Instead of building apps in the mainstream way, in the time-honored way, on data fetching, where you have all these issues from coding across the network to deal with.
There are massive gains, much larger gains to be had in abstracting away the network and extending the reactivity all the way to the server, with both this inner loop of client-side reactivity and this outer loop of client-server reactivity. You get this model where if you start at the view, you can make a local write, and it can be instantly added as optimistic state to the client store. That then triggers the view to re-render, and the user sees it instantly. Then the write syncs in the background to the server. Then if the app is subscribing to changes, when any relevant changes arrive in the database, whether they're your changes, whether they're changes from somebody else, they sync back out into the client. You update the store, swap out the optimistic state, and render. That's the model that I'm talking about.
Sync: The Next Frontier in Frontend
It is pretty obvious when you think about it, like clients don't exist in isolation. Clients need to talk to the server. Particularly with agents, there's now loads more going on on the server. Talking to the server takes 10, 100, 1,000 milliseconds. It's by extending this reactive state management to the server that you can get rid of this loading spinner. You meet the need for speed, and you close that gap that the network introduces. You get optimal user experience and developer experience based upon declarative data bindings that go across the whole stack. It is kind of like React, but for data loading instead of just UE. React gave us declarative UE and data bindings in the client. I'm old enough to remember development when it was like dollar functions with jQuery and imperative manual DOM updates. Of course, React just abstracted all of that away.
With this sync-based architecture that I'm talking about, you go from manual and imperative data fetching to declarative data bindings that go across the network where the system can then just take care of concerns like data transfer, data placement, retention. Basically, it handles all of the network programming for us.
As a result, you get this user experience where everything's instant because the data is just there locally. It works across patchy connectivity or downtime. It is naturally real-time and collaborative because it's based on syncing the changes back from the server. Of course, the thing nowadays about agentic systems is that they are inherently collaborative. Like you go with an agentic system, you go from at least one single user and one agent. It just starts off as a multi-user system. Then, in many cases, you spawn lots of agents, and then they're spawning sub-agents. All of these become like actors, users, writers, processes in your system writing data. Then you need to keep the humans in the loop on all of this activity that's now happening on the server. What are you going to do? Poll the changes? Like, no.
It would just be a hole in the head to build systems like that, like across all of the agents and all of the workflows, even when the agents are spawning their own agents, and their swarms and swarms of them. You're not going to do it like that. You need a reactive data plane to be able to keep users and agents in sync in this next generation of software, which means by definition that you need reactive state management that extends across client and server. This gap is about speed, but it is also about collaboration. At the same time, the way in which we are now all building software is also changing. We are increasingly not writing the code. We are working through coding agents. The challenge with coding agents moves away from actually being able to write the code ourselves to issues with quality and understandability.
If the coding agents are writing code like this, which is the code that we looked up up front with that imperative data fetching, then it just rapidly becomes a big ball of spaghetti. Because the LLM is just writing plausible component code that's then making network fetches, and there's no control over as the app is rendering, like what's making what requests where. That just doesn't scale at all. Whereas if the LLMs write code using declarative data bindings, which looks almost identical but it is so different under the hood because the system can take care of optimizing data transfer and storage and placement, you don't get this fetch spaghetti. Which means that the LLM code can actually scale into production.
Let's recap. I am arguing that sync is the next frontier in frontend. It is the best way of building apps generally, fast, modern, reactive, multi-user applications. It is the only sane way of building agentic software, keeping humans and agents in the loop. It solves some of these key limitations with working with LLM coding agents.
HTTP vs. WebSocket
Before we jump into how to do it and the tools that I want to show today, let's just maybe touch on why this isn't the norm already. Like, what is the catch? If you dig into it, it used to cost a lot of innovation tokens to build on this architecture. Just too many for most teams. All these reference apps like Figma, like Linear, like Notion that are built on sync engines, all these companies have absolutely crack-hot teams. They've invested millions of dollars into making this kind of tech work. The same at Meta. Not just making it work, but actually running it in production. If you just adopt the persona of a CTO or a tech lead? Imagine you are. Now imagine that you are evaluating how you are going to build a mission-critical product that you just want to get to market quickly.
Now compare these two technologies, an HTTP request and a WebSocket. Which one do you want to be shipping?
Participant 1: WebSocket.
James Arthur: That's the wrong answer. Why do you want to ship the WebSocket?
Participant 1: It depends on what the product is. In my case, a lot of our stuff is web-based, so having an open channel would make sense.
James Arthur: You're saying, ship the WebSockets to get the real-time streaming behavior. Absolutely. There's a thing where with WebSockets, people can build more real-time systems. If you just look at it operationally, an HTTP request is stateless. It's the kind of thing that you can deploy. You have instrumentation. You have observability. You know how to debug it. Whereas WebSockets, what happens when your application scales up? Is it maintaining too much memory on the server because of the state on the server side of the connection? How do you debug them? You just don't get the same level. It is basically an innovation token that you're deploying to get that capability. This is the simplest possible example of this type of thing. We all know that there is this wonderful, fascinating rabbit hole to dive into with things like CRDTs and incremental view maintenance and local-first software.
Believe me, because as a company at Electric, we do this stuff. These are the PhDs on our team. It's like literally the inventors of CRDTs. That rabbit hole is PhD-level deep.
Building End-to-End Reactive Apps - Tooling
If you just come back to like you're a normal company or a normal team trying to build product, you're really looking for an off-the-shelf solution to that type of engineering. You don't want to have to invest millions of dollars into engineering the sync engine for your application. You ideally want the tech to just exist. The thing in the past is it didn't used to exist. Or at least if it did, it fell over in production. What's changed nowadays is that actually there's a really nice range of solutions that now do exist for teams to be able to build on this architecture. There are very cool products and companies like Convex, which is like an end-to-end reactive database with this reactive programming model. You have Ditto, which is a very mature more edge sync engine for offline apps.
You have Instant, which is another reactive database system. You have LiveStore, which is a really good sync engine based on an event source change flow. You have Jazz, which is another very cool reactive database system with peer-to-peer end-to-end encryption. You have PowerSync, which is quite a mature core Postgres sync engine with a syncing into SQLite in the client. You have RxDB. You have Zero. Zero is a very cool system built by a company called Rocicorp. They built something called Replicache previously. They do very advanced incremental view maintenance and query-driven sync to get the right data into the client. These systems, they now work. You can just reach for them today and build these end-to-end reactive apps with them.
There is a catch, which is that actually they all come with this red pill. Because to solve the complexity of all the distributed system stuff and make things actually easy to use, they've had to build opinionated greenfield stacks. They have to be your one true stack for building your application. I don't know about you, but not everybody either can or wants to use somebody else's stack. I don't particularly like being told what to do, and I like my technology choices. I don't know about you, but maybe you like your technology choices, and maybe we disagree. A lot of people or companies have investments in existing software systems that you can't just replace and rewrite. Also, then you have lock-in, if it's somebody else's greenfield stack as opposed to the simple technology components that you would maybe choose to compose a stack out of.
Like we were looking at Vercel earlier. Vercel is a super compelling platform, but it is also a Faustian pact. That sweet Next.js caching does not run so well on other people's infrastructure. There's a whole bunch of lock-in just built into the choice of the platform. As a tech lead, as a CTO, you are looking at a choice. Do you build your own sync system and spend all your innovation tokens and get woken up at night? Or, are you using somebody else's, and you have to rewrite your systems to use it, and you suck up the lock-in? Most teams historically have looked at that choice, and they have taken the Cartman option. Sensibly.
Long story short, what we, what the whole world has really needed is an off-the-shelf solution for end-to-end reactive state management across client and server that works with your stack, your existing stack or your preferred technology choices, and that doesn't lock you in to a particular system or infrastructure provider. That is what we've built with Electric and what I want to show you today.
Electric, and TanStack DB
Firstly, at Electric, our core product is the Electric sync engine. It does this part of you change data in your database, and we reactively sync it into the client. Technically, it does read-path sync with partial replication and fan-out. It just gets the subset of data that you want to sync to that client, and then it fans it out. We actually do the data delivery through an HTTP protocol that runs through existing CDN infrastructure like Cloudflare and Fastly. Importantly, it does it through your stack. In this diagram, that proxy is your API or your edge worker, depending on your network topology, and that Postgres is your database. That can be hosted wherever you want. That can be AWS, Neon, Supabase. It's any standard Postgres as long as it supports logical replication.
Then the sync protocol works with any web stack that speaks HTTP and JSON, which obviously is every web stack. Electric has this HTTP API. Then you have low-level clients like this core TypeScript client. This resiliently syncs data into your client application via your API. Then, this can be used to sync data into anything you like. There's a simple protocol. There are clients for lots of languages. You can build your own clients. This Electric sync engine takes away about half of the complexity of building these sync-based reactive end-to-end apps. Then the other half is things like having sync-aware reactivity in the client, strategies around how you do the data loading, retention, and importantly, writes. You want this model where you can do local writes that display instantly and then sync back to the server in the background. For that, we built TanStack DB.
TanStack is a very popular infra-vendor agnostic. It's a project that's maintained. It's an open-source community. There's about 30 maintainers. It's stewarded by this guy, Tanner Linsley. It's not sponsored by or owned by any one company. It is an app development framework. It's probably number two in the world to Next.js right now. It has a lot of momentum behind it. You can see the download numbers here. It just passed 4 billion downloads. TanStack grew out of a library called React Query, which is now called TanStack Query. TanStack Query gives you things like managed queries, caching, and mutation primitives when you're building API-based applications. This code here is idiomatic TanStack Query code. It loads data into a React state variable using a managed query function. The function here defines how you fetch the data, like, in this case, calling your API.
Then TanStack Query basically manages calling that function, retrying, caching. Then when it comes to writes, you create a mutation using this useMutation hook. Again, you have a mutation function that defines how you want to handle that. Again, in this case, just post it to your API. Then you can use this mutation in your components to make local optimistic writes. This is how TanStack Query works. This is how a lot of apps are currently built. It's canonical frontend code. LLMs are used to writing it. What actually do we need to add to this to extend the reactivity to the server? We need to change from fetching the data using this managed query primitive to syncing the data reactively. That means we need a primitive to sync into, which we call a collection. Then we need the local mutations to apply optimistic state to this collection primitive.
Then we need to handle the writes by syncing them in the background. We need to tie the optimistic state lifecycle in with the sync machinery. We need to be able to build views on the sync data in the client, which means you need live queries across the collections.
Just to recap. You need a collection primitive. You need sync. You need live queries across collections. You need a way of handling these local optimistic writes that not only works with the write-path sync, but also monitors the read-path sync in order to automatically discard the local optimistic state. If you have this, then you have a fully working end-to-end sync stack built natively into TanStack that works with any web stack. That is TanStack DB. It is a fully reactive, sync-aware, client-side database. It provides this collection primitives. It provides live queries. It provides transactional optimistic mutations that tie into the sync machinery. It can be used with TanStack, or it can just be used standalone in any app. It's very lightweight. It's literally a 20-kilobyte JavaScript dependency. This is not some 4-meg WASM download to put a database inside your app.
This is something that you can just put into existing code and adopt one root, one component at a time. The key idea is that it allows you to incrementally migrate existing cloud-first applications which are built on APIs and querying, for example, using React Query, to query-based applications using TanStack DB to sync-based applications using TanStack DB, all the way to a fully local-first architecture that is reactive, consistent, offline-capable, and, as we'll see, insanely fast.
How TanStack DB Works
Let's dive in and see how it works. You start by defining a collection. We have some imports here at the top from TanStack DB and Query. We create a queryClient, which is a normal thing for TanStack Query. Then we use these to define a collection. In this case, we are defining a query collection. This loads the data using a managed query function, which is just code we've seen before. The change to your application to use this for a React Query developer is minimal. Then when you have the data in the collections, you can bind it to your components using live queries. To write the data locally, you can define handlers in your collection for just the simple insert, update, and delete operations, where you decide how to send the write to your backend, in this case, again, just like send it to your API.
Then in your component, you can just make local writes directly to the collection. Then, that's the simple case if it's just I just want to make individual inserts and updates, which is common across your app. If you have more complex writes, you can define custom actions. You can also define custom transactions, where the onMutate handler here updates the local optimistic state. In this case, it's just a simple insert, but it could be multiple operations across multiple collections, all done at the same time atomically. Then the mutation function in the action handles sending it to the backend again. In this case, just post it to your API. It then calls refetch on the collection. Then to use your actions in your components, you just call it. That is the end developer experience for working with data in your client-side app.
All the writes made are instantly applied locally. There's the background sync and the optimistic state lifecycle, and any necessary transactional rollbacks is all just handled for you.
That is the migration pathway from React Query to TanStack DB. TanStack DB is not only designed to work with queries. It's also designed to work with sync. The way you do that is instead of defining a query collection, like we saw before, you define an electricCollection like this. The data syncs in through your API, as we saw before, using that low-level Electric TypeScript client under the hood. It is worth emphasizing, so this is an Electric-backed collection, but anyone can implement collections for TanStack DB. Many others have. There's like PowerSync, RxDB. Then to query the data, the component is just exactly the same as before. Components don't need to know where the data came from, which is what gives you this separation between LLM written application code and actually how the programming is done across the network. Components just work against the local collection.
Except now that we've defined two collections, I can show you we have like a query collection, and then we defined an electricCollection, where you can query across them both using joins across collections. You can query across as many collections as you like, and they can be collections of different types. You can load some data from an API, some data from your database through a sync engine. You can have a local collection for UE state, and they're all part of the same logical model in the client. The thing is you can have as many complex queries with joins and aggregates, because the query engine is based on a TypeScript implementation of differential dataflow.
When the data changes, you're not doing this thing where you're having to say, go, the data's changed, so I will rerun the query against the database, which can take like 10, 20 milliseconds depending on the complexity of the query. Then you have 10 components all doing that, and suddenly it takes like 300 milliseconds. Then your application grinds to a halt. In this case, you're just propagating the deltas. When the data changes, everything can just re-render sub-millisecond, and you can have as many complex queries as you like across as many components, and it all stays within that single animation frame. There's an example here of a very simple benchmark we did where these are three different queries.
They're all running multiple joins across multiple tables with many thousands of rows with grouping and aggregates, and Basic JS is just doing that thing where you load the data and do it in raw JS loops, which is the dirty approach to building apps. SQLite here is when you're having reactive live queries but actually rerunning the queries when the data changes, and TanStack DB is just everything is fractions of a millisecond.
We've seen that the component code stays the same, but that the queries are actually super powerful under the hood, and the same is true of the mutations. You've switched your application to sync, but the component code is untouched. In your mutation function or operation handler, all that you need to do is have your backend return the transaction ID from the database that the changes were written under, and then the system can just monitor the sync stream for that transaction and automatically discard the optimistic state when the transaction streams back. If it errors for any reason, the local transaction is rolled back. This is both simple and pragmatic and also quite sophisticated and powerful. It's simple and pragmatic in the sense that it's server authoritative and you have rollbacks. It's simple and you sidestep nearly all the complexity with this local-first CRDT stuff.
Because you're matching on the transaction, it allows the local optimistic state to be rebased on top of concurrent transactions from other users, even if they're made to the same row. That's the basics on how TanStack DB works. There is a lot of expressivity in the live query and mutation systems. It's worth checking out the docs. You can load data from basically any source. It is this unified client store for building apps on sync. When you are building apps on sync, you have this key challenge of what data you sync into the client, this thing around data loading. With the examples I've shown you so far, these will basically sync all the data when the collection is created.
Expressivity of Data Loading
Now let's imagine that we make this a little bit more realistic. It's a projectsCollection that we sync through API projects with auth credentials in the header. What is actually going to happen there is your API or proxy is going to take the details in the request, which is like the path component, projects, and the authorization header, and turn it into something a bit like this SQL query. Which is then going, I know I want to select from projects and I took the auth header and I mapped it to a user ID, and now I'm filtering that query by users. When you establish a subscription, it can already be filtered so that you're syncing the data that that user is allowed to see or that you want to access.
However, what if you've got lots more data than the user is allowed or might want to access? It's fine if you've just got a small application, you just sync all the data in the database that that user can see. There are many applications where you can't. If you've got 35,000 issues in the React issue base, you're not going to sync all of those straight away onto the app. Equally, one of the common patterns is when people have quite interrelated data models. Say you're building something like a CMS or a CRM, and someone navigates around and you open up like a case or, in this example here is like a AI meeting notes that you would attach to a meeting record.
Imagine you've got all of those video files in your database, you're not going to sync all of them ahead of time, you're going to sync in the files when the user navigates to that part of the application. How do you achieve that expressivity of data loading, which is quite straightforward with just like loading for roots with standard fetch with a sync-based system? The answer is, as of the latest release in TanStack DB, TanStack DB now supports query-driven sync. I'll explain it, but this is an absolute game changer in terms of capability for this type of sync system. You don't have to take my word for it. Just under that tweet, that's Joe Savona, the famous React and Relay developer at Meta. "This is 'kicking myself we didn't build it' good." This is Sunil Pai, who we saw earlier talking about it. This is Tanner Linsley.
Query-driven sync is going to be transformational. It's changed his perspective on state management, it's what he only dreamt of being possible when he started building React Query.
How does it work? You add one line to your collection definition, specifically this one, which is syncMode on-demand. Then what happens is the query predicates from your live queries, which in this case is the project ID, which is a React state variable. It's passed into the where clause in your live query. This is your component code, and you're just saying, this is what I currently want to display in this component, and that projectId in this example is passed in as a prop. It could be coming from the URL. It could be coming from some other useState user input. Then, every time that variable changes, the demand for that query predicate, that project ID, gets pushed down to the collection. Then the collection can use it to fetch or sync the additional data.
For a query collection, it results in a fetch, basically like this, where the predicate gets just turned into like a query parameter. Whereas with an electricCollection, it results in a snapshot request, which looks like this. What this does is it basically takes the original shape definition, in this case like select all files belonging to a workspace, and the subset query, which is derived from that query predicate, and it ANDs them together. The results which match both of those query predicates can be added into the set of data that is synced into the client. The original subscription, which we called a shape, becomes the outer boundary, and then you can just progressively sync multiple subsets of the data at runtime as long as they fit within the shape. They are naturally de-duplicated, and rows are put into the right position in the log for consistency.
Here, the outer boundary is the data that you could sync, or you'd be authorized to sync into the client, and then the colored circles are the data that is actually synced onto the client, dynamically at runtime in response to user demand. If your live queries are ordered, then your subsets will be too. This gives you pagination, infinite scroll. It gives you all the flexibility that you need to be able to craft good UX while still keeping the actual fulfillment of the data loading out of the application code.
Demo
Shall we have a quick look at a demo? I'm just running this locally. This is a demo. We've had a demo for a while called LinearLite, it's like a linear clone just built on the stack. We recently just changed it to be LinearLarge, where we just load loads more data into the database, and we use these new capabilities to just optimize the data loading and show how they work. If I just pull it up. This is like 500,000 rows, and that was me just literally loading the page. You have 500,000 issues in the database here, and the application just loads instantly. I know it's coming off the local data, but even so, like loading that amount of data out of the database would just take many seconds, like 10, 20 seconds normally.
The way that it does that is it's just fetching a small subset of the initial data, and then as I'm scrolling here, it's filling in the data under the hood, which is just normally how you would do infinite scroll, but in this case you're able to do this infinite scroll behavior with this sync engine. As I go to the top here, for example, I can just change the filter predicates, and as you can see, the number of issues may be changing in the top there. Suddenly we're just drilling down to like these filters now only apply to 25,000 issues, and the ones that have come in the page, it's loaded the additional ones to fill in the amount of data that I want to be seeing.
As you navigate around this app, just naturally everything is instant, just despite the sheer amount of data actually in the application that you could sync into the client. Then, if I maybe just pull up the network inspector, you can just see what I was showing with these requests under the hood. Currently record, I'll just refresh the page. Again, that was just the page loading, including the overhead of the browser console being open. If I just clear this so we can see things a bit better, some tip-tap stuff going on. As we drop these down, you can see that you get these subset requests coming in, or at least we should do what's coming in here.
This demo works with query-based sync or Electric-based sync, so I was going to show you the subset request, but here I can just show you that basically those predicates are put into the query parameters, as I was showing. You can see here, for instance, you have the sort order, and then you have, like in this case, it's like priority high, and then that's the request that the query collection is making to fulfill the additional data at runtime.
This shows you the type of application you can build. You can have a lot of data, and you can just load the application instantly, or the data can always be synced in in advance. You get nice stuff where you can use the root loader to preload queries, and the live queries as people are navigating around the app, so you get the thing where the user hovers their mouse over a link, and you get 300 millisecond grace to run the preloader, so by the time they open the page, the data's already there. You tie this ongoing predicate-based data loading into the UX of the application.
Recap
Everything with this model is instant on the client. It's instant reads. It's instant writes. It's sub-millisecond live queries. Everything is reactive and real time across the stack. You have this flexible expressive data loading wrapped up into query-driven sync. The resulting UX is blazing fast. It solves this need for speed. It is the best way of building agentic systems, of keeping humans and agents in the loop, because everything is real time reactive. It stops the LLMs creating this big ball of spaghetti, because after all, why would you fetch when you can sync? These are the project links. The QR code has a bunch more things like blog posts and references. If it's interesting to dig into the tech, check it out. This is all just available. It's all open source. It's all documented. Build something, tell your team, tell your friends, tweet about it.
Questions and Answers
Participant 2: How does the server communicate with the client when the change has happened on the server side?
James Arthur: You write changes to your database, Electric is monitoring the logical replication stream from the database, and then it basically serves data to the client. The actual sync, like you'd typically think it would be done with WebSockets or that type of thing, and that's often how these systems are built. We deliberately built an old-fashioned long polling-based protocol, because it allows us to do the data delivery through existing CDNs. What happens is the client is making requests into the server, those are proxied through your API, so you can do authentication, authorization, and then these requests come in as long polling requests to Electric. Then as soon as data arrives in Electric, the long poll request returns and reconnects. That's how the core sync protocol works. The key thing is then, it means that what we're actually doing is just serving HTTP responses.
For the initial data load, those can be cached in the CDN, but for the ongoing live data, you get this really cool behavior called request coalescing in the CDN layer. We have this shape primitive for partial replication, and it's just a web request with a URL. If you have multiple clients connecting to the same URL at the same time with the CDN, it just collapses them down to the single request to the origin. What we'll have is, say you have a whole bunch of users all in the same chat session or they're all in the same workspace, they're all subscribing to the same URL, but you only get one request coming through to the sync service, which just takes the traffic away from it. My co-founder is Kyle Mathews, who is from Gatsby.
Gatsby did a lot of this data delivery stuff through CDNs, and so he brought a lot of his experience on how to actually just more reliably and scalably do this kind of data delivery, moving away from WebSockets and other fancier tech, because they are just more stateful, harder to scale, and we could just offload everything to the CDN.
Participant 3: You mentioned during the talk that you do read-path synchronization. Why not also do write-path synchronization?
James Arthur: Read-path synchronization is something every app needs, and you need a sync engine to do it. Now with writes, actually many applications are different. Solving write-path sync in the general case is extremely hard, and we actually tried to do it with an earlier version of the product, and we're trying to hold on to lots of very complex guarantees like finality of local writes and referential integrity and stuff. Actually, what we found is if you just look at things on an application by application basis, you can often just choose the tradeoffs that you would want to make. Some applications are just totally fine with last write wins or server authoritative writes. Other applications might be trying to do something more complex with real-time presence and conflict resolution and commutativity and stuff.
What we did was we just deliberately, with the Electric sync engine, we pushed it out of scope to say, look, we sync into the client, and you can just choose to write back to your database any way you like. The baseline solution is like just have online writes. You don't actually have to do any of this fancy offline optimistic writes at all. Then we documented a whole range of different patterns for going from online writes to optimistic state to more comprehensive optimistic state to through the database sync. We documented a lot of those, and we saw what people were using. There was just this very clear desire path around the model that we've then implemented into TanStack DB. With the Electric and TanStack DB stack, we do have some machinery for supporting write-path sync.
It's just that it's built around this principle that you can still actually choose how you want to save the write. We're not doing some sort of like we write it into the database for you. You could send a change to your backend, and then that change could just be a blind update in the sense that you're going this row, this is the new value. Or you could be sending an operation, like increment this counter or do something, and your backend can choose what to do. Your backend could take an instruction and make 16 writes. It's totally up to you. Then all that happens is you write them to Postgres, we pick up on the changes, any changes get synced out. This thing of like read-path sync is something that should be generalized and built into a core sync engine.
Actually, write-path sync is there's such a diversity in the solution space, it was much better to come up with a more agnostic mechanism where you choose how you do the writes, and we just still tie it into the optimistic state machinery.
Participant 4: When you were answering his question earlier, you mentioned long polling for getting the updates. If there's a scenario where you need the users to be updated instantly, do you have a way to use WebSockets or something that's more instant?
James Arthur: The long polling mechanism is instant, because it's not just that the client is just polling every second or something. What happens is the client is always connected, and they're just holding the HTTP connection open. The instant that data arrives, it comes back. You do get with long polling an overhead of then the client has to reconnect in order to consume the next changes, so that if you have some high throughput, that can lead to some almost buffering of changes on the server. You are still fulfilling it as fast as the client can consume. For cases, things like presence and token streaming, Electric also supports a server-side events mode, which just eliminates that buffering while still using the HTTP protocol.
We do either just like pure long polling, or if you have a particular stream that is sensitive to that high-frequency small message throughput, then you can switch it into an SSE mode. Then you just get a longer continuous stream of data, and then it reconnects every two minutes or something. It is very counterintuitive that you think that you'd want WebSockets to achieve this real-time data, and just like actually it works better to not have the WebSockets with a stateful protocol. The client is aware of maintaining an offset and a cache-busting cursor, just like two little parameters, and there just is no state. Everything's stateless, which just massively simplifies scaling.
Participant 5: I was curious if you have seen any patterns where the local state that gets updated has to be reverted because of errors, and how to handle that kind of scenario?
James Arthur: Yes, absolutely. There's a big world of that. There's quite a variety of patterns. I don't know if you're familiar with Ink & Switch, who've done a bunch of the thinking around a lot of these challenges around local-first software, and they defined the local-first software manifesto. They've looked at these different modes of collaboration or potentially concurrent writing, and in some things you want automatic conflict resolution, and some other cases you might want to push messages to the user. It's more like a Git mode of manual conflict resolution. TanStack DB just pushes that out of scope, and it's available for you to deal with. What happens is you apply optimistic state, the change is sent to the server. If the server rejects it, you remove the optimistic state, but there's also then a hook for you to just do stuff.
I think one of the realities around this is that for most applications, that just straightforward server authoritative with rollback is very simple, and it just does the job in nearly all cases. It is only like more sophisticated real-time things like text editing or some more low-latency real-time collaboration stuff that tends to have problems where you go into the CRDT space. What you can do with this Electric and TanStack DB stack is, as I was saying, you can just send a write to the server, and if you want to treat that as a logical change operation that you write into a CRDT structure, you can. A CRDT could just be like an array, and it's literally rows in the database are like a growing array. Or you can use pg_crdt inside Postgres. We also have like Yjs and Automerge integrations with a Yjs provider.
If you want to put CRDTs into the data model to better support concurrent editing of the same data, you can do, but the vast majority of apps just typically don't need it. It's a massive area. As I was saying, we don't think that it's our job to solve that, because if we solved it, we would be prescribing a whole bunch of choices and tradeoffs for you. It's just much better to actually be able to bite those off yourself and go, what am I building? Am I just building a simple CRUD interface, or am I crafting some highly multiplayer editing experience, and therefore just implement the best solution for the application?
Participant 6: I'm thinking about a potential race condition with long polling. Once the request returns, I imagine you have some machinery to make sure that while the next request is going out to establish the long poll, that there wasn't an event or an update that was missed?
James Arthur: On a single shape stream, then it is just sequential, like you can process those updates, go back, get more. You do have this thing where if you're trying to maintain referential integrity or integrity between multiple shapes, you can choose to consume multiple streams and make sure that you've got the updates for all of them for a transaction before you apply the data. There isn't a race condition for the single subscription, but if you're doing this across multiple tables, then you can choose to put these constraints on, which will maybe just cause things to wait a little bit. One of the things we do is that TanStack DB as a client store doesn't have any formal concept of referential integrity, but you do the joins like in the live queries.
That typically means that you can be very tolerant to scenarios where you've synced a comment, but the blog post hasn't arrived yet, because your live queries will just be like, the join doesn't match and there's no data. It'll only actually match when both of the data is there. It's interesting, again, that the things you almost think you need when you do something like sync into a SQL database with foreign keys enabled, you actually can sidestep in a lot of cases. There are cases where if you want that referential integrity, it's a little bit complex to be able to provide it. We also developed this project called PGlite, which is like a Postgres that you can run in the browser, and we have a sync adapter for it.
That has, for instance, a sync multiple shapes to multiple tables method, which deals with those concurrency challenges, because you want referential integrity.
See more presentations with transcripts