16:14Yeah.
16:15So I think the simplest pattern for
writes with an application would be to
16:19just, for instance, send a write to a
server and require you to be online.
16:24So, because there's many applications
that are happy, for instance, with read
16:27only, like there's a lot of people who
are building, data analytics applications,
16:31data visualization, dashboards, et cetera.
16:33And so if you have a sort of read
heavy application, then in some cases
16:37it may just be a perfectly valid
trade off, not to really deal with the
16:40complexity of say offline writes at all.
16:42But you still have a lot of benefits by
having local data on device for the read
16:46path, because all the way you can kind of
explore the application and the data is
16:50all just instant and local and resilient,
then the sort of simplest pattern to
16:56layer on, support for offline writes.
16:59On top of that as a sort of starting
point where imagine that you have like a
17:03standard REST API and you're just doing
put and post requests to it as normal is
17:08to add this concept of optimistic state.
17:10So optimistic state is just basically
you're saying, okay, I'm going to go and
17:14try and send this write to the API server.
17:16And whilst I do so, I'm going to
be optimistic and imagine that
17:20that write is going to succeed.
17:22And in two seconds later, it's going to
sync back into the state that I have here.
17:25But in the meantime, I'm going to Add
this bit of local optimistic state to
17:30display it immediately to the user, and
because in most cases that of happy path
17:34is what happens, then you end up with
what just feels like a perfect local-first
17:39experience because it's an instantly
displayed local write, and that sort
17:43of data is resolved in the background.
17:45Now, You know, immediately with that,
you do then just introduce like a layer
17:49of complexity with like, well, what
happens when the write is rejected?
17:54And so you have both the challenge of, for
instance, say you stacked up three writes.
18:01Did they depend on each other?
18:03So if one of them is rejected,
should you reject all of them?
18:06and different applications and different
parts of the application would have
18:09different answers to that question.
18:11In some cases, like it's very simple
to just go, if there's any problem with
18:14this optimistic state, just wipe it.
18:16And for instance, like the React use
optimistic hook, like its approach is just
18:20like, it waits for a promise to resolve.
18:22And when the promise resolves,
it wipes the optimistic state.
18:25And so it's very much just like,
if anything happens at all,
18:28it's like, And so it's only.
18:30Interestingly enough, there's also a lot
of people coming from React Query and so
18:35on, from those sort of more traditional
front end state management things.
18:40and that brings them to local-first in
the first place, because they're like
18:44layering optimistic, one optimistic
state handler on top of the next one.
18:49And if there's a little flaw inside
of there, everything collapses
18:53since you don't really know have
principled way to reason about things.
18:57So that makes a lot of sense.
18:59Exactly right.
19:00And so like a framework like TanStack, for
instance, with TanStack query, it has like
19:05slightly more sophisticated optimistic
state primitives than just say the kind
19:10of a primitive use of optimistic hook.
19:12And one of the thing, one of the
challenges that you have is that for
19:15say, a simple approach to, to just using
optimistic state to display an immediate
19:20write is like, is that optimistic
state global to your application?
19:24Shared between components?
19:25Is it scoped within the component?
19:27And so, as you say, like there's an
approach where you could come along
19:30and say, okay, I've got three or four
different components and so far I've
19:33just been able to sort of render the
optimistic state within the component.
19:37But now I've got two components that are
actually displaying the same information.
19:40And suddenly I've got like stale data.
19:42It's like the old days of manual
DOM manipulation and you forgot
19:45to update a state variable.
19:47And so.
19:48Yeah, in a way that's where you come
to a more proper local-first solution
19:53where your optimistic state would be,
stored in some sort of shared store.
19:58So it could just be like a
JavaScript object store, or it
20:01could be an embedded database.
20:03And so you get a slightly
more sophisticated models of
20:07managing optimistic state.
20:08And the great thing is there are, like
TanStack Query and others, there's
20:11like, there's a bunch of existing
client side frameworks that can handle
20:14that kind of management for you.
20:17Once you go, for instance, like to
an embedded database for the state.
20:21So one of the kind of really nice, points
in the design space for this is to have a
20:27model where you sync data onto the device
and you treat that data as immutable.
20:32And then you can have, for instance, so,
so say, for instance, you're syncing a
20:37database table, say it's like a log viewer
application, and you're just syncing the
20:41logs in, and it goes into a logs table.
20:44Now, say the user can interact
with the logs and delete them,
20:47or change the categorization.
20:49And so you can have a shadow logs
table, which is where you would
20:52save the local optimistic state.
20:54And then.
20:55You can do a bunch of different techniques
to, for example, create a view or a live
20:59query where you combine those two on read.
21:02So the application just sort of feels
like it's interacting with the table,
21:05but actually it's split in the storage
layer into a mutable table for the sync
21:09state and a kind of local mutable table.
21:12And the great thing about that is you
can have persistence for the, both the
21:15sync state and the, local mutable state.
21:18And of course it can be shared.
21:19So you can have multiple components,
which are all sorts of just going
21:22through that unified data store.
21:24and there's some nice stuff that you can
do in SQL world, for instance, to use
21:27like instead of triggers to combine it.
21:29So it just feels like you're
working with a single table.
21:32Now it's a little bit additional
complexity on something like defining
21:35a client side data model, but what
it gives you is it gives you a
21:39very solid model to reason about.
21:42So like, You can go, okay, basically
the sync state is always golden.
21:46It's immutable.
21:46Whenever it syncs in, it's correct.
21:48If I have a problem with this local state,
that's just, that's like mutable stuff.
21:53Worst case, I can get rid of it, or I can
develop more sophisticated strategies for
21:57dealing with rollbacks and edge cases.
22:00So it in a way it can give you
a nice developer experience.
22:04with that model, you could choose then
whether your writes are, whether you're
22:08writing to the database, detecting
changes, and then sending those to
22:11some sort of like replication ingest
point, or whether you're still just
22:15basically talking to an API and writing
the local optimistic state separately.
22:21So, so at that point you can have,
again, you can have, you have this
22:24fundamental model of like, Are you
writing directly to the database and
22:27all the syncing happens magically?
22:29Or are you just using that database as a
sort of unified, local optimistic store?
22:34So this is the sort of type of
like progression of patterns.
22:36And once you start to go through something
where you would, for instance, have a
22:42synced state that is mutable, or you
are writing directly to the database,
22:46that's really where you start to get a
little bit more into the world of like
22:49convergence logic and kind of merge logic
and CRDTs and sort of what's commonly
22:54understood as proper local-first systems.
22:57And I think that's the point where
almost the complexity of those
22:59systems does become very real.
23:01Like, as you well know, from building
LiveStore and as we see from the
23:04kind of, quality of libraries
like AutoMerge, Yjs, et cetera.
23:08so that's probably where as a developer,
it makes sense to reach for a framework.
23:12And you certainly could reach for
a framework for that sort of like.
23:15Combine on read, sync, sync into a mutable
kind of persist local mutable state.
23:21But what we find is that it is
actually if you want to, it's actually
23:25relatively straightforward to develop
yourself, you can reason about it
23:28fairly simply, and so it's not too
much extra work to just basically go
23:32as long as you've got that read sync
primitive, you can build like a kind of
23:36proper locally persistent, consistent
local-first app yourself, basically.
23:42Just using fairly standard
front end primitives.
23:44Right.
23:45Okay.
23:46Maybe sharing a few reflections on
this, since I like the way how you,
23:50portrayed this sort of spectrum of
this different kind of write patterns.
23:54in a interview that I did with
Matthew Weidner, I learned a lot there
23:58about the way, how he thinks about
different categorizations of like state
24:02management, and particularly when it
comes to distributed synchronization.
24:07and I think one pattern that got clear
there was that there's either you're
24:12working directly manipulating the
state, which is what like Automerge, et
24:16cetera, are de facto doing for how you
as a developer interact with the state.
24:21So you have like a document
and you manipulate it directly.
24:25You could also apply the same logic of
like, you have a Database table, for
24:30example, that's how CR SQLite works,
where you have a SQLite table and you
24:35manipulate a row directly and that is
being synchronized as the state and
24:41you're ideally modeling this with a way
where the state itself converges and
24:46through some mechanisms, typically CRDTs.
24:49But then there's another approach,
which might feel a little bit more
24:53work, but it can actually be concealed
quite nicely by systems, for example,
24:58like LiveStore, in this case, unbiased,
and where you basically separate
25:02out the reads from the writes.
25:05And often enough, you can
actually fully, re compute your
25:10read model from the write model.
25:12So, if you then basically express
everything that has happened, that
25:16has meaningfully happened for your
application as a log of events.
25:20Then you can often kind of like how Redux
used to work or still works, you can
25:24fully recompute your view, your read model
from all the writes that have happened.
25:29And I think that would work actually
really, really well together in tandem
25:33with Electric, where if you're replicating
what has happened in your Postgres
25:39database as like a log of historic events,
then you can actually fully, recreate
25:45Whatever derived state you're interested
in and what is really interesting about
25:49that approach, but that particular write
pattern is that it's a lot easier to
25:54model that and reason about that locally.
25:57Did you say like, Hey, I got those
events from the server, those
26:00events, I am applying optimistically.
26:03You can encode sort of even a causal
order that doesn't really, If someone
26:09is like confused about what does causal
order mean, don't worry about it.
26:13Like you can probably at the beginning,
keep it simple, but once you layer
26:18on like more and more dependent,
optimistic state transitions, this is
26:22where you want to have the information.
26:25Okay.
26:25If I'm doing that, and then the other
thing depends on that, that's basically a
26:29causal order and modeling that as events.
26:32I think is a lot simpler and is a way to,
to deal with that monstrosity of like,
26:38losing control over your optimistic state.
26:41Since I think one thing that's, that
makes optimistic state management
26:44even more tricky is that, like, how
are things dependent on each other?
26:50And then also like, when
is it assumed to be good.
26:54I think in a world where you use
Electric, once you're from the
26:57Electrics server, you've got sort
of confirmation, like, Hey, those
27:01things have now happened for real.
27:02You can trust it.
27:04but there's like some latency in
between, and the latency might be
27:07increased by many, many factors.
27:10One way could be that you just, you are
on a like slow connection or the server
27:15is particularly far away from you and
might take a hundred milliseconds, but
27:19another one might be your have a spotty
connection and like packages get lost and
27:25it takes a lot longer or you're offline
and being offline is just like a form
27:30of like a very high latency form and
so all of that, like if you're offline,
27:36if it takes a long long time, and maybe
you close your laptop, you reopen it.
27:41Is the optimistic state still there?
27:43Is it actually locally persisted?
27:45So there are many, many more
layers that make that more tricky.
27:49But I like the way how you're like,
how you split this up into the read
27:54concerns and the write concerns.
27:56And I think this way, it's also
very easy to get started with new
28:00apps that might be more read heavy
and are based on existing data.
28:05I think this is a very attractive
trade off that you say like, Hey, with
28:09that, I can just sink in my existing
data and then step by step, depending
28:14on what I need, if I need it at all.
28:16Many apps don't even need to
do writes at all, and then you
28:19can just get started easily.
28:21Yeah, I think, I mean, that's explicitly
a design goal for us is like, yeah,
28:25if you start off with an existing
application and maybe it's using REST
28:29APIs or GraphQL, it's like, well,
what do you do to start to move that
28:32towards a local-first architecture?
28:34And exactly, you could just go, okay,
well, just, let's just leave the way
28:37that we do writes the same as it is.
28:39And let's move to this model
of like syncing in the data
28:41instead of fetching the data.
28:43And that can just be a first step.
28:45And I think, I mean, Across all of
these techniques for writes, there
28:48is just something fundamental about
keeping the history or the log
28:52around as long as you need it, and
then somehow materializing values.
28:58So sort of internally, this
is what a CRDT does, right?
29:01it's clever and has a sort of lattice
structure for the history, but basically
29:05it keeps the information and allows
you to materialize out a value.
29:09if you just have like
an event log of writes.
29:11So as you were saying with, with
LiveStore, when you have like a
29:14record of all the write operations,
you can just process that log.
29:17so I think, you know, you can do
it sort of within a data type.
29:21And I think that fits as well for
greenfield application where you're trying
29:25to craft, kind of real time or kind of
collaboration and concurrency semantics,
29:29but like from our side of coming at it,
from the point of saying, right, when
29:32you've got applications that build on
Postgres, you already have a data model.
29:35You just sort of layer the same kind of
history approach on top by like, keeping
29:39a record of the local writes until you
of sure you can compact them and actually
29:44that same principle is exactly how the
read path sync works with Electric.
29:49So Postgres logical replication, it just
basically, it emits a stream, it's like
29:56transactions that contain write operations
and it's basically inserts, updates,
30:00and deletes with a bit of metadata.
30:02And so we end up consuming
that and basically writing
30:06out what we call shape logs.
30:07So we have a primitive called a shape,
which is how we control the partial
30:10replication, like which data goes to which
client and a client can define multiple
30:14shapes, and then you stream them out.
30:16But that shape log comes through our
replication protocol as just that
30:21stream of logical update operations.
30:23And so in the client, you can just, you
can materialize the data immediately.
30:28So like we provide, for instance, a shape
stream primitive in a JavaScript client
30:32that just omits the series of events.
30:34And then we have a shape, which we'll
just take care of materializing that
30:37into a kind of map value for you.
30:39but you could do what you want, whatever
you wanted with that stream of events.
30:42So if you found that you wanted to
keep around a certain history of the
30:46log in order to be able to reconcile
some sort of causal dependencies,
30:49that's just totally up to you.
30:51And so, yeah, it's quite interesting
that it's almost just the same approach,
30:54which is the general sort of principle
for handling concurrency on the
30:58write path is also just exactly what
we've ended up consolidating down on
31:02exposing through the read path stream.
31:04That makes a lot of sense.
31:05So, Let's maybe go a
little bit more high level.
31:08Again, for the past couple of minutes,
we've been talking a lot about like how
31:12Electric happens to work under the hood.
31:14And there's many commonalities
with other technologies and
31:17all the way to CRDTs as well.
31:19But going back a little bit towards
the perspective of someone who would
31:23be using Electric and build something
with Electric and doesn't maybe
31:28peel off all the layers yet, but get
started with one of the easier off the
31:32shelf options that Electric provides.
31:35So my understanding is that you have
your existing Postgres database.
31:40you already have your like tables,
your schema, et cetera, or if it's
31:44a greenfield app, you can design
that however you still want.
31:47And then you have your Postgres database.
31:50Electric is that infrastructure
component that you put in front
31:53of your Postgres database that has
access to your Postgres database.
31:58In fact, it has access to the
replication stream of Postgres.
32:02So it knows everything that's
going on in that database.
32:05And then your client is talking
to the Electric sync engine to
32:10sync in whatever data you need.
32:12And the way that's expressed what
your client actually needs is through
32:17this concept that you call shapes.
32:19And my understanding is that a
shape basically defines a subset
32:23of data, a subset of a table
that you want in your client.
32:28since often like tables are so
huge and you just need a particular
32:32subset for your given user, for
your given document, whatever.