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