00:00I feel like our bar as an
industry needs to be higher.
00:02And I, I honestly think that web
development is to blame for some of that.
00:05That's my spicy take because it makes
it easy for us to throw up our hands.
00:09And say, I can't go past
that layer of abstraction.
00:13Whereas in the native
world, you have a problem.
00:15You can dig into those, the C binaries.
00:19it might be hard, but you have the
power to go in and change things.
00:22Whereas on the web, it's
like, it just works that way.
00:25It sucks.
00:26Like too bad.
00:26I think we need aim, aim higher.
00:28Welcome to the local-first FM podcast.
00:31I'm your host, Johannes Schickling,
and I'm a web developer, a
00:34startup founder, and love the
craft of software engineering.
00:37For the past few years, I've been on a
journey to build a modern, high quality
00:41music app using web technologies.
00:43And in doing so, I've been falling down
the rabbit hole of local-first software.
00:48This podcast is your invitation
to join me in that journey.
00:51In this episode, I'm speaking to James
Long, the creator of the local-first app.
00:56Actual Budget and the absurd-sql
project, which helped to pave the way
01:00to bring back SQLite to the browser.
01:02In this conversation, we go deep on
his journey, building Actual Budget,
01:06including implementing a syncing
solution from scratch and expanding
01:10from an Electron app to mobile and the
web while reusing most of the code.
01:15Before getting started, also a
big thank you to Expo and Crab
01:18Nebula for supporting this podcast.
01:20And now my interview with James.
01:24Hey, James.
01:25So good to have you.
01:26Hey, Thanks for having me.
01:28So I've been a long time
fan of your prior work.
01:32I think this has really been like
the first time where I've seen an
01:36Actual local-first app pun intended.
01:39You've been working on the Actual
Budget app in the past, and which
01:43has led to quite a few, technical
innovations, particularly for the web.
01:48So that's what I'm looking forward to
exploring today, but I'm curious what has
01:53led you to exploring local-first and what
has led you to work on Actual Budget.
01:58Sure.
01:58So first of all, I'll just state
that Actual Budget is, uh, I
02:01don't actually work on it anymore.
02:03And I open sourced it about two years ago.
02:05And so the community has taken it
over and done a great job with it too.
02:08Um, but I started it around 2017.
02:11And back then I just really wanted
a local app, just the web back
02:16then was even worse than it is
now, just like development-wise.
02:19And I don't know, I, it
just, It didn't excite me.
02:22It's a personal finance manager.
02:23It felt like it should be local.
02:24It felt like I should be able to throw raw
SQL queries at it and get my data back.
02:30So it just felt like a very good fit.
02:32And using Electron back then was
amazing because I could just like
02:35load up SQLite and load a SQLite
database from a local disk and use
02:41the native SQLite C bindings, right?
02:43It was, it was fantastic.
02:44So I built a basic local app just
because I wanted it to be local
02:48and I just didn't want to deal
with like hosting it somewhere and
02:50I was like, this is just for me.
02:51This is just a fun thing
for me for certain apps.
02:53I've always liked the idea of it.
02:55Just like being super local.
02:56Uh, you, you own everything.
02:58It's not dependent on anything else and
that you just have raw access to the data.
03:02Obviously, at some point, I hit this
problem where it's like, well, shoot, if
03:05I drop my laptop, All my data is gone.
03:07Right.
03:07or like my wife just wants to check
one charge somewhere and she's on
03:11her laptop and she can't do that.
03:13And so, uh, eventually I was like,
well, crap, if I want to make a
03:16business out of this, especially I got
to solve the collaboration problem.
03:20And so that's what led
me down into syncing.
03:22And so I investigated a couple of things
and it worked out really, really well.
03:24And so I went down that path from
there and I just continually.
03:27Invested into it.
03:28And it was fun.
03:29That is super impressive, particularly
given that you've already started
03:32working on that in 2017, just for
reference, the official local-first essay
03:38by Inc&Switch, uh, came out in 2019.
03:41So it looks like you've been on the same
journey, uh, maybe aware, maybe unaware
03:46that other people have been also exploring
the ideas of local-first, and then you've
03:51just arrived on very similar conclusions.
03:54So I'm curious to learn more about
the technical challenges that
03:59you've been facing, really, uh,
innovating on so many fronts there
04:03to make this, this vision a reality.
04:06And I would like to better understand
also one aspect you've been mentioning
04:10that the, uh, App should work local.
04:12Are you referring to an app here as a
desktop app from something like Electron?
04:17Or are you also talking about a
more of like a progressive web
04:21app that runs also in the browser?
04:23So back then at the time, I was mostly
thinking just like a desktop app.
04:27I want to see the icon in the doc.
04:28I want, I wanted it to just be native.
04:31Native-ish.
04:32I know we're kind of faking that
with Electron, but I wanted just
04:35an app that I can click and I
open my finances and I search and
04:38then I do command Q and it's gone.
04:40So back then it was more a desktop app.
04:42And eventually I did port
everything to the web.
04:44And that was a whole nother thing because
it's really hard to compete with the web.
04:47I mean, just not only
distribution, uh, but just like,
04:51that's just where everybody is.
04:52So it's evolved a little bit more
to now mean probably Like when I
04:56say local app, it could totally be
a web app as well, or a mobile app.
04:59Like all of it should just be
something that can work locally.
05:01I think the web still needs to catch
up in a lot of ways to get there.
05:05But at this point, I, when
I say it, I mean like a.
05:07It could be a web app or a desktop app.
05:09So before diving a little bit more into
the nitty gritties of the technological
05:14choices that you've made, you've
already mentioned that you've been
05:17choosing SQLite for the data layer.
05:20So I think the, the web world is.
05:23Sometimes like divided across
front-end and back-end, I think using
05:28a SQL database is much more common
on the, on the back-end and on the
05:33front-end, I think you're more used
to things like Redux, MobX, et cetera.
05:39And given your use case of
an Electron app, this is sort
05:43of like a murky in between.
05:45So I'm very curious to hear
more about your intuition.
05:49What led you to wanting to use
a SQLite database that I guess
05:53in the realm of Electron rather
falls into the front-end realm.
05:59So I'm curious what led
that design decision.
06:01Well, like I said, I mean, for the data
layer itself, it was just so obvious
06:05to me that it's a small set of data.
06:07Some, some people worry about SQLite not
scaling well to like millions of rows.
06:11I think I have at this point, nine years
of my transactions in there and I, I'd
06:15have to count, but it's, I think it's
in the like tens of thousands, uh, aims.
06:18So this is like not a large set of data.
06:20SQLite would just seem like
such the perfect fit for it.
06:22But then once you have SQLite and it's
a local app and then you're writing in
06:27front of components, it seems silly to
have to design this API layer that's
06:32either like a WebSockets messaging layer
or like an HTTP like URL based API.
06:38Like it seems purely you're going
through the motions to have an API
06:43that literally just is intercepted
locally and then runs the data locally.
06:47So once and then like you get that.
06:49Data back as like JSON.
06:50So you, you query something with
like an HTTP or like web socket
06:53API, and then you get the data back.
06:55It's like you, why not just go ahead
and just like write SQL queries, right?
06:59In those components.
07:01It literally makes no difference
because it's intercepted locally.
07:04So then I started kind of,
Exposing SQLite even more and more.
07:07And I ended up coming up with my own
little data, data querying language,
07:11because when you're doing really
quick stuff, it is nice to, it's not
07:15an ORM at all, but it's basically
like a query builder type thing.
07:19It's, it's a pretty common thing
that I think a lot of people do.
07:21I think there's a library called Knex.
07:23K N E X that like helps you
build sort of like that.
07:26But I, I built it because I like
building stuff myself and it was, it
07:30was a pretty interesting thing, but it
does basically let you construct SQL
07:32things and then it hands that to the
backend and it knows how to execute it.
07:35The other thing that it knows how
to do is it knows how to do like
07:38live queries, which you don't get
with like raw SQL queries, unless
07:40you parse the query or something,
but it knows which tables to watch.
07:44And so as other syncing messages come in,
the data will just automatically update.
07:47And so, yeah, it just felt
like a really nice fit.
07:51Since everything is local anyway.
07:52That sounds super compelling.
07:54And I think we've arrived at a few
similar conclusions given that with my
07:59work on Riffle and LiveStore, I've also
built some similar aspects such as like
08:04the reactivity that you've pointed at.
08:06I'm curious, like how you actually
went about implementing that since
08:10for the listeners who might not
be as aware of all the SQLite
08:15internals, SQLite itself doesn't
really help you much with reactivity.
08:20There is a few hook points, but,
uh, you get to roll quite a bit
08:24of the stuff yourself on top.
08:26So I'm curious how you went about that.
08:28Yeah, that part.
08:29Honestly, it's not super innovative.
08:30I actually remember around 2017
when I started doing this kind
08:34of a stuff, I went really deep on
SQLite internals and there's a hook.
08:38I forget what it's called, but
it's like SQLite underscore pre
08:42post update or something like that.
08:44There's a hook that you get that gets
called whenever, whenever an update
08:47happens and you get like the data
before and the data after, but it turned
08:51out to just be a really weird hook.
08:53When you want to do live
reactive stuff, you, you want
08:55it to be somewhat fine grained.
08:57It doesn't have to be super fine
grained, but if, if you want A change
08:59happens, then the, the entire, like
all of the data on the app re renders.
09:03SQL is fast and it's
local, but it's still slow.
09:06If you're going to re render every
single time you change one little thing.
09:10And so you want to make sure that you'd
somewhat scoped to like the table that
09:13changed or something like that, at least.
09:15And that hook, like it
provided back like weird IDs.
09:19Like I could never figure out how
to actually get the data itself
09:22that changed, like this column.
09:24And this row changed from X
to Y it was like, I couldn't
09:28even get like that basic stuff.
09:30And maybe I was just doing it wrong.
09:31There are some hooks in
there that seem promising.
09:34But I think from what other people are
doing, they're using like, like the wall
09:37file to do like really interesting things.
09:39I think that seems like a
more reasonable approach.
09:41But overall, actually, honestly,
it's pretty, it's pretty basic.
09:45All updates have to go through an API.
09:47And so unfortunately, if you open up the
SQLite file locally, you can update it.
09:52But those messages will
not be synced around.
09:55You'll have to like reset the file
across all devices, which it's possible.
09:59And people have done that before, if they
really want to like mess with something.
10:02But, um, it's not a thing where
you can open up the SQL file and
10:05update things directly from there.
10:06You have to call the update function,
which then creates a bunch of like CRDT
10:11stuff, sends it out, and then actually.
10:13I take that back a little bit.
10:15You need to call the update function to
generate the CRDT, like the sync messages.
10:19And they go out to the server
and they get applied locally.
10:22It's the part of the pipeline
that applies the messages,
10:24which does, does the reactivity.
10:26That allows me to kind of
watch for changes, right?
10:28Because All mutations through the system,
even if they're just local mutations, go
10:33through this like CRDT messages system.
10:36And so you can see when column X
on table transactions changes, it's
10:41changing from this value and I have
the before value and after value.
10:45And so when those messages get pumped
through is when it fires off all
10:49of the listeners that are listening
for that small piece of data.
10:53And yeah, that's basically how it works.
10:55Got it.
10:55So you've captured most of that
and probably in, in JavaScript.
10:59And since you're all using, you're
building the library, you're building the
11:03app, you know how to use it correctly.
11:06So you've built a lightweight
query builder on top.
11:10How are you using that query builder then?
11:12Um, have you like wrapped
those in React hooks?
11:15Or how are you wiring up your data
into the UI directly at some point?
11:20Yeah, there's a useLiveQuery() hook,
and then there's a usePageQuery() hook.
11:25The page query one is interesting
because it allows you to like page
11:28in results, and it returns an object
that has like a dot next function.
11:32I mean, it just knows how to automatically
add the limit and, and, and offset.
11:35And it does some other
pretty fancy things too.
11:37And so they're just hooks
that Knows how to rerender the
11:40component when the data changes.
11:43Got it.
11:44I mean, uh, I've gone through a similar
journey of where I used like some
11:49state management libraries and react
front-ends in the past, and then,
11:53uh, actually being able to use and
embrace SQLite for all its benefits and
11:57the front-end is quite magical, like
little tricks that I found to be super
12:01compelling is that I can actually like.
12:03Touch the SQLite file, whether I can
like look at it and see the values being
12:08updated by the app, or also like go to
it and like delete it, reload the app.
12:13And I'm starting from scratch.
12:14Those little things, they're
just super compelling and make
12:17it super fun to work on the app.
12:19Is there some things like that, that
you found that just gave you like a
12:24really nice boost in your development,
like velocity and productivity and fun?
12:29Sure.
12:29Yeah.
12:29I mean, it's.
12:30It's totally true that it kind of
forces it to be like, if you're not
12:33using Postgres and you're using SQLite
instead, and they're all files, it's
12:37so easy to just like move things
around and create like a fresh file.
12:40The, the demo app literally
just copies like a demo.
12:42SQLite file into the
app's like database file.
12:46And if you're using Postgres, that
kind of stuff just gets a lot harder
12:49because it's so like process oriented
and like process based, you can't
12:52just easily do that kind of stuff.
12:53But like, and yeah, being, being
able to dig into things and do
12:56SQLite queries, it felt really fast.
12:59I think you, You know, if you're
developing the app, you have
13:01access to the Postgres database.
13:03So you can do that kind of stuff as well.
13:04But one of the things that was, does
come to mind is when I was building
13:08the Electron app, I had this strategy.
13:10And I think I wrote a post
about this, where there is a,
13:12there's actually two windows.
13:14One window is for the front-end and
one window is actually for the backend.
13:17So the backend does not run
in a, like just a node process
13:21that is, you know, Invisible.
13:23It actually runs in another Electron
window with like node integration set
13:27to true, and it can access node APIs.
13:30What's cool about that is
that I can open the dev tools.
13:32I expose a bunch of stuff
to the top level, and then I
13:35can like query around stuff.
13:37I can query the database directly.
13:38I can get objects back and
get it that really nice Chrome
13:41dev tools, like object viewer.
13:42Um, I can track performance.
13:44So I can click the performance
tab and click start.
13:46Start performance recording,
do a bunch of stuff on the UI.
13:49And then like, I can set,
um, stop the performance.
13:51You can do performance tracing.
13:52So you can actually start tracing
how long the SQL queries took.
13:55So being able to access the backend dev
tools or the Chrome dev tools for the
13:59backend, and like being able to interact,
like directly interact with, with the
14:03database was like super, super great.
14:05Yeah, that, that sounds amazing.
14:07So you've been using SQLite with Electron,
but you've already hinted at before
14:12that at some point you realize, okay, a
single Electron app is not quite enough.
14:18We also carry phones.
14:19We might carry other devices.
14:21Um, What if your, your
single device gets lost?
14:24What about all of your app state?
14:26So I'm curious at which point
you've then found your way to, to
14:31also implementing collaboration or
syncing and how you went about that.
14:35So I, I can't remember when it
was exactly, maybe 2018, 2019, I
14:40started looking into this and I.
14:43Went about it by just hearing things
that my friends were talking about
14:47that were kind of interesting.
14:48And so they were, they were doing like
Raft and some of those protocols in
14:52the backend and kind of just, they
were deep into that kind of area.
14:55And so I heard, honestly,
I almost gave up.
14:58I was like, this just, all of this
stuff seems way, way too complicated.
15:01I do not have time to do this.
15:02But then I ended up just poking
around to see if there was any
15:05possibilities of things I was doing.
15:07I had this like a really initial
implementation, which was super naive.
15:10I, there's a gist that I explained
it somewhere and it was like
15:13really, really easy to pick up.
15:15Pick it apart.
15:15Once I started implementing it and
like running a test against it, like
15:18there was like, I think it was sort of
operational transform based, but it just
15:22like was, it fell apart way too easily.
15:24And I was like, this is really hard stuff.
15:25And when I looked at CRDTs, like I
could never grasp what they were.
15:30Nobody really, I, back then, at least
it was just not a good, it was too much
15:33math, too much, very, very intimidating.
15:36And they're really not that hard.
15:37Like it does not need to be explained
that way, but something clicked at
15:40some point when I finally implemented.
15:42A basic last right winds map that
the thing that really unlocked.
15:46It was hybrid logical clocks.
15:47I can't remember where, like, when
I found that, but when I started
15:51looking into that and reading that
paper, uh, there's a simplicity about
15:54them that I found really compelling.
15:56And it really matched match my
own technical kind of approach
15:59for things, which is like.
16:01make things as simple as possible and,
and, and the least surprising as possible.
16:05And you get a lot of benefits from that.
16:06Now there are, there are drawbacks
to HLCs, but the benefit of them,
16:11especially for this use case
seemed like a really good match.
16:14And so I went off and, you know,
it was one of those things where
16:17like everything kind of came
together in a couple of weeks.
16:19And I started seeing some really
compelling success with that.
16:22And also the ability to unlock
things like undo and Yeah.
16:25Redo, because once you start using the
system that like mutates everything
16:29through these like messages, you can
suddenly start tracking those messages
16:33and you can invert the messages.
16:35So undo literally becomes take this
batch of messages that happened
16:39in the last action and invert them
and then apply those messages.
16:43And suddenly, Actual turned out to
have a really robust undo and redo
16:47system, which I'm super proud of.
16:49And like, work, like literally
everything that you do in Actual,
16:51you can press command Z to undo.
16:53If you import 2000 transactions and
it runs a bunch of rules and mutates
16:57them, press command Z and in like 500
milliseconds, or not 500 100 milliseconds.
17:03Um, everything will go
back to where it was.
17:05So I started seeing signs of
this architecture, which was
17:10like really exciting, and then
I just kind of went from there.
17:12So just as a side questions for, for those
of us in the audience who might not be
17:18familiar with hypological clocks, could
you give a quick explainer, uh, what the
17:22concepts are and what they're used for?
17:24Sure.
17:25So I'll try to be fast.
17:26I think we could probably talk
about this, uh, area of research
17:30probably for the rest of the time.
17:32Hybrid logical clocks is a way to
solve the coordination problem.
17:35So the problem with distributed systems,
which a local-first app is a distributed
17:39system, because you have copies of the
app and copies of the data across multiple
17:44devices, is you need to know when, if you
have something like a last write wins set.
17:48If two people write that
offline, then come back online
17:50to sync up, which one wins?
17:52So you, you, you have to have a clock
for every single mutation in the system.
17:56There's a vector clocks.
17:57There's a more advanced clocks that are
built on top of things like vector clocks.
18:00There's a lot of different approaches for,
to, to solve this kind of a problem, to
18:03say, which one came after the other one.
18:06To take a pretty simplistic approach
here, but it's still super robust.
18:09And the very short summary of it is
it actually, the neat thing about it
18:14is that it serializes into a string.
18:15Um, And so the comparison of if this
message came after or before is you just
18:19compare the string, like it says less than
or greater than the string is always the
18:24same length and so it's lexically ordered.
18:27So you can just say, if I wanted to
inspect my database and get the messages
18:31in order, I would say, select star for
messages and then like order by the.
18:35CRDT, HLC, and then everything
comes back ordered, right?
18:39Whereas like vector clocks and all these
other complicated ones are like complex
18:42object data structures that have to keep
track of the ID or the counts or whatever
18:46of every single device in the whole world.
18:48And you have to like name, name those
devices and, and, and do a lot of things.
18:52HLCs are a thing that Take the
current time, like the clock of the
18:57system, which sounds terribly scary.
18:59If you know anything about this
thing, like involving the clock
19:01of the current local computer, it
sounds awful, but they do it in a
19:04way that is really, really novel.
19:06So the first part of the string
is like the, the UTC, like that
19:10timestamp in the format, that's
like that Z at the end, right?
19:13That like, I forget what it is.
19:14And so it's that, it's that big
timestamp and then it's dash.
19:18And then there's like
a, a padding of bits.
19:21And then there's another like dash.
19:23And then there's the device ID,
I think, at, at, at the very end.
19:27And so the way that you get
deterministic order is that,
19:31that little bucket of bits in the
middle, that's the key there, right?
19:35So that bucket of bits
represents an integer.
19:38When you receive a message,
normally you use your local time.
19:41Well, that's not exactly true.
19:43You actually use your local time.
19:45Or the last highest time that you've
ever seen from the whole system.
19:50So if you are receiving a bunch
of messages, you're reading the
19:53times off of those messages.
19:54And you say, if that time is greater
than my time, so that laptop's
19:58clock is like one minute faster,
then I'm going to have a timestamp.
20:02One minute faster.
20:02I need to, I need to fast forward
my time to match that clock's time.
20:06Cause that was like that person's
clock is later than mine.
20:09But when you start doing that kind of
a stuff, you can't generate another
20:12message with the same exact time, right?
20:14You need to differentiate those
two messages locally somehow.
20:16And so you increment that.
20:18That number that's stored in
those bits in the middle by one.
20:23And so if things are generating with
the same timestamp, well, suddenly that
20:26timestamp is not ordering things anymore.
20:28Now it's the little set of bits in
the middle that are ordering things.
20:31And you can serialize them as like
a hex value, I think, or something
20:34like something that's still like a
string that can be lexically ordered.
20:37And so you're bumping that up, but
the minute that your, your system
20:40moves forward, like a second.
20:42Or, or some, some amount of time, you
can reset that back to zero because
20:46the, the, the minute that your system
meets up with the time that you had
20:50seen from the other system, and then
you can start using your local system
20:53again, then you can start using, then
you reset the counter to zero and you
20:57don't need to use that anymore, right?
20:58So it's, it's really tricky to, this
is really interesting technique where.
21:02You can sort of leverage your
local time and other everybody in
21:06the system can coordinate on this
and you get this really simplistic
21:09approach throughout this whole thing.
21:11Now, the big downside of this is that
you can't have something that's so far
21:15ahead in time that you, your timestamp
locally is just meaningless now and that
21:19you're always incrementing the bits.
21:21You're going to hit a ceiling where
you can't, that integer is too large.
21:24And you can't store it in like the,
you know, whatever the 24 bits it is.
21:28So then you're screwed.
21:29Then that thing is all busted.
21:31So there's this whole like
thing where there's approach.
21:33You can say like all of the
devices in this system need to be
21:37synchronized with at least like
five minutes or like an hour.
21:41And if you try to generate or
send a message or get a message.
21:45That is outside of that
timeframe, you just reject it.
21:48So it's a little bit of a like
brute force simplistic approach.
21:53Uh, but for something like Actual,
it worked out really, really well.
21:56It would not work in a complex
distributed system where there's
21:59like many, many, many clients.
22:01And you know, they're, they're,
it's not as robust for sure.
22:05That's as simple as I can explain it,
hopefully as, as a little bit longer
22:08than I was hoping, but that's, that's,
that's the best way that I can help.
22:11No, this was super insightful.
22:13And I think going deep into those
kinds of topics, I think that's, that's
22:17what the audience is interested in.
22:19So thanks so much for, for
like taking that little detour.
22:23I understand that now much better.
22:25So you've taken this concept and then
went from your local SQLite database
22:31And how did you take Hyperlogical
Clocks with your SQLite database
22:36and now made things collaborative?
22:38So once you have Hyperlogical Clocks,
it becomes pretty easy to sync things
22:42around because I essentially, honestly,
I don't use a super complicated.
22:47I know, is it Martin Kleppmann?
22:48I think works on like a lot
of really, really robust data
22:52structures that work really well.
22:53When you're in this distributed
world, you have to make sure that
22:56things don't end up in a bad state.
22:57So if you want like a.
22:59Tree data structure.
23:00And you want to say, Hey, this, like,
there should never be any orphaned
23:03nodes in this tree tree data structure.
23:05Well, it's really easy to get
that state in a naive thing.
23:08If you say like a last right, when set
that it's like parent child, and then
23:12like one person updates a node and the
other person had deleted that node.
23:17And so the node gets like.
23:19removed from the parents, but
then the message comes in later
23:22that this person edited that node.
23:24So the node gets created again,
but it's an orphaned node.
23:27Like that's a weird place to be.
23:29So like there's work in the CRDT
world, which is fantastic and makes
23:33those kinds of things super robust.
23:35I did not use any of that kind of stuff.
23:36So like those kinds of things, I just kind
of accepted and was like, well, I just
23:39code defensively against like bad data.
23:41And generally speaking in my app,
I didn't have a ton of places where
23:45things needed to be super robust.
23:47It was pretty easy to defend against
bad data, but essentially like I said,
23:51there's like an update, a crate and a.
23:53Delete function, they all actually
intercept to a single like lower level
23:58update function because instance and
updates are exactly the same thing that
24:02take a, take an object of things to set.
24:04And that object has to contain an ID.
24:06It turns it into CRDT messages.
24:09So every single field set.
24:10So if you said, set the transaction
amount in the transaction date to
24:13X and Y, those would become two
different messages with the same.
24:18object ID, right?
24:20Like they're, they're
targeting the same object.
24:22One is set the field amount to X and
one is set, set the field date to Y.
24:26There's two messages get created and then
this gets sent off to a syncing server.
24:30The syncing server is
really, really stupid.
24:32It just holds the messages and
when it's, when a client asks for
24:36messages, it gives us messages back.
24:38And so it can ask for messages
since a certain point in time,
24:41which again, in our, uh, HLC clock
world, that is our point in time.
24:46So it's literally just a Postgres query
that says select star for messages
24:50where HLC is greater than X and X
is the HLC that you gave it, right?
24:55So, so nice that you can just do
that, like greater than comparison.
24:58And then it, so it gets the messages back
and then it says, Hey, I'm, I'm, I'm,
25:01I'm all synced up and that's essentially.
25:03How it does.
25:04There is like a Merkle tree here in there.
25:06And I'm happy to talk about that.
25:08Yeah.
25:08Let's get into it.
25:09Uh, how Merkle tree is fitting in here.
25:11Sure.
25:12So yeah, the, so there's
a big problem here, right?
25:15Like, how do you know that
you're actually in sync?
25:18How do I know what messages to ask for?
25:19So when I, when I open up Actual and I
hit sync, do I ask for all the messages
25:24in the entire history of the world?
25:26Or like I could ask for.
25:28The messages since I've last opened
the app, like that seems nice, or like
25:32the last time I have synced, right?
25:34And then I would get those
messages and apply them.
25:37But there's like several questions there.
25:39One, what if another client had created
a message and just hadn't synced it
25:43yet, and created the message before
you last synced, and then you closed
25:47the app and like didn't open it.
25:48And then the other app, Came up, came
up and synced and then it like sent that
25:52message into the system and then you
open, you know, the, the, the original
25:56app, you would miss that message because
I've, I've seen since, since I've
25:59last synced, there couldn't possibly
any, couldn't possibly be any more
26:03messages, but that's not true at all.
26:05In this distributed world, you have
to assume everything bad, everything
26:08out of order is going to happen.
26:10You cannot code like that.
26:12The other problem.
26:14Is when I have synced up, like, let's say
I asked for all, all of the messages in
26:18the world and I apply them locally, how
do I know that I'm just actually valid?
26:24Like there could be a bug in my system.
26:26And so like, how do I know that
I have, like, when you, when you
26:29get all of those messages back,
how do I know which ones to apply?
26:32So locally, there's like kind of a
ledger of things that you've applied.
26:35And so you, you, you go through
every single message and you
26:38say, have I applied this message?
26:39I've already applied it.
26:40Don't apply it.
26:41If I have not applied it and it is a valid
message, like if it's a last right, when
26:46set, it'll say, if this has been written
by a message, like later than this one,
26:52then I can just describe this as well.
26:54So there's logic about how
to apply the messages there.
26:57That could be buggy or just like
a network request is so weird.
27:01And there's a state that I
just, I didn't anticipate.
27:03You need look kind of like a, a, another.
27:06piece of data structure, and
that's, that's, that's the Merkle
27:08tree to sort of audit things.
27:11It's, it's, it's a hash of
everything in a system, right?
27:13So if you hash all of the objects, all
of the CRDT messages, you can think of
27:18all the CRDT messages as like leaf nodes.
27:21And then you have like them grouped into
buckets and every node in the tree, all
27:26the way up to the root is a new hash.
27:28And so the hash at the root is a hash
of everything in the entire system.
27:31So you can quickly compare if like,
have I seen all of these CRDT messages?
27:35I just compared the two root hashes.
27:37And then if I, if those hashes are
exactly the same, then I know I've
27:40like, these have been processed.
27:42Where it gets weird is that like,
I use, A base three system, I think
27:48of like, basically every node in
the tree is a zero one or two.
27:52And basically you can construct
a timestamp of basically
27:57how I bucket the messages.
27:59Like I talked about buckets, right?
28:01You have to have like,
what are those buckets?
28:03The buckets for me were
basically time windows.
28:06And so I had time windows of
like, down to like, 10 minutes.
28:10So every single like CRDT messages
all applied within a 10 minute window
28:15would be hashed together into one hash.
28:17And that would be one
leaf node in the tree.
28:20And those would be all
the separate leaf buckets.
28:22So the bucket above it represented
a new, a larger window.
28:26Of time, right?
28:27And it was a, because it was base three,
you could reconstruct this timestamp.
28:31The thing that I wanted to guard
against was these are a lot of messages.
28:34There could be tens of
thousands of messages.
28:37You have to come up with a, a system
that is detailed enough so that you're
28:41not requesting too much, too, too, like
too many messages, but it needs to be.
28:46Coarse enough so that the tree
just doesn't get massive because
28:49this tree is sent across the
network every single time you sync.
28:52It's stored in your database and
updated, like updated as a blob
28:56every single time that you sync.
28:57So it cannot be a huge tree.
28:59And so this base three system encoded
these windows in a way that allowed
29:03me to calculate things down to the
five minutes windows and not be huge.
29:07Like I think it could ever
only get like 10 leaves deep.
29:10And so that solves the depth problem.
29:12It doesn't solve the.
29:13breadth problem of the tree.
29:14So the Merkle tree could still
get really wide if you're using
29:17the app over years and years.
29:18So if you think of this huge tree, we're
ever seeing in windows of time, there's
29:21only ever two paths down 10 nodes deep.
29:25And before that, it just
prunes them all away.
29:28It just deletes them.
29:29What that means is that if I miss a
message, like I think it was about a
29:33nine month mark, that if a message comes
through past nine months previous to that,
29:38I don't know which things to download.
29:41I check that I've received all of the
messages by comparing the hashes, right?
29:45If the hash of the root is wrong,
then I go down and I compare.
29:48That's how I get the window
to retrieve messages by.
29:50So I compare down and I get, I get
the, the node that is different.
29:54And then from that different node,
I can construct a window and says
29:57something came through like, like
a message from like, like three
30:01weeks ago, there's something here.
30:02That's a difference.
30:03I need to, I need to retrieve all of
the messages from three weeks ago, and
30:06I need to reapply them all because like
something changed their Merkle tree hat.
30:10Like the, the server sends the
Merkle tree, it's Merkle tree to me.
30:14And so I can compare the service Merkle
tree to mine and say, Hey, the service
30:18Merkle tree like changed around this time.
30:20So I need to get those messages.
30:22There's a certain point in time, which if
a message, if the Merkle tree like gets.
30:25Change like a long time ago, like
a really old message comes to the
30:29system and just screws everything up.
30:31I actually won't know that because
I've already pruned that tree away.
30:34So you'll either have to redownload
all of the messages, which is
30:37probably not practical, or you just
reject that client and you like
30:40detach it from the whole system.
30:42So there's edge cases there that
you have to sort of think about,
30:44but the really nice thing that the
Merkle tree gives me is that window.
30:48And a validation that I have actually
processed, like if the roots hashes
30:52are the same on the server and the
client, then I know I'm up to date.
30:55So thank you so much for giving
this, uh, quite in depth overview
31:00of how Merkle tree works and more
specifically how you applied them
31:05on the, the Actual syncing scenario.
31:07That was very insightful.
31:09I've now seen Merkle trees applied on
a, on a few different technological
31:13scenarios and the way how you've now
used them with the time buckets, I
31:19think is super elegant where it can,
in the happy path, save a lot of
31:23work, uh, for, for the syncing engine.
31:25And once you've understood the entire
system, actually quite simple to think
31:29about, and I'm sure also when you, maybe
something went wrong at some point and
31:33you at least still had sort of like an
intuitive mental model, how you can debug
31:38this, et cetera, and how you can test it.
31:40So I'm sure you didn't.
31:41just build a syncing system just for one
or two Electron apps, but probably for
31:48a vision to go beyond the Electron app.
31:51So how did Actual evolve
31:54from here?
31:54Yeah.
31:55So once I validated that I was going
to build a syncing engine and that
32:00it worked, I built a mobile app.
32:02Because obviously that was part of the
reason for wanting to do syncing is to
32:06be able to support like a mobile app.
32:07And I sort of naively was like, I'm
not going to build a shoddy mobile app.
32:11I'm going to, I'm going to do
this and I'm going to do it right.
32:13And so I, I, I did use React Native.
32:15I didn't, I wasn't so naive that I was
going to build a separate Android and iOS
32:20at myself, but I did attempt to use React
Native and I built it with React Native.
32:24And then I leveraged a project that
was at the time was having a decent
32:27amount of maintenance and investment
from, I forget which company started
32:31it, but it's a project called Node.
32:33js mobile.
32:35And so they basically took Node and
they compiled it for iOS and Android.
32:38And they basically built this thing
that would load your JavaScript bundle.
32:42And you could write JavaScript
based off of Node APIs.
32:45And it was great.
32:46Cause like HTTP, like all of the
standard node APIs that you would
32:50expect worked there and it fired off
as like a separate process, like it
32:53did it in, in the, in the proper way.
32:55And so I got a prototype working
and it worked really well.
32:57And so I was able to use
the exact same backend.
33:00And then I built a new
front-end in react native.
33:02That was like mobile specific.
33:03Cause I was like, I really want to
think this through in like a mobile way.
33:06And I'm not going to like shrink
down this transactions table and from
33:10desktop and like, Try to shove in
this complicated table on the mobile.
33:13I'm going to have a proper
mobile app and do it right.
33:15But yeah, having to, to design and
develop two separate UIs for two different
33:19use cases was just, was just awful.
33:21I mean, just time time wise, it was
just a bad decision, but I did, I
33:24did get the mobile app working and I
released it like, and it was a used
33:27thing that most people use, I had
like, I don't know, 900 installs on,
33:31on iOS and, uh, some, a couple of
hundred on Android, I think as well,
33:35people use it and it, it had the same.
33:37So like, React Native
powered the UI part of it.
33:41Um, and then in the backend, the syncing
engine and all the exact same code ran.
33:44And so the syncing, the syncing stuff
would, would run and it would send
33:47messages off to the server and then
it would be synced back to the desktop
33:50and, and it worked pretty well.
33:51That must've been a magical moment.
33:53Once you had your data from the desktop
app show up in the mobile app, you make
33:56changes on the mobile app and things
are appearing on your desktop app.
34:00All that work paying off.
34:02Yeah, yeah, I think I have a, I might have
a tweet, I think that showed them side by
34:08side and it was like, look, you can change
one thing here and it shows up over there.
34:11And it's always a cool,
like a cool demo able thing.
34:13Yeah, I think if as like local-first apps
are luckily becoming more and more normal,
34:20I think we will take it for granted.
34:22But I feel quite nostalgic about like
the, the days where this is not normal.
34:27And like the, the magic is really strong.
34:29And in my opinion, it's still
strong when you see like the, the
34:33real time collaboration of apps, et
cetera, and those things just work.
34:37So you've mentioned that you've
been using what was called Node.
34:41js mobile.
34:42And so that allowed you to
bring most of your code there.
34:45Did even like the, the SQLite bindings
and the SQLite reactivity system, did all
34:50of that just work also on React Native?
34:52It didn't just work, but it
wasn't too hard to get it to work.
34:55It was sort of just like Electron.
34:57I had to figure out how to load in.
34:59I think there was already an
Electron like C library that
35:02allowed me to easily access SQLite.
35:05But for React Native, I actually
built, I think I built my own SQLite.
35:08Bindings for react native.
35:12I'm actually trying to remember,
maybe I, maybe I did it.
35:14Maybe it did just work because
the, no, I think it was weird.
35:17I think the Node.
35:18js mobile allowed you to
compile C dependencies, but
35:22like it didn't fully work.
35:23And so I had to kind
of get things working.
35:25So it didn't just work, but it wasn't
like super hard to get working.
35:28You basically had to compile SQLite for.
35:31IOS and Android, right?
35:32And so you have to like hook in their
build process and get that loaded.
35:35And Node.
35:36js's mobile support for like C
dependencies from Node wasn't straight
35:41out of the box, but honestly it
worked, it worked well enough to where
35:44it wasn't that hard to get working.
35:45So I was, I was pretty impressed
by that, but I did, I did have
35:48to kind of wire up the, the, the
core things that I still needed.
35:51Got it.
35:52But I suppose that's mostly one of work
and the overall goal of like reusing
35:57the same code from your Actual desktop
app in the mobile app to the most
36:01degree aside from the UI that paid off.
36:03Yes, I think that paid off like
that specific investment worked.
36:07The real downside to all of this stuff
about actually like actually having an
36:12app and especially for Mobile is that
the ecosystem just changes and your app
36:17is in this like proprietary app store.
36:19And like the proprietary app store
is like forced you to, to update it.
36:23Like I think Android at one point forced
me to like, they basically deprecated an
36:27API and I could not release a new version
of the app until I stopped using that
36:32old API and started using like a new one.
36:34The new one requires some really
weird thing that like triggered
36:37a fault, like a crash on Node.
36:39js mobile.
36:40So like, that's a super risky.
36:42Place to be in and so it, it paid
off, but like, it was a continuous
36:46investment risk that didn't require
any maintenance really, except when
36:52I was forced to buy the ecosystem.
36:54And so that's the real
win of the web, right?
36:55Like, you're not like, the
web definitely is weird.
36:58sucks in a lot of ways and like
they change things and they
37:00force you into certain things.
37:02But generally speaking,
backwards compatibility is a
37:04huge, huge value on the web.
37:06And it's just not the case
in the mobile ecosystem.
37:08And they have leverage on you
because you have to go through
37:11their app stores, which just, I
just hate, like, I love mobile apps.
37:15I love having like a real
app, but like that Node.
37:18js mobile was like a risk for sure.
37:21Because There's like certificates and how
things are assigned that kept changing.
37:25And like was, I luckily
never got totally stuck.
37:29Andre actually, I think was the
only other person using this.
37:31And so luckily he had
like a PR that fixed it.
37:34Cause he, he was stuck in the same way,
but he actually knows how to change stuff.
37:37And so he had a PR merge like
three days earlier that luckily
37:41I was able to update Node.
37:42js mobile and it finally
worked, but it's scary, right.
37:45To be.
37:45Like I could have just been totally
stuck as far as I know, there's not
37:49a great way to have that set up today
without a similar level of risk to,
37:53to, because you're, you would have to
build a pretty bespoke sort of custom
37:57native app on, on mobile to, to do this
whole, uh, truly local-first type thing.
38:02Got it.
38:03Yeah.
38:03I think, well, as you mentioned, uh,
well, most platforms are evolving over
38:08time, some more aggressively and yeah,
with little tolerance for developers
38:12who don't update the apps, I think the
web is more graceful in that regard.
38:16On mobile, the rock is sometimes
being pulled underneath you.
38:20But on the, seeing the glass half
full, you also get hopefully improved
38:24APIs and the ecosystem is improving.
38:27I've been recently getting a bit closer to
the React native ecosystem and Expo seems
38:32to make a lot of that quite a lot nicer.
38:35So there's an impressive amount of.
38:37Bindings to native APIs that
you, that you might need.
38:40So, and also given that JavaScript
is also evolving in terms of
38:44standards, supported standards.
38:46Uh, I think there's now also the
lines are getting a bit blurrier
38:50of what needs to run a Node.
38:51js or what is just supported
by like other JavaScript
38:55execution environments or mobile.
38:57You have JavaScript
core on iOS on Android.
38:59I don't know what it's called, but
you also have Hermes, the tool by the
39:03JavaScript runtime by Facebook, I think,
which is specifically designed for mobile.
39:08So I think you get more options,
but there's still like sharp edges.
39:13So did you eventually give up
on those mobile apps or did
39:17you, did this lead you to web?
39:19How, how did the journey
continue from here?
39:22Sure.
39:22So I'd never gave up on the mobile
apps until I fully open source Actual
39:26when I kind of gave up on Actual and
entirely as, as a whole business.
39:29And that was when I told the
community, Hey, if you want to,
39:33like, this is the source code, I
included the source code of it.
39:36And so if you want to figure out how to
like build this and purchase a developer
39:40account and get this working, like it's.
39:42Totally up to you.
39:42But in terms of the Actual apps
that were on people's phones at the
39:46time, yeah, there was never going
to be another update of those.
39:48I'm still working on shutting down Actual.
39:50And so those apps still do exist in the
App Store as far as I know, but they
39:53will be removed when I shut down Actual.
39:54So there was another phase where
I had not given up yet, but I did
39:59sort of admit defeat in terms of
Where the investment should go.
40:03And I just was like, I
need to be on the web.
40:06Like at that, there was a
point when Actual just like,
40:08didn't even work on the web.
40:09It just was not a thing.
40:10And so I think I might have
had a demo on the web, but it
40:13just like, didn't support it.
40:14Loaded the loaded in the
entire SQLite file locally.
40:18And then it just like,
didn't persist anything.
40:20Right.
40:20It just like the app ran, but it didn't
actually, it kind of like removed a
40:24lot of the functionality and it was
just like a quick demo, but like the.
40:27Man forcing people to, to, to
download an app and running into
40:31problems with their local device.
40:32Like it just, the, the web is such
a powerful distribution mechanism.
40:36And I think that's, you know, everybody
knows that and it's really hard to, to
40:40fight against to fight against that and
to force people to, to download apps.
40:44I honestly still love apps.
40:45Like once it gets set up,
I think it's really nice.
40:48Like if it's an app that I use almost
every day, or even just a couple,
40:51like a couple times a week to have it.
40:52In my, in my doc and I can like close it.
40:54I can quit it.
40:55It doesn't have to be in my mess of tabs.
40:57I have a billion tabs and I, so
like managing my, an app that I use
41:01frequently in the browser to me, I,
I don't really like that, but at the
41:05other, at the same time, There are
things that I use all the time on the
41:09web that are only in my tabs, actually.
41:11And it's very kind of nice to
like be able to close the tab and
41:14then like quickly open up a tab
and go to the app really fast.
41:17Whereas like the app startup for,
you know, Mac OS tends to be like
41:20at least two or three seconds.
41:22So I don't know, it's, it's weird.
41:23I kind of say that I like local
apps and yet I live in a browser
41:26like 80 percent of my time.
41:28So there's something there.
41:30And as much as web technology
kind of sucks, and it's so
41:32confusing and I don't like it,
the web is a really powerful draw.
41:36And so I eventually was just like, you
know what, I need to figure this out.
41:39So I never like stopped investing
in the Electron and, and Node.
41:43js or the um, mobile Node.
41:45js mobile stuff.
41:46I would still update.
41:48Do a release of those
every couple of weeks.
41:50But the first, I remember the first time
that I got Actual working on, um, on the
41:54web and I used, you know, that's where
absurd-sql came from, where I was able
41:58to, to compile things out at a compiled
SQL light to web assembly and use
42:03techniques such that like you can open
the app in multiple tabs and actually
42:07change the data, actually query the data.
42:09And like, when you change data
on one tab, that when you.
42:12Query that data on another
tab, it actually shows up.
42:15I got all that working, which is
really, really great, which is
42:17something that we can talk about more.
42:19I remember like the first time that
I released that and people were
42:22just like instantly able to use it.
42:24And then when I was able to fix a bug
and I was just like with like, basically,
42:28basically in, in, in our sync command.
42:31Was able to like, get that bug in
people's hands or that, that, that
42:35bug fix, um, out there was just like
insanely addicting that, that, I mean,
42:39that's the power of the web, because also
actually, it's just a local app, right?
42:44That's the other thing that I
realized when I built the web app, it
42:47literally is a set of static files.
42:49There's no.
42:50Like there's a syncing service needed,
but like in terms of the entirety of
42:54the app itself, I don't need to do
any database mutations when I deploy.
42:58There's no, there's no large CI pipeline
that does all of these complicated
43:02things to deploy, to restart services, to
update services and do all of this stuff.
43:06It is literally an HTML file and
like five JavaScript bundles.
43:11I can just rsync those over to my web
server that host static files and users
43:18like load those new static files and then
it queries their local data and all of
43:21their local stuff is now reading that
like fresh stuff, but like, just the
43:26ability to just like sync those files
over and just, you know, Deploy like
43:29a hundred times a day just unlocks an
iterative development speed that just
43:34cannot be matched by mobile development.
43:36Yeah.
43:37Waiting for the iOS app store to
finally approve your app to be
43:40released is not a fun state to be in.
43:43And having the ability to just in
a matter of seconds, release a new
43:47version of the, of the web app.
43:49It's just so liberating.
43:51So you've now went from this transition
of initially building an Electron
43:55app, which was just an Electron app.
43:57And then you went to another
platform, mobile, Android, and iOS,
44:02where you could still bring Node.
44:04js with you.
44:05And so the, the platforms were still
like similar enough and they, you could
44:10leverage the native aspects of those
platforms still to the extent that
44:15you've so far needed, um, uh, But the
web is quite different in that regard.
44:20There is no native C
bindings you can leverage.
44:24And I'm not sure how far Wasm was along at
that point, but there would have been one
44:29path where we just say, okay, I need to
completely rewrite Actual, maybe even give
44:34up on SQLite and just like embrace all the
standard things that people do on the web
44:40or somehow bring the architecture and the
technological choices that you had so far.
44:45Bring them to the web.
44:47And that means like a
pretty intense pioneer path.
44:50And I think you've chosen the letter.
44:52So tell me more about that.
44:54Absolutely.
44:55Yeah, it was really a fun, a fun
experiment because the things
44:58that I was using were not that.
45:01Like novel SQLite was really like
the biggest one that just did not
45:04work on the web, everything else.
45:06You know, like if you need like
a background process, you can
45:08just fire up a, a web worker.
45:09It's, it's not too bad.
45:11I'm trying to think of other things I did.
45:12So I was using the, the old node async
hooks, which is now the async local
45:17storage for some really, really neat
stuff for, for the undo and redo mechanism
45:22for Actual, uh, it actually tracks the
messages that get generated for an entire.
45:28Like API requests.
45:29So I have these like handlers.
45:31And so it sets in local and async
local storage, like a buffer of
45:35messages that is fresh each time.
45:38And so when you send an action to do,
while it's executing that entire, like
45:43transaction create method, it tracks
all of the messages that are done
45:47during, like created during that time.
45:49And then it, and then when that
action is done, it reads that from
45:53that buffer and then it packages
them up as like an, like an undo.
45:57Packet, right?
45:58That, and that then gets
stored onto another queue.
46:00And so if you want to undo that,
that's how it knows like which
46:04messages and through which points
of time it needs to like, it needs
46:08to undo up to a certain time, right?
46:10And so the async local source is
really, really great for that.
46:12Cause I didn't have to thread
through all of this stuff.
46:15The web just doesn't support
those kinds of things at all.
46:17So there are things like
that, that I had to undo.
46:20Luckily, that actually improved things
on mobile as well, because Node.
46:23js mobile was actually, I was
finding some, some bugs with some
46:27of those more advanced Node APIs.
46:30So there are things that I had to
simplify and like kind of undo,
46:33but for the most part, SQLite
was like the main hard problem.
46:37And so yes, this is where I, I
experimented and I played and I
46:41was like, it's I mean, WebAssembly
was pretty mature at this point.
46:44I think this was like 2020 or so.
46:46Um, I could be wrong on that, but it
was, you know, like WebAssembly was
46:49like a pretty good at that point.
46:50I had no problems like trying
to bet on WebAssembly then.
46:54It was not that hard to compile.
46:56Uh, SQLite's WebAssembly
at that time, the SQL.
46:58js was, I think, the big library that
already came with the pre compiled SQLite.
47:01So I got the app running with SQL.
47:04js and without a ton
of work, to be honest.
47:07Then it was like, crap, what
happens when you change the data?
47:10Because SQLite literally just slurps in
the whole database in local memory, right?
47:15So you can change the data.
47:16You can be working with your data.
47:17I mean, it all works totally fine.
47:20But then you refresh the tab
and then all that data is lost.
47:22So obviously that's not,
that's not going to work.
47:24So I came up with all these ideas,
like, well, what if I like stored the
47:28messages persistently in the background?
47:31And then like, when you
refresh the tab, it.
47:34You know, reapply this
messages after loading it up.
47:37But that means that every single time
you open up a new tab, it's loading
47:40in the entire database into memory.
47:41And like, for me, at least that
was like a hard requirement.
47:46Do not load the whole
database into memory.
47:48Even worse than that, do not write
the whole database back into memory.
47:52Cause that was, that was the thing that
some people were doing at this time.
47:55They were like, Oh, just
solve this persistent thing.
47:57Okay.
47:57You change one number from four to
five, and you're going to rewrite
48:00this entire six megabyte database.
48:03Back into memory, it is so inefficient.
48:06And I think that's one of the, my gripes
with the web is that like, we've, we've
48:09lost the plot of software development
where it's, where our bar for, for quality
48:16and, and good engineering practices
is so low and it's, Might be a spicy
48:22take, but like, we just accept the
fact that things have to be this bad.
48:27And like, it's so like acceptable to be
like, well, there's nothing I can do.
48:31It's out of my control.
48:33Right.
48:33So I'm just going to write, it's right.
48:34Six megabytes of memory.
48:35Every single time I change
something that is like so bad for
48:39like your computer's hard drive.
48:41It's so bad for your.
48:42Memory consumption, your, your power
is so bad in all sorts of ways, right?
48:45It's so wasteful.
48:46Man, it just makes the app entirely
slow and prone to all kinds of problems.
48:51Like it's, I do have gripes with
just, I feel like our bar as
48:55an industry needs to be higher.
48:56And I, I honestly think that web
development is to blame for some of that.
49:00That's my spicy take because it makes
it easy for us to throw up our hands.
49:03And say, I can't go past
that layer of abstraction.
49:07Whereas in the native
world, you have a problem.
49:09You can dig into those, the C binaries.
49:13it might be hard, but you have the
power to go in and change things.
49:17Whereas on the web, it's
like, it just works that way.
49:19It sucks.
49:20Like too bad.
49:21I think we need aim, aim higher.
49:22And so that's, that's, that
was my approach, right?
49:25I was like, I'm not, I'm
not going to accept this.
49:26This is unacceptable.
49:27Is there a way I, um,
I can get this to work?
49:30And maybe there wasn't.
49:30And then I would have
to just sort of give up.
49:32But I discovered a kind of
novel thing that ended up
49:34working actually pretty well.
49:36And that's how absurd-sql came
about was I, I figured out a way.
49:39The real trick here was, so don't
read everything from memory, right?
49:44So how do I not read
everything from memory?
49:45Well, I have to store it in an existing
database that exists on the web.
49:49There's no question at the time
that that was IndexedDB, right?
49:52That was like the only
mature database abstraction.
49:55Okay, so I can store all this
in IndexedDB, this whole blob.
49:59But I'm still having to read
this whole thing into memory.
50:02How do I stop that?
50:04So I figured out, I looked
into SQLite's internal APIs.
50:06At this point, I was pretty familiar
with SQLite's internal APIs.
50:09Honestly, it's a really fun read.
50:11They're well documented,
really straightforward C code.
50:14It's really, really fun.
50:15So basically, SQLite works in blocks.
50:17So you have like, I
forget what size it is.
50:20Is it 5k blocks?
50:21Like you can actually change, change the
block size that SQLite works, works by.
50:24But like, I think the default is 5k.
50:27I think like 4k, or 8k,
something like that.
50:30But like, yeah, that,
that ballpark, I think.
50:32Yeah.
50:33So that's, that's the amount that it
reads from your hard disk per chunk.
50:36Like if it needs to run,
read one little bit.
50:39It reads at least that amount, right?
50:41So it like reads a whole block in
and then it does what it needs to.
50:44And then it writes that block back
in if it, if it needs to write stuff.
50:47So it does, that's how it like, doesn't
read everything in from all the memory.
50:51Um, and so I thought, I was like,
okay, well, what if, what if in
50:55indexedDB we store these blocks?
50:56Like that is the, like, this needs to
be just basically like a file system.
51:00Like I need to make this
treat as a file system.
51:02And so I.
51:04Started playing with this
and I, I, conceptually, I was
51:07like, this, this could work.
51:08Like, I don't see any reason why
this couldn't work because once
51:12I have that, once you can read
different blocks and you don't have
51:14to read the whole thing into memory.
51:16Well, SQLite, it was very
straightforward in SQLite's code
51:18about how it writes stuff down.
51:20I was like, well, I can also, I
can also just write the blocks.
51:23Right?
51:23Every, I feel like every, every
three or four years I have this,
51:27like, I see a gap and like, it's,
it's intriguing to me a lot.
51:31And like, I, I like prettier
was kind of this thing too.
51:34Like I see this thing that's like, man,
there's this problem that everything
51:37is having, but like, I see this thing
that works really well over here.
51:40Like, can we just.
51:41Take that from over there
and like apply it here.
51:43Like, like, does that work?
51:44And you know, a third, a third
of the time, it ends up being an
51:47interesting thing that does work.
51:48Essentially all of that to say,
I discovered how to store these
51:53blocks in, into IndexedDB to read
and write them from, from IndexedDB.
51:58And I was able to leverage
IndexedDB's transactional
52:02semantics so that locking worked.
52:05And that's the real critical thing here
is that like SQLite, heavily relies
52:09on file system locking API, such that
says, I'm going to lock this block.
52:13I'm going to lock this piece of data here.
52:15Do not let anything else write to that.
52:16That is a stable database.
52:18Like that is how it does not get corrupt.
52:19If it, if you break those
semantics, it will get corrupt.
52:22And so, um, I was able to leverage.
52:25IndexedDB is like locking semantics.
52:27And I was like, I can map these
onto these such that like, when I'm
52:30writing this thing, nothing else
should be able to read from it.
52:32And it worked.
52:33So you can like load in
the data in different tabs.
52:36You can write to that in like one tab
and like see that data in the other tab.
52:40And it worked out really well.
52:41And I was like, this would be
a really interesting thing to
52:43open source and talk about.
52:43And so.
52:44It became absurd-sql and it became
like a, a pretty influential thing.
52:48I think, honestly, this kind of project,
I invest like a month or two of my life
52:52into, and then like, I have a day job.
52:54And so I just did not have a lot of time.
52:55Like I got it working for Actual and it
deployed Actual and it worked on the web.
52:59And that was amazing, but I just didn't
have the resources or the time to really
53:03like build it out or like fix really
obscure bugs that people were coming up, I
53:07sort of like kind of left it as a project
that wasn't, that wasn't Maintained
53:10super well, and sometimes I feel guilty
about that, but I think there's other
53:13community members that have taken it on.
53:14It's, it's more of like an
influential prototype, I think,
53:17more than like a mantained thing.
53:19Yeah, I think for me, this is
actually kind of like a, a definition
53:24of a kind of project category.
53:26Like not every project needs
to like prettier lived on and
53:30now it's like super common.
53:32And like, I think that's one very
successful way how a project can evolve.
53:37But I think with absurd-sql, you've built
something it's even more than a proof of
53:41concept because you used it in production.
53:44I, I use it for a while in production.
53:46Other people use it for a while in
production, but, uh, like you said, like
53:50it became a very influential project.
53:53And I think it showed to the world,
like, Hey, we don't need to settle for
53:57saving six megabytes or 50 megabytes
for every little change as you do.
54:02Want to do persistence in the web before
people would just like ride, like.
54:0780 megabyte JSON files, like all the
time to, to index to be, and then we're
54:12then wonder why their, the app is slow
and their, their CPU is going crazy.
54:16So you aiming for higher, I
think that has a massive impact.
54:20And I think that's now those
ideas are still like leveraged
54:24now in other projects.
54:25Whether I think.
54:26The wa-sqlite, um, project that is,
I think also using some, some similar
54:32approaches and by now we also have a
different storage mechanism and the web
54:37that is more and more common, namely
OPFS, original private file system.
54:43Which also already gives you a file
system representation where you can
54:47lock individual files, et cetera.
54:49I think the details are still being
figured out right now, but that's, for
54:53example, what I'm using for Overtone and
in production, that's already quite nice.
54:57But I think you've really, you, you went
super early on that and very daring from
55:03first principles and that's so admirable
and that, that like very inspiring.
55:07Thank you very much.
55:07I appreciate that.
55:08Sometimes I, I don't know.
55:10I, I don't really track.
55:12The, like some of the fallout sometimes,
like I, I just had to invest in my job
55:16a lot for the next like six months.
55:18And so sometimes I'm, I'm not sure.
55:20I don't, I don't know how much influence
it has, but it's, it's good to hear
55:22feedback that work is impactful.
55:25I, I would love to hear about your
experience with, with it, with OPFS.
55:29And, and, uh, are you using
the file system native access
55:31stuff as well and in your app?
55:33How's that going?
55:34The latter, not yet.
55:35I'm also planning to, since like
for Overtone, which is a music
55:39player, I do want to also support
you bringing your own music that
55:43you might have like in a download
folder on a music folder somewhere.
55:47Right now I don't use that
particular browser APIs yet.
55:51But I'm using OPFS for many purposes.
55:54I'm using OPFS for persistence
of a SQLite database, where you
55:59would have used IndexedDB before.
56:01And I think some people still
use IndexedDB for targeting
56:05older browsers as well.
56:06But I'm also using OPFS for like what
you would use a file system for as
56:11well, which is like storing files.
56:13So for example, Before displaying
images in the app, I actually don't
56:18use like just an image tag and
then point to an external URL since
56:22those URLs might go away, et cetera.
56:24So I actually download those images, store
them in OPFS and then pre process them.
56:30Um, on a worker and sent them
over on a, on an off screen
56:34canvas to the main thread.
56:35So I'm like using some, some more
like native development practices here
56:40and trying to bring them to the web.
56:41Very, very much sharing the same
opinion, like your spicy take from,
56:46for me, not so spicy take that
we should aim higher in the web.
56:49And I think we can learn quite a lot from.
56:52More native development backgrounds.
56:54And this is, I think this is how we
get, uh, after all pretty fast web apps.
57:00Figma is another notable example
there where how you get actually a
57:04really high performance app that feels
nice is by aiming higher and, uh,
57:10bringing some of those methodologies
from other environments to the web.
57:14So I think.
57:16There's still a couple of
interesting aspects in absurd-sql
57:19that we haven't yet gotten into.
57:21So you've mentioned that you're
using index to be, uh, with, uh, the
57:26block, um, the, the block storage
and the index to be transactions,
57:30but to actually write that you could.
57:33I guess either do, could you do
that on the main thread since you've
57:37chosen to do that on a worker?
57:38So maybe you can talk a little bit
about the, the threading model and
57:42also how you even went beyond IndexedDB
transactions and using atomics and
57:49shared array buffers to still slay some
dragons that needed to be slayed here.
57:54Yeah, there is one little trick that I
discovered and I think I was actually
57:59a live stream and I think I have a
recording of it, which was kind of fun.
58:01It's like a fun little blew
my mind at, um, at the time.
58:04And I think this is a trick
that was independently
58:07discovered by several people.
58:08I think there's a, uh, there's another
library that I think from is from some,
58:12some Google folks about how, like,
like loading in third party things
58:15on, you know, In like an iframe or
something to kind of like sandbox things
58:19that that uses this same technique.
58:21But essentially here's,
here's the problem.
58:24When you compile SQLite through
WebAssembly, you can intercept
58:28the API calls that it makes.
58:30So you can intercept the like read
The read and write command, like
58:33there's a C API is reading right
for reading and writing files.
58:37And so you can say, okay, I'm going to
implement the read command for like,
58:41through WebAssembly, the WebAssembly
compiled version of this SQLite.
58:45I'm going to implement the
read command and like read some
58:47data for it and give it back.
58:48So I can just read from index, from index
db and everything is going to work right.
58:52The, the problem is, is that
the read command from C is
58:55completely synchronous, right?
58:56So the WebAssembly compiled binary
expects that to be synchronous.
59:00Like you can, it'll call out to
JavaScript and you can implement the read
59:03command, but you cannot await in there.
59:06Like you literally have to, in that event
loop, synchronously return some data.
59:10And so, uh, when I hit that, I was
like, I thought I was dead in the water.
59:13I was like this, this.
59:14I just can't do this.
59:15Maybe, maybe it wasn't that bad
actually, because I think I did
59:18see, so one option is you can in
WebAssembly, I think it has a way to
59:22convert synchronous APIs to async.
59:24I think it's called like
asynchronify or something like that.
59:27I was terrified of it to be honest.
59:29And so I thought it just
wasn't going to work.
59:30I was terrified about the performance.
59:32I've had experiences in some
codebases that overuse of asynchronous
59:36APIs really killed performance
because waiting one promise tick.
59:41In every single layer of the
abstraction is very not noticeable
59:46at a small scale, but it very
becomes noticeable at a large scale.
59:50And the really pernicious thing about it
is that it's death by not a thousand cuts.
59:54It's death by a million cuts.
59:55Like, it's, you can't
go in there and remove.
59:5820 by 20 awaits, right?
59:59And like, it'll be fine.
1:00:00The thing that I don't like about
async code is that as you abstract
1:00:03things away, so if you take it from
two abstractions to 10 abstract, like
1:00:06you split a two async functions into
10 async functions, well, every one
1:00:10of them has to await on each other.
1:00:12And so it's literally making it slower.
1:00:14And that's not the
case , with synchronous stuff.
1:00:16You can make it two functions.
1:00:17You can make it 500 functions.
1:00:19You can abstract things
the way that you want to.
1:00:21The shape of your abstraction
literally impacts performance when
1:00:25you're in, in, um, async world.
1:00:26So I have experience with code bases that
like, that did things in a really weird
1:00:31way, like awaited literally everything.
1:00:33And it was, it was causing
all sorts of perf problems.
1:00:36And so that, For that reason, from my
experience, I was terrified of it because
1:00:40I thought it made every single function
in C in the WebAssembly binary async.
1:00:44It turns out that I was probably wrong
and I probably should have done my
1:00:48own investigation in like performance
profiling because I got some feedback.
1:00:52Later on, like months after I released
absurd-sql, I think from some people
1:00:56that I respected pretty well that
were saying, uh, the async stuff is
1:01:00actually fine, like it actually has
a very negligible performance impact.
1:01:04So there's probably some tricks
that they, they, they do that.
1:01:07So it's probably fine.
1:01:07And I think that WA SQLite
project that you mentioned, I
1:01:11think it actually uses this mode.
1:01:14And the good thing about that is
that it doesn't need atomics, which
1:01:17means it doesn't require HTTPS.
1:01:19So I'm probably.
1:01:21Let me jump back a little bit to
explain why atomics are even needed.
1:01:24So let's go back to that
synchronous method, right?
1:01:26So I need to call into IndexedDB
and get and do an async API.
1:01:30How am I going to do that?
1:01:31Well, there's a little trick that
you can do by making an async call.
1:01:37And I believe that it's Has
to be on a different thread.
1:01:42So there's like two
background threads, right?
1:01:44There's like the normal backend thread.
1:01:46And then there's another separate thread.
1:01:48That's like the thread that reads and
writes from index CB like asynchronously.
1:01:51Right.
1:01:52Between those two threads, you
share a shared array buffer, which
1:01:55is this really low level, really
interesting thing on the web, which
1:01:59is shared memory across threads.
1:02:01And on top of that, you, uh, there's
APIs you can use called atomics, which
1:02:05allow you to interact with this shared
array buffer, and you can actually
1:02:08coordinate across the threads and do
some really, really powerful things.
1:02:12So one of those things that you can
do is you can write to that shared
1:02:15array buffer in a certain way.
1:02:17And then in another thread,
you can call atomics.Wait.
1:02:20And what atomics.wait does is it literally
synchronously blocks that thread from
1:02:24running like it, you call that thread.
1:02:26And if it does not wake up,
then it won't wake up ever.
1:02:29Like you call atomic weight and you
tell it, wait for this bit to be
1:02:33flipped in the shared array buffer.
1:02:34And then the other thread can
flip that bit when it's not.
1:02:37done, and then the other
thing can continue executing.
1:02:40Using that technique, we can read from an
asynchronous thing in the second thread,
1:02:44do whatever we need to, and store that
data in some sort of buffer somewhere.
1:02:49And then while the first thread is
blocked on that atomic set, wait.
1:02:52And then once the, once that
bit is flipped and it continues
1:02:55executing, then it can read from that
buffer and actually get the data.
1:02:58And so using that technique, Um, I
use that technique in every single API
1:03:02in, in Upstairs SQL to turn an async
function into a synchronous function.
1:03:06And that's how it can
interface with IndexedDB.
1:03:09The downside though, is that to use
atomic set weight, it's one of those
1:03:13newer APIs that Chrome and other browsers
force HTTPS a secure context on you.
1:03:18And so your app has to be running under
HTTPS, which I've always said, like.
1:03:22Who cares?
1:03:23Like, of course you're going to
be running under that anyway.
1:03:25It's been enough of a,
kind of a pain point.
1:03:27Like people are using like weird
reverse proxies or doing their
1:03:30own things where suppose, I guess
some people just don't really care
1:03:32and they just want to run HTTP.
1:03:33And so Actual, like the open source
version of Actual can't run under HTTP.
1:03:37And that's like, you always
have to be setting up to me.
1:03:40It's, I don't know, I still am a
little bit like, I don't really care.
1:03:42Like you can just like set up
HTTPS, but it's enough of a source
1:03:45of a pain that I can see benefits
and not, not having to require it.
1:03:49Right.
1:03:49Yeah, I think this was also related
to the, uh, Spectre exploit at
1:03:53some point, uh, vulnerability.
1:03:55And I think it's not just that you need
to run on HTTPS by now, but I think you
1:03:59also need to have a few HTTP headers set.
1:04:03I think like cross origin, uh,
open policy and embedders policy.
1:04:07And I think there was also like
a limitation that Safari didn't
1:04:10support it well for, for some time.
1:04:12So yeah, there it's, Still, browsers
are still growing up, but I think at
1:04:17some point this can be assumed that, uh,
this will just work, but on the other
1:04:21side, I think those tricks might also,
you mentioned that asyncify approach.
1:04:26So that is also another option.
1:04:28And I think there's even a new
approach stabilizing right now.
1:04:32Where WebAssembly natively
can integrate with Promises.
1:04:36So I think that's a, that's a new, um,
development around WASM and browsers.
1:04:42So I think there's, you, you certainly
use quite the bleeding edge there.
1:04:47And so, yeah, right now it's
getting more stabilized.
1:04:51But I do think there is some truth
to what you've mentioned in regards
1:04:55to avoiding asynchronous code when
possible, since it's not just like
1:04:59a potential performance overhead.
1:05:02So, and I think some people go even
as far as saying that going from
1:05:06callbacks to async await and promises.
1:05:09Was one of the biggest
mistakes in JavaScript.
1:05:12I think that can also be
considered a hot take.
1:05:14Let's see where we'll end up
in a couple of years on that.
1:05:17But, uh, aside from the performance,
I think another common downside
1:05:22of asynchronous code is that it
basically introduces distributed
1:05:25systems problems into, into your code.
1:05:28In this case, where we basically
just wrap an API, I think it's okay.
1:05:32Um, But, uh, that's a, that's another
notable difference of like how, what
1:05:37we've been exploring with LiveStore
and, and Riffle is by really making
1:05:42the, like in a browser context, still
from the main thread, allowing for
1:05:46synchronous SQL queries, which return
very fast and therefore like you can just
1:05:52write your normal JavaScript as you do.
1:05:54comes at a risk of potentially
blocking the main thread.
1:05:56That's a, that's a different
challenge, but if you, if you have
1:06:01performance under control, it gives
you a much simpler programming model.
1:06:04So that's another weak one.
1:06:05Yeah, that's, I'm all here
for synchronous SQL queries.
1:06:09I think it's crazy.
1:06:11I think there's been no libraries
before that integrated with SQLite 3 and
1:06:14like made all of the APIs, um, async.
1:06:16It was crazy.
1:06:17Like it's such a in
memory super close local.
1:06:20thing and , you really want at the
low level to, to provide the at
1:06:24least option to be synchronous.
1:06:25And it, so that I just remembered
actually, it's not just
1:06:28wrapping an async function.
1:06:30So like it's, it's an internal
implementation detail.
1:06:33If you go the asynchronous by route,
which turns C functions into asynchronous
1:06:37functions, therefore you can interact
with like asynchronous stuff.
1:06:41Believe that that, that forces you
when you call a SQLite by like method,
1:06:47that method is an asynchronous method.
1:06:49You cannot synchronously execute a SQL.
1:06:51That was a big reason why I also really
did not want to use WI SQL because I
1:06:56wanted my functions to be synchronous.
1:06:58My app depends on many of them being
synchronous and just my workflows.
1:07:01And it just, it just greatly
simplifies the entire workflow.
1:07:04There's no reason for
me to make this async.
1:07:06This is a single client.
1:07:08App, there's one request
coming through at a time.
1:07:11I can control it entirely, right?
1:07:13This is not a web server handling
thousands of requests at a time
1:07:17to take on the complexities of
asynchronous code with the performance
1:07:21hits was just ridiculous to me.
1:07:23And so I, This technique I still think
has has merit, actually, the more I think
1:07:28about it, and it's something that I owe
this community like a great deal of, of
1:07:34like blog posts and and and research.
1:07:36I need to sit down and really
like go through what the latest
1:07:39and greatest is and really.
1:07:40Vetted and see, see what people
are landing on, because if it's
1:07:44still not possible to do that,
I think that's a huge downside.
1:07:47I do.
1:07:47I did just see that the, the
file system access APIs, I
1:07:51believe they finally converted.
1:07:53Originally there were some of them
in the, in a worker thread that
1:07:56were supposed to be synchronous, but
in the spec, we're actually async.
1:08:00And that made me really, really not happy.
1:08:03And I filed like a GitHub ticket.
1:08:05I think they finally, if I.
1:08:07Just checked earlier this morning.
1:08:08They finally have on the MDN
page says that they are now
1:08:11synchronous, which is great.
1:08:12So hopefully we're moving
in the right direction.
1:08:14But I think it's, yeah, I think it's
really, I'm all about synchronous 'cause
1:08:17it means that you can use them in context
that like are synchronous, like there you
1:08:21might be removing a lot of functionality
because once something is async, the thing
1:08:27that uses it has to be async as well.
1:08:29And a lot of these cases for local-first
apps especially, it just is, there's,
1:08:32there's literally no benefit to it.
1:08:33, it's a local app.
1:08:35There's not a second user that
can come be querying this.
1:08:38I don't know.
1:08:38I think I get it.
1:08:40I think we've, we've over
indexed a little bit on this.
1:08:42And I do think that I
forgot about the headers.
1:08:45That is because like reverse
proxies can like drop those headers
1:08:48and then like a user is like,
why isn't this app even loading?
1:08:50So I, I don't know.
1:08:51It's a trade off.
1:08:52I still probably lean a little
bit towards it's worth it.
1:08:55But yeah, I completely agree with, uh,
I think this is the same theme again,
1:09:00where in web development, we've kind
of gotten so used to some practices.
1:09:06And I think it's one is, uh,
being efficient and performance
1:09:09minded, but another is just
like the programming models, how
1:09:13they've kind of eroded over time.
1:09:15we need to deal with distributed
systems problems where they're
1:09:18just completely accidental.
1:09:20And , we're just so used to, to like
so many things being asynchronous,
1:09:25which doesn't need to be asynchronous.
1:09:27We kind of went from callback
hell to async hell in a way.
1:09:30And in React we use, useEffect for
so many things where we shouldn't.
1:09:35And is just so wild
that like most of our.
1:09:38Data interactions are asynchronous,
like in a way it's almost like if
1:09:43react would not just give you a value
right away, but would give you like a
1:09:48tuple of like either it's loading or
a value is like we're already halfway
1:09:52there just in, in such a bad direction.
1:09:55So I, uh, I think this is kind of a
subtle difference for people to understand
1:10:00how much synchronous code execution
can simplify your app development.
1:10:05But I think I'm, I'm preaching
to, to the choir here.
1:10:09Yeah, sure.
1:10:10Totally.
1:10:11I mean, it helps with debugging.
1:10:12Like if you're stepping over code,
whenever you're hitting like async
1:10:15code, Chrome tries to do this thing
where it like will step over the await,
1:10:19but it only works like half the time.
1:10:21Like it's yeah, it's super annoying.
1:10:23I do get this needed probably most of
the time, but it makes me sad when I just
1:10:27see it like applied without any thought.
1:10:29So after fulfilling your goal with
bringing Actual to the web through
1:10:35absurd-sql, which I think is like
just to look back, like how much
1:10:39pioneering work you've really
done to make this app happen.
1:10:42Like you started this journey before.
1:10:45The term local-first was there.
1:10:47You've built one of the first credible
local-first apps and really invented
1:10:51so many things along the way, like all
by yourself with the help from, from
1:10:56some of your, your friends, but you,
like you figured out like how to use
1:11:00SQLite in like even in a web context in
a reactive way, but then also made it
1:11:05collaborative through your sync engine and
ultimately brought all of this to the web.
1:11:09That is an impressive journey.
1:11:11And I think you've been on the journey,
not just building all of this full
1:11:15time, but you actually had like some,
some, some Actual full time job next
1:11:20to that, which I think at some point
was just too much, which led you to,
1:11:24to at some point, uh, hand over the
project to the broader community.
1:11:29Can you share a little bit
more about that transition?
1:11:32Sure.
1:11:32Absolutely.
1:11:33So I, there was about three or four
years when I was doing consulting work.
1:11:37And that was during that time was around
when I started it in 2017, around 2019,
1:11:43I think was the year that I full time
just didn't do any consulting work.
1:11:46And I was like, I'm just going
to give this a try to see if I
1:11:48can build this out as a business.
1:11:50And it got moderately successful.
1:11:51I think I got up to around 800
users, I think at, um, at the height.
1:11:55And I was only charging 4 a month, which
again, I had no idea what I was doing.
1:11:59Um, so too low, but like, you
know, like if you do the math,
1:12:03that's like, that's not terrible.
1:12:05A lot of people aren't
even able to get to that.
1:12:07Like that much, like, but it wasn't even
close to becoming like a full time thing.
1:12:10I honestly look, looking back and
all the experience that I have now
1:12:13too, I, I, I could have built it out.
1:12:14And especially if like Mint shut
down now, like there's so many
1:12:17things that I've missed that I, I
probably could have built it out.
1:12:20But, um, building a business
is really hard though.
1:12:22And it's, I think I realized that I
also had a hard time finding people
1:12:27to kind of like work with me on it.
1:12:29Like I kept trying to, people were like
sort of interested in helping and which
1:12:33made this kind of weird dynamic where
There is like four times when I tried to
1:12:37allow contributors, uh, like without pay,
they just wanted to help, but without,
1:12:42without paying them, it's just weird.
1:12:43Like, they would do something for
a week and then be gone for two
1:12:45weeks and not even like speak to me.
1:12:47Like, I can't operate like that.
1:12:48I'm operating a business here.
1:12:49So I try to like, I try to.
1:12:51Think about how I could hire a developer,
but I was so busy with everything else.
1:12:56It just felt, I had no
idea how to filter through.
1:12:58I don't know, should I use like,
you know, one of those like Upwork
1:13:01or other things I had no, this just
seemed like so full of spam and, and
1:13:05people who had no idea what they were
doing that it just was overwhelming.
1:13:08I never figured out how to hire people.
1:13:10And that was, that's my biggest regret.
1:13:11Probably it's not figuring out how
to involve people in a way that
1:13:14would help share some of the burden.
1:13:15And so that, it, Would help it a little
bit, be a little bit more sustainable.
1:13:19And I was just, I was ready to work with
people again, work at a company that
1:13:22was like, I could grow from grow, how
to like, learn how to lead people and
1:13:25learn how to, you know, lead teams and
do like product requirements and kind
1:13:29of focus more on just what I'm good at.
1:13:31And so ultimately, yeah, I just
wasn't, I wasn't serving Actual well,
1:13:35I wasn't serving so that I did that.
1:13:37Well, I did do that for a whole year.
1:13:38which was, which was good, but
it, it, I wasn't growing enough
1:13:42for it to be like a real thing.
1:13:43And so I got a little burned out on
trying to make it like a full time thing.
1:13:48Um, and so I was talking to a
friend who referred me to Stripe.
1:13:51And so I got hired at Stripe.
1:13:52I did try to work on it kind of
like in the mornings for like an
1:13:55hour or two and then work at Stripe.
1:13:57But obviously like that, just when I
got more and more involved in Stripe
1:14:00and doing like really interesting things
there that kind of absorbed my time.
1:14:04And so two years ago, I just was like,
I'm not serving Actual well, I'm not.
1:14:08Spending enough time there.
1:14:08I'm not serving stripe.
1:14:09Well, cause I'm not spending enough time
or like, it was too tiring to like do
1:14:13both and it wasn't serving like my family.
1:14:15I wasn't serving my
own personal interests.
1:14:17Well, either it's just like too
way, like way too overinvested.
1:14:20So I decided to, to
not work on it anymore.
1:14:23And so the choices was either sell
it or open source it, or just.
1:14:26Shut it down.
1:14:26I investigated selling it a little bit.
1:14:28And this is like, again, some of the
downside of, of doing such a novel app is
1:14:31that, um, I talked to a couple of people.
1:14:33It's just so clear that nobody
had not much interest in it.
1:14:36One, one, it just wasn't making enough
money, but two, like even starting
1:14:39to explain how it worked, that
just, it's not a, you know, People
1:14:43aren't interested in buying that.
1:14:44Like they're interested in buying
like the super run of the mill apps
1:14:47that they, that they can take and
grow from a thousand to 10, 000 users.
1:14:51So this app was not that app at all.
1:14:53So selling it was not in the cards
and with the money it was making, it
1:14:56wasn't going to sell for that much.
1:14:57So I decided to open source it and
it was a decent amount of work.
1:15:00It was decent amount of work
for me for like the year after
1:15:02that too, to help transition.
1:15:03Doing it.
1:15:04And sometimes I was like,
should I have just shut it down?
1:15:06But I'm really glad I did because
now it's running entirely on its own.
1:15:09Like I, I'm, I'm not even
on, on the discord anymore,
1:15:12which I probably should be.
1:15:13Honestly, I was starting to get
involved in it again in January.
1:15:16And then like a work thing
happened and totally absorbed
1:15:18my time for the last two months.
1:15:20So I will, I want to start interacting
with the community again, but there
1:15:23there's, they seem to be, they
seem to be running it really well.
1:15:26And so it's, it's been great.
1:15:27They've.
1:15:28They've been taking it and
like fixing a lot of bugs.
1:15:30And like it, there's definitely like a
power of, of, of open source at work.
1:15:35It's, it's messier.
1:15:36I honestly think that there's things,
parts of the app that don't look as good.
1:15:39They're, they're not as clean, not as,
not as polished, not as thoughtful, but.
1:15:44There's a lot of added functionality,
a lot of stuff that is good and a
1:15:46lot of stuff that is improved too.
1:15:48Thank you so much for, for
sharing this entire story.
1:15:51I mean, I have massive respect to
you of how you've like navigated
1:15:55this entire journey and takes a lot
of, I think there's not just on a
1:15:59technological level, there's like doubt.
1:16:01It's just like, is this possible?
1:16:03Does this make sense?
1:16:04But you're like building a
product, you're building a company.
1:16:07So there's a lot of uncertainty if you're,
if you're not in a full time job where
1:16:11someone else Takes care of the things
and you have your responsibilities.
1:16:15Here's like, everything is uncertain.
1:16:17And so navigating that while
also like having a family and
1:16:21other responsibilities, that's,
that's a lot of commitment.
1:16:25So I have a lot of respect for
like, Not just the decisions you've
1:16:29made, but like also , how thoughtful
you went about those transitions.
1:16:32Like how much you, like you said, like
you owe the community, like an update.
1:16:37I don't think you owe the community
anything you've given the community
1:16:39a lot, but I think all of your
contributions are really well received.
1:16:44And, and I think highly
valued by a lot of people.
1:16:47And so I think even if Actual now did
not work out as like that, uh, That
1:16:52billion dollar startup by itself.
1:16:55Uh, I think it will actually influence
a lot of those and who knows, maybe
1:16:59you take another stab at the same thing
at another thing in a similar way.
1:17:04And I can't wait to hear sort of what
you'll be innovating on at that point.
1:17:08You've mentioned that every four
years of like, you're, you're staring
1:17:11at a problem where you feel like,
ah, there has to be a better way.
1:17:15So can't wait to see which sort of absurd
things you might build in the future.
1:17:20So you mentioned that right now
you're at Stripe and I think at Stripe
1:17:24you're working on design systems
and like more UI related things.
1:17:30So which sort of things are
you, are you working at Stripe?
1:17:33Yes.
1:17:34So I work on our design system.
1:17:36It's called SAIL.
1:17:37And so SAIL recently, it used to just be
a single team called the design system
1:17:41team, but now there's been changed into
like a larger org, which is really cool.
1:17:45Exciting in some ways, it kind of, you
know, messed up our team a little bit.
1:17:49Some, some people got moved
around, which was, we had to kind
1:17:52of work through that, but we did.
1:17:53And like, there's a wholesale
org now, which is great.
1:17:56So it's like a, a bigger investment.
1:17:58SAIL itself is becoming this thing.
1:18:00That's actually more than
just a UI design system.
1:18:02We're also starting to integrate other.
1:18:04Concerns that you always hit when you
start doing front-end work, which is like,
1:18:08how do you internationalize something?
1:18:09How do you, how do you do observability?
1:18:11How do you do like GraphQL queries or how
do you just fetch data like in general?
1:18:15And so SAIL is becoming like
a bigger platform overall.
1:18:18I am focusing more on the, on the
UI design system part, but I'm
1:18:21also collaborating a lot with the
others as well to sort of integrate
1:18:24this into a cohesive platform.
1:18:26Internally, we have a, a.
1:18:27A whole system for like
variants and tokens.
1:18:30And we have a, like a
view and a, and a CSS API.
1:18:33And it's something that I don't
think we've talked a ton about.
1:18:36I think we really should talk
about it more, um, openly.
1:18:38And cause it's, it's, it's pretty neat.
1:18:40We try to.
1:18:42Use third party libraries when we can.
1:18:44We actually leverage a lot of React Aria.
1:18:46So a lot of our components
use, use React Aria.
1:18:48Yeah.
1:18:48We actually use the, also the like
lower level hooks API, which is like in
1:18:51some ways really cumbersome to use, but
like, that's kind of intentional, like,
1:18:55because it gives you like direct access
to the entire way that the things work.
1:18:59They now have a higher level API, like
the React Aria components API, which
1:19:02is all like really, really great.
1:19:03But we do things with React Aria
that I don't think anybody else does.
1:19:07I think that like, there's like
classes that we import and use.
1:19:11That, like, we've talked to the
ReactARIA team and they're like,
1:19:13nobody else has imported that class
and, like, used that directly.
1:19:16There's, like, a list, like, a list
collection class that is, that it uses.
1:19:19And we import that and create
our own collection system.
1:19:22And, like, cause, like, Stripe
has pretty specific needs, right?
1:19:25And so what I love about ReactARIA
is that it's taken this, like,
1:19:28unabashed approach to being cumbersome.
1:19:31And it's, like, kind of intentional.
1:19:33Like, if you look at the docs for some
of the hooks, like, it's a lot of code
1:19:36to get a, Menu working, but because it
is that openly exposed, you have like
1:19:43total control over how everything works.
1:19:45So it's really allowed us to go
in and really, really wire things
1:19:47up the way that we want to.
1:19:49So yeah, it's, it's great.
1:19:49It's a fantastic library.
1:19:51It's super, super good.
1:19:52Um, lots of good accessibility
that, that you get from that.
1:19:55But that's our component system, right?
1:19:57We still have a lower level,
like view and CSS APIs.
1:20:01And so how you do CSS, how you do tokens,
how you, how you do variants, how do
1:20:04you make sure that you, when you, uh,
create a new component, that everything
1:20:09is wired through such that like, The, um,
like if you pass styles to it, but you
1:20:15also internally want to style the, the
same div, like, like the same top level
1:20:19div that you take the styles from your
props and you apply styles to that div.
1:20:23But like in that component,
you also want to apply styles.
1:20:27Do that div.
1:20:27How do you, how do you
combine those styles?
1:20:29I guess you could like spread on the
styles that you get from the props, right?
1:20:33And then spread on your
own styles as well.
1:20:35But then you get into like
precedence problems, like which
1:20:38order do you spread those on?
1:20:40So we have a whole way of like creating
a component, a whole pattern for
1:20:44taking the props that you get and.
1:20:46And spreading, and like, spreading
those props onto some internal element.
1:20:50And we have a very strict precedence
order for how styles and how
1:20:54variants and, and how other things
like that get merged together, um,
1:20:58in a way that's a very intuitive.
1:21:00Um, and it's all very, very consistent.
1:21:01So we have like a, an
API called create view.
1:21:04So that's how you create a new component
that interacts with our whole system.
1:21:07When you call create view, you get back
an exact same React component, just like
1:21:11you would call it forward ref, right?
1:21:12Like it's, it's taking a component, but
it's giving it additional capabilities.
1:21:16And the, the, the two things, like
there's a couple of things that
1:21:18it gets, it gets like a CSS prop.
1:21:20So we, we pass CSS by, by saying
button CSS equals, and then an object.
1:21:26And in that object, we have a
whole like little system that
1:21:29is wired up with our tokens.
1:21:30And so you can say margin
is, you know, small.
1:21:34And small gets resolved to a token.
1:21:35We also do like things where you
can, um, say like small is eight
1:21:39pixels and we have an ESLint rule
that automatically knows that for
1:21:43the current theme and for the current
system wired up, small is eight pixels.
1:21:48So you've hard coded a token here and it
automatically actually fixes that for you.
1:21:52So it's changes eight pixels to small.
1:21:55So we, we try to let developers build.
1:21:58Like not get in their way.
1:21:59Basically, we really want them to
sort of, we have the same, like fall
1:22:02into the pit of success where you
should go off and build a product
1:22:05the way that you want to build it.
1:22:07And sale should meet you where you
are there and like, let you build
1:22:11components that end up getting wired
up the way that we want them wired up.
1:22:15But just because that's the
The way that feels natural.
1:22:18Like you shouldn't have to feel like
you have to like go against our system
1:22:22and like begrudgingly use it to like
be conformed to Stripe's design system.
1:22:26Right.
1:22:26We want you to be like happy using it.
1:22:28And so a lot of our work goes into
like making the APIs feel good.
1:22:32Yeah.
1:22:32There, there's a lot of stuff here
that I think we should talk about more.
1:22:34Like we have our own variant system
and our whole, like how tokens are
1:22:37wired up is really, really interesting.
1:22:39Cause one thing about Stripe.
1:22:41It's a very, very broad company.
1:22:43We have a lot of different products.
1:22:45We have the dashboard.
1:22:46We have connect.
1:22:47We have like people taking
pieces of functionality and
1:22:50embedding it in their own website.
1:22:52And then they're theming that
content, but it's like exactly the
1:22:54same little widget that you would
take from the dashboard, right?
1:22:57It's like a payments list that
you would take from the dashboard
1:22:59and embed into your own page.
1:23:01And then we also have like custom hosted.
1:23:04Invoices and a bunch of other
little like third party things.
1:23:06Then like checkout and like elements
as a whole, another thing as well.
1:23:09We're trying to bring all of that
together into us and to use the same
1:23:12design system that can be themed and like
leverage and customized for each surface.
1:23:17And so it's really hard problems.
1:23:20Like honestly, and it's, it's, it's
a lot of work, but it's really fun.
1:23:22That, that sounds fascinating.
1:23:24Uh, I feel like I want to learn a lot
more about how to do design systems.
1:23:29I should also educate myself more of
like, should I use a design system
1:23:33even for a smaller scale products?
1:23:35For example, would you now
looking back, would you've used
1:23:37a design system for Actual?
1:23:39Is this like, rather like, does it
solve an organizational problem or does
1:23:43it really help even on a smaller scale
when fewer engineers are involved?
1:23:48But I most certainly love the design
principle of like the pit of success
1:23:52that can't go wrong with that.
1:23:54Yeah, it's great.
1:23:55It's, it's been a good, like a
good way to frame things for sure.
1:23:58So you're building with React,
uh, Stripe it seems, and you've
1:24:03also used React for Actual.
1:24:05React has changed quite a bit over
the years, so I'm curious to hear
1:24:09whether you have any opinions on
where React has gone over the last
1:24:13few years and where it's going.
1:24:15I don't have super strong opinions
only because I feel like I
1:24:18can't back them up right now.
1:24:20I haven't given it the time to
sit down and really like write
1:24:23out like a thorough argument for
why I should feel a certain way.
1:24:27I think it's been hard for just
in general, like everybody.
1:24:29I respect the team in a
lot of ways because I think
1:24:32they have a high bar, right?
1:24:34Unlike many other teams.
1:24:35Developers, generally speaking,
I feel like we don't have a high
1:24:38enough bar and I feel like the
React team has a high bar and like,
1:24:41ultimately, I do respect them for that.
1:24:43I do think there's, there's been
times when it's just like very heavy
1:24:47investment and very complicated
things when it feels like there's
1:24:50lower hanging fruit, which are like.
1:24:53Man, this really sucks to have to do this.
1:24:54Like every single time sucks to have
to use forward ref every single time.
1:24:58Why can't just ref be a freaking prop?
1:24:59And like, I know that there's like
backwards compatibility problems and
1:25:02like all sorts of reasons, but it, when
like two years are spent on this really
1:25:05complicated thing, and like, we're still
hitting these problems on a day to day
1:25:09development thing, sometimes there's a
little dissonance there that can be a
1:25:13little bit like, ah, you know, we also
don't pay for react, it's almost like.
1:25:17I don't know, like we're
getting it for free.
1:25:18It's up to them to ultimately decide.
1:25:20We're not, they're not
forcing us to use React.
1:25:22There's a little bit
of a lock in for sure.
1:25:24Like I don't know how Stripe would
possibly move away from React, but
1:25:27ultimately like the members of the
team, I respect really, really well.
1:25:30And I'm not going to say
anything like super bad about it.
1:25:33I think react server components
is really interesting.
1:25:35Again, it's like, sure, like the
local-first stuff might be able to
1:25:38be cleaned up a little bit with it.
1:25:39Like, there's kind of some interesting
things there where like, maybe you
1:25:42could like run components on the server.
1:25:44Like, even if it's just like a
backend web worker process, but
1:25:47the wins are much less for sure.
1:25:49Like.
1:25:50Everything's already local.
1:25:51I don't care if it goes
through a WebSocket message.
1:25:53Like I can, like, you
can embed SQL queries.
1:25:55Like we're embedding SQL queries already.
1:25:57We don't need React
Server components for it.
1:25:58So it's less convincing to
me if you're already doing it
1:26:01the way that we're doing it.
1:26:02But for Stripe, like React
Server components could be
1:26:04potentially pretty compelling.
1:26:06But it's still, again, overall,
like, Man, I just want support
1:26:10for exit animations, right?
1:26:11Like I want the ability to not have
to wrap something in animate presence
1:26:15to just freaking get something to
like maintain it in a dom while it
1:26:19animates out and then unmount it.
1:26:21Like React still doesn't
allow you to do that.
1:26:23Like exit animations are
a huge pain in the butt.
1:26:25Yeah,
1:26:25I, I fully agree.
1:26:26And I think the React server components,
what you've mentioned, I think the,
1:26:30the way from my current perspective,
how it would most meaningfully help
1:26:34in a local-first context as well.
1:26:36is on the initial upload experience,
since that is sometimes a bit of
1:26:41like the cost that you're paying that
you're, you're kind of with local-first,
1:26:45you say like no more spinners.
1:26:47Well, sort of, since you have like one
front loaded, typically larger spinner.
1:26:52And I think this can be addressed also
with react server components, where
1:26:55you get sort of an hybrid approach.
1:26:57Where you get much more quickly reactive
initial version of the app that then
1:27:03upgrades itself to be local-first.
1:27:05So that, that's my take on react
server components, but the other pain
1:27:09point that you've pointed at with
like exit animations, et cetera, and
1:27:14where this is kind of where we are
now paying the cost for virtual DOM,
1:27:18which constantly updates everything.
1:27:20I've had actually some, some
interesting results now with just
1:27:24using web components for that.
1:27:26Where it's a bit funny since like, in
some regards, it feels like going a bit
1:27:30back in history where your DOM is more
static in a way, but this way you actually
1:27:36also can think a little bit differently
about like, for example, animations.
1:27:41So, and this is where I feel also on
the general theme of learning more from
1:27:47other native programming environments,
other native platforms, where for
1:27:52example, on iOS, when you have like.
1:27:55Things like a collection view or
a table view controller where you
1:27:58have those cells and they're like,
they don't constantly cycle out.
1:28:03They cycle out if you're like, if they
leave your view, but based on like
1:28:07entering the view, leaving the view,
this is how we can do animations.
1:28:10And with an approach like web
components, you can actually do that by.
1:28:15Using the native aspects of the web, like
native to the platform web, much more.
1:28:20And I feel we're now fighting
a bit of an uphill battle to
1:28:23get those benefits from React.
1:28:25Agreed.
1:28:26Yeah.
1:28:26It's interesting.
1:28:27There's a decent escape
hatch there, right?
1:28:29Where you can mount a div and then in
an effect, like you can do whatever you
1:28:33want there, which is, which is nice,
especially for a company like Stripe,
1:28:35because we can sort of like opt out when.
1:28:38Needed, but something does like, there are
certain things that are like systematic.
1:28:42And, you know, as something, somebody
who works, works on a design system,
1:28:44seeing like animations are not something
that you can just like opt out for.
1:28:48Right.
1:28:48That's something that you apply on
like almost every single, I don't
1:28:51know, like a 10th of the elements
on the page, which is a lot.
1:28:55have animations.
1:28:56You can't just like opt
out when you want to.
1:28:58So it's, it's hard.
1:29:00Yeah.
1:29:00I would love to see
some improvements there.
1:29:02Just recently on my blog, I have quick
and dirty type thing, but like I don't
1:29:05use re I use react, I use, um, Remix.
1:29:08And so I use reactor on the backend,
but I don't use it on the front.
1:29:10It just feels so fast and nice.
1:29:11It's just a.
1:29:12Just don't do it just content, but like I
am starting to add more interactive parts.
1:29:16And so I'm sort of playing around with
like, well, okay, what do I do here?
1:29:20Do I need to load in the full react?
1:29:22Maybe this is where like partial
hydration could be interesting.
1:29:25Uh, but like, I just don't, it's my blog.
1:29:26I don't need partial hydration on
my frigging blog, but I do want
1:29:30to be able to like, um, there's
like, I have like live interactive
1:29:33demos and I want to be able to.
1:29:35Have a view source button.
1:29:36And when you click that, it opens up
this, like, it basically like zooms
1:29:40into the interactive demo to where it
shows the demo, like, like the demo is
1:29:44still running, but it's running in a
dialogue and the demo is run on the left.
1:29:48And then on the right is the code
for the demo and then all sorts
1:29:51of other controls for the demo.
1:29:53And the way that I implemented that
was I literally transport that real
1:29:57Dom node, I take the Dom node instance
itself from the, like the whole
1:30:01demo Dom node, and I, I move it.
1:30:04I create the dialogue and I put
the code on the right, and I moved
1:30:08the DOM node into the dialogue.
1:30:10And the demo just keeps
on running just fine.
1:30:12Like it, it moves into the dialogue.
1:30:14And I did that in like 10 lines of code.
1:30:16And there is a certain amount of
like, Oh man, it feels so good to be,
1:30:20yeah, I just have access to like this.
1:30:22Dangerous, dangerous API APIs, but like
I, to do this in react would have felt
1:30:26really backwards, like bending backwards
and doing all sorts of weird things.
1:30:30And then you can like, and you
close it and it moves it back and
1:30:32it just keeps on running just fine.
1:30:33Right.
1:30:33And I, I don't know, to, to, to think
about it in the react mental model,
1:30:36I would have been like, Oh, okay.
1:30:37I need to create like a portal,
create a bunch of components and
1:30:40like wire them all, I don't like
just saying like dom node, remove
1:30:44node, and then like demo dialogue.
1:30:46append.
1:30:47Uh, I completely agree.
1:30:48I feel like we've been now over the last.
1:30:51React has been now around for 10 years.
1:30:53I feel like over the last 10 years,
we've been really leaning to use the
1:30:59React hammer for like every little thing.
1:31:01And I think that brought a whole lot
of benefits to us, but I think we
1:31:06don't question it really anymore.
1:31:08And like, we, I think this is
like the only way to go about it.
1:31:11And the web has since then really evolved.
1:31:14Like we've gotten a lot more,
the primitives in the web
1:31:18itself have gotten a lot better.
1:31:20And so I think web components
have also come quite a long way.
1:31:25They haven't seen as much investment
in terms of building a nice ecosystem
1:31:29around it, but I think it's the closest
we got to a native feel in the web.
1:31:35And, uh, I think sometimes you'd be
surprised how simple things can be if you
1:31:40embrace those primitives more directly.
1:31:42One little anecdote I want to share
there, a friend of mine, Cheng Lu,
1:31:45who's I think also worked on the React
team for a while, he did a couple of
1:31:50really fascinating demos and I think he
launched some of them on, on Hacker News.
1:31:55Where he, for example, built a photo
gallery and a Hacker News clone as well.
1:32:00And so when you try those apps, those are
the smoothest animations and the smoothest
1:32:05feel you've ever seen on the web.
1:32:07And you kind of feel like,
what is going on here?
1:32:10Is this like rendered to web
GPU using Wasm or something?
1:32:14And turns out this is all
just normal DOM, normal CSS.
1:32:18He's, he's built this like
very simple, elegant system.
1:32:21I think the photo gallery is like in
a hundred or 200 lines of JavaScript,
1:32:26like no dependencies, nothing.
1:32:28And this feels like a total native app.
1:32:30So this really reminded me of like
how capable the web really is.
1:32:34If we use it directly without
layering too many things in between.
1:32:39I have high hopes for, for the web.
1:32:41If we set up, set ourselves a high bar.
1:32:43Same.
1:32:43Yeah.
1:32:44And I think React needs to figure out
how to like move with the web without
1:32:48breaking backwards compatibility,
find the right trade off there.
1:32:51Like, I guess even the like web
component support type stuff in that
1:32:54has been like super long in the making.
1:32:56So I think that's something that.
1:32:57Could be improved, but
1:32:58hey, James, you've been very generous
with your time and walking us
1:33:02through like your entire journey of
the, the various chapters of Actual
1:33:06from Electra and the mobile apps,
the web, absurd sequel, et cetera.
1:33:11So thank you so much for your time.
1:33:14If there's anything you want to share
with the audience here, any sort
1:33:17of shout out, now's a good time.
1:33:19Uh, cool.
1:33:19Yeah.
1:33:19Thank you for having me on.
1:33:20This was amazing.
1:33:21I mean, honestly, shout out to
Martin Kleppmann, PVH, Peter.
1:33:25I found them through
their local-first essay.
1:33:28And I, I came and give like a, gave like
a workshop at their research studio and
1:33:33like talk to them a bunch since then.
1:33:34And they've always been very encouraging
throughout the whole process.
1:33:36So super fun to talk to
them about all this stuff.
1:33:38Awesome.
1:33:39Thank you so much for your time
and coming on the show today.
1:33:42Thank you so much.
1:33:43Thank you for listening to
the localfirst.fm podcast.
1:33:46If you've enjoyed this episode and haven't
done so already, please subscribe and
1:33:50leave a review wherever you're listening.
1:33:52Please also tell your friends about it.
1:33:53If you think they could be interested
in local-first, if you have feedback,
1:33:57questions or ideas for the podcast,
please get in touch via hello at
1:34:00localfirst.fm or use the feedback form on
our website, special thanks to Expo and
1:34:06Crab Nebula for supporting this podcast.
1:34:09See you next time.