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