A podcast about local-first software development



#7 – James Long: Actual Budget, Hybrid Logical Clocks & Absurd-SQL

The guest of this episode is James Long, the creator of local-first app called Actual Budget and the absurd-sql project which helped to pave the way to bring SQLite back to the browser. This conversation will explore his journey of building Actual Budget including implementing a syncing solution from scratch and expanding from an Electron app to mobile and the web while re-using most of the code. 

Mentioned in podcast


Thank you to Expo and CrabNebula for supporting the podcast.


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: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: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: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.
SQLite as Data Layer
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?
Best Practies
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
Hypological Clocks
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.
Merkle Trees
25:08Let's get into it.
25:09Uh, how Merkle tree is fitting in here.
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.
Beyond Electron
31:54from here?
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
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: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?
Working on mobile apps
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: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: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.
Bringing the app to the web
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: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: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: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: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: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: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: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: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.
Transition to Open Source
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: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: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: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: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: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
Opinions on React
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: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: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: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: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: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: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: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: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: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: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: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.