The Recipe for Local-First
Alex Anderson provides a thorough examination of "local-first" computing, tracing its origins from the early days of mainframe and client-server models to present-day mobile apps and edge computing. He emphasize how the trend towards thicker clients and localized tasks is affecting web development.
Local-first computing has seven ideals that it lives by that Alex dives into in this talk:
Easier ideals:
- No spinners
- Data not trapped on one device
- Network is optional
- Seemless collaboration with your colleagues
Harder ideals:
- The long now
- Security and privacy by default
- The user retains ultimate ownership and control
Discussing the principles involved and their development implications, Alex considers the challenges of data synchronization, the necessity of offline functionality, and the complexities of multi-user scenarios. He also introduce resources for local-first software development, such as AutoMerge, Yjs, and localfirstweb.dev.
Alex uses a recipe app to demonstrate how local-first computing can be effectively implemented. He highlight the use of RepliCache, a client-side sync framework, and discuss strategies to improve web app performance using Remix.
The talk concludes by exploring how web apps have serious limitations, even when implemented as Progressive Web Apps (PWAs), when examined under the lense of the local-first ideals. Web apps particularly struggle to uphold the last 3 ideals as data on the web is not permanent and hard to properly secure. Alex remains optimistic about the future of local-first computing in web development seeing them as levers to pull when needed, encouraging developers to explore these techniques to enhance user experience and data control.
Share this talk with your friends
Transcript
Hi, everyone. My name is Alex. I work at an agency called Echobind, building fancy websites.
And today I want to talk to you about the recipe for local first. And I'm going to start at the end and spoil the whole talk for you. Time is a flat circle. Since the invention of the computer, we have gone through cycles, shifting where the compute and data is for
doing compute things, computer things. First, we had timed, time-shared mainframes. Then personal computers brought computation into the home. Then the web and the cloud moved things back into servers with browsers as thin clients. And at the same time, apps and
smartphones pulled it back onto devices. And then edge computing is trying to blur the line between client and server altogether. But until the Internet is as ubiquitous as the air that we breathe, there will always be a line between server and client. And I
kind of see a trend back towards thick clients. I like to cook. But I hate meal planning. And I needed an easy way to have all of my recipes, to put them in a meal plan for the week, get all of the groceries that I need, and then take that to the store so that I can buy my groceries. And there are apps that do this, but I decided I wanted to build it
myself because I am not a normal person. And I used Remix, added progressive enhancement. And it was great. One day, I was shopping for groceries, as one does. I had just grabbed some cilantro off of the shelf and tapped the button to mark it off when I was greeted
with a server error, a connection error, actually, because I couldn't connect to the server because I had lost my cellular connection. And I couldn't go back to the grocery page because I didn't have proper caching in place. And it was infuriating. I literally abandoned my cart, not a virtual
cart, a real cart, abandoned, in the store, went outside to get service again, pulled up the page, and then didn't push any buttons for the rest of my trip. And this presented me with a tricky problem to solve. I could have just cached all of the data that I needed
on my device, but I'm also dealing with updating the data when I check off my grocery list. And I used the app both on my computer and my phone, and my wife also uses it. So multiple users, the app needs to work completely offline, but I still want to server-side render some
of the pages for SEO. And so finding a way to meet all of these constraints, I fell into the rabbit hole of local first, where the communities of cryptography, edge computing, deprecated browser features, decentralized data, PWAs, mobile apps, and real time all
converge. It was a trip. So what is local first? Well, back to the beginning, you have your server, which has its database, and browsers connect to it to get data. And we started with just one server and one database, and then we're like, well, what if we put
servers closer to users? And so we did that. We put the servers closer to users, they're talking to a centralized database. And then we thought, well, what if the data was closer to the computers, to the servers? So we split our database and have multiple databases that synchronize with each other. And this is being pushed to the limit with edge computing and
edge databases, where your data is as physically close to users as possible without actually being on the device. The only problem is this falls apart as soon as the device goes offline, because we can't access those servers. How many of us have ever lost internet as we are
hiking a mountain, or on an airplane, or on a subway, or a cruise, or wherever? You lose internet all the time. No, for this to work properly, for users to be able to access their data and the apps whenever they want, it needs to be as close to them as possible on
their device. And we have all used apps that do this. Two very great examples, Apple Notes. Who's used Apple Notes before? Works great. Even when I was telling my Lyft drivers about this talk, I told them, Apple Notes is a local first app. And they're like, oh, I get it.
Git as well, local first. You can use it, but you can also synchronize it with your friends or coworkers, as the case may be. Local first itself was described in detail. This is actually the first time the term was used, as far as I know, by this ink and switch
paper published in 2019. And in it, they outline seven ideals which local first apps should strive for. No spinners. Your work should be at your fingertips. But not trapped on
one device. The network is optional. Seamless collaboration with your colleagues. The long now, which is another way of saying being able to use the app long after the people
who made the app don't support it anymore. Security and privacy by default with end-to-end encryption. And the user retains ultimate ownership and control. And we can plot these on a spectrum of how difficult they are to implement, especially in web apps. These ones
over here, nigh impossible. And we'll get to that towards the end. And because of that, you shouldn't feel obligated to use all of these anytime you're building any kind of app. You can pick and choose which ones make the most sense and which ones work for you.
But as you go closer, you get as you go further up this scale, you get closer to that ideal local first app that Incanswitch was describing. So let's start with Spinnageddon. Hopefully we've seen this page from the Remix website. If you haven't, go there right now and scroll
down. It's beautiful. And the point is that Remix has helped to remove spinners from apps by parallelizing loaders and making it so that your apps include fetching and sorry,
caching and optimistic updates so that we can avoid showing these spinners whenever possible. And Ryan has demonstrated this. Ryan Florence, if you don't know who that is, he's demonstrated this further with a Remix video library single where he was able
to almost instantly search the entire database from his browser using a client loader, which is a feature in Remix that lets you call loaders that only run in the browser. They don't run on the server and a client side cache, which the client loader was fetching from this right
here. This screenshot in the video is the part where he's demonstrating typing in the search box. Every time he types, it's hitting his server and coming back with the search results. But eventually it stops. Instead of hitting the server, that very I guess it's
the third request for allmovies.json has downloaded all of the movies into his browser's local database. And now it's hitting that local database instead of the remote database. If you haven't seen this, I'd highly recommend checking it out. It uses local storage. Sorry,
index DB. And it's a fabulous example of how you can take data and store it on your device for really fast searches, filters, whatever. And hey, now that we have our database on the client, offline support is much easier. You just make it a PWA by throwing a service
worker on there. You cache the code and you're good to go, right? Well, that works fine when you're just reading the data. But hardly any apps are read only. Remix itself made mutations a first class citizen with actions. So we make an action to the server. The server calls
the action, sends back the action response, and then we make a request to get all of the revalidated loaders so that we have the freshest data possible. And we shove that inside of our local database so that we can access that locally as needed, right? This is a great user experience, especially when only one person is changing the data at a time, like
for a site owner of a CMS. But what if we have multiple users who are using the same app at the same time? And it's possible that they could be offline at certain times. And
this can introduce conflicts in their local databases. As they're offline, they make changes to the local database because they can't access the server. But those changes might be different from what the server is expecting. And they might not even be two different people's devices.
They might be your devices, just one is your phone and one is your computer. And then when we finally send the request, the responses, sorry, the mutations to the server, the server's like, what am I going to do with this? I got two different mutations that update the
same data. What do I do? And what actually happened here is as soon as our devices went offline, we distributed our system. And yes, that means we need to talk about the cap theorem.
If you don't know what this is, it is some deep magic of databases, where you can only have two of these three things. Availability, which means you are able to make writes to
your database. Consistency, which means two databases have the same data at the same time. And partition tolerance, which means your databases can handle being separated from each other, being offline for a time. When you have any kind of distributed system, you
will always have partitions at some point in time. It is just the nature of entropy and the universe. So that means you have to pick CP, where everything is consistent, but you aren't able to make writes at certain times. This is the default of the web, because if you go to a web form on most normal websites and you submit it while you're offline,
you're going to get a connection error like I did. Or AP, where you have the availability to be able to send mutations to your local database, but that will then create inconsistencies with the server database. So we need some way to synchronize data between clients,
and some way to manage these conflicts. And this is a really tough problem to solve, but fortunately there are tons of tools popping up all over the place to make this easier. I'm not going to go over all of them, but if there is one URL that you need to remember from this talk, it is localfirstweb.dev. That's localfirstweb.dev. It is a great resource for all kinds
of local first things, and all of these tools are listed there, and you can go research them yourself. One last time, localfirstweb.dev. I do want to highlight two of these, though. They are special. This is AutoMerge and Yjs. They are implementations of CRDTs. That's conflict-free
replicated data type. This is some fancy mathematics that makes it so that you can have two people updating the same data at the same time, completely separated from each other, and when that data comes back together, it's able to figure out what the conflict resolution should
be. I'm not going to go over how that works exactly, but these are the two biggest libraries in JavaScript for doing that, so check those out for sure. Okay, so we've got a local cache. We've got a sync engine that's able to handle conflicts, and adding real-time at that point is actually
relatively easy. You connect your sync engine to a web socket or server sent events endpoint, make it work with those CRDTs, and now you've got collaboration. However, we run into issues. We need to be aware that most, if not all, local-first tools are client-centric, so don't
expect to be able to do server-first and local-first at the same time. Some don't even let you access your sync data on the server, so it won't work with SSR at all. All the server is good for is synchronizing data between different clients. Remember, thin server, thick client. Some
even treat the server itself as a specialized always-on client, which is actually a good mental model for keeping track of these things, but patterns like SSR break down in that paradigm. So let's come back to my Recipe app and talk about how I made it local-first. Remember, it already existed as an app with its own database and everything, so I couldn't just
make it local-first. I still had to keep the server without rewriting the whole thing. And so I decided to use a tool called RepliCache for handling this. They call themselves a client-side sync framework that works with most back-end stacks, and hey, that's what
I need. It makes very few assumptions about your app. First, it needs to run on the web, and second, it needs to expose a couple of HTTP endpoints. Push is for when the client tells the server about any pending mutations that happened while it was offline. Pull for
synchronizing the current server state back to the client and storing it in the local database, and then Poke, which is a real-time endpoint called by Push to let clients know that they should call the pull endpoint and get updated data. So I added these endpoints to my app and hooked it up to my existing database. And what's cool about this approach
is since the data is still mine on the database, I can use Remix's server-side loaders for fetching initial data to generate my HTML, so I still get SEO, but then I can use the client loader with client loader.hydrate equals true to make it so that any additional requests
after that go to my local database. So I'm getting the best of both worlds because of client loader, which conveniently was released as I was writing this talk, so thank you Remix team for that. Speaking of, let's add offline support. I decided to take advantage of the
new Vite compiler in Remix and use the Vite PWA plugin. This lets me configure my PWA with some service worker caching strategies that I pulled from Google's Workbox package. It was a little bit finicky to set up, especially if you have dynamic paths in your app, because there's no way to know at build time what those dynamic paths are going to be unless
you're doing all kinds of crazy stuff. Again, this kind of thing is much better if your app is client-centric, and that means a single-page app. It makes it a lot easier. But fortunately, the most important routes that I wanted to cache, they aren't dynamic routes. They're just the meal plans and the groceries and the main page. And for any of the other ones,
I can add in special redirection to make it so that it just goes straight to the main page if I navigate directly to, say, a recipe. So that's great. Later navigations at that point will use client-side routing, client loader, and local data, so I don't have to
worry about issues caching everything. And then one other thing to keep in mind is those pesky network errors. In this case, the failed-to-fetch error. I just have to handle that inside of my client loaders to make sure that it doesn't crash my entire app. So the app worked, and
it worked well enough. I actually am still using it for my grocery planning. But there is a big catch. Remember that I've been talking for 15 minutes already, and we've touched on no-loading spinners, sharing data between devices, working offline, and collaboration
through real-time, but there are still those three ideals that we haven't touched yet. The long now, making it so that people can still use your app after you've stopped hosting it, security and privacy through end-to-end encryption, and ownership and control for the users. And these ones range from difficult to downright impossible to implement in a
web app. Let's start with the long now. PWAs, they're great. They make it so that you can do app-like things on your, for your website without having to build an actual mobile app. But the limitations are huge. There is a sync engine feature that is built into service
workers, but there isn't great cross-platform support for it. It doesn't work in Safari. And even if it did work in Safari, suppose the host website goes down forever, and you want to be able to install this PWA on another device. How do you do that? I don't think
that's possible. If it is possible, can someone talk to me afterwards? Because how do you get that cached code from one browser to another browser? I don't think it's possible. So much for the long now. And unless you've installed your app as a PWA, Safari and other browsers
could decide to just delete your data if you don't use it. If you don't use your app for a certain period of time. So PWAs are out. Like, they just don't cover all of the needs that the Ink and Switch team says you need for local-first software. This also applies
to end-to-end encryption keys, which are needed for the sixth ideal of security and privacy by default. If you can't count on your data sticking around, that also includes your encryption keys, which means your users lose access to all of their data as soon as the browser decides
to get rid of those keys. Unless you support something like paper keys or something. That's not very good either. And that's kind of the conclusion that the Ink and Switch team came to. They said, all in all, we speculate that web apps will never be able to provide all
the local-first properties we are looking for due to the fundamental thin client nature of the platform. Hey, there's that thin client again. By choosing to build a web app, you are choosing the path of data belonging to you and your company and not to your users. And this is something I experienced as I was building the app. I was incredibly frustrated
with the inability to do things locally on the device, like keeping a timer running while I closed the app. That's not something you can do with a service worker. It felt like I was pushing against the platform a little too hard, and maybe I should just rewrite
the app as a mobile app anyway. That doesn't mean that there isn't demand for local-first PWAs. I hardly ever install native apps these days, but being able to quickly add PWAs that work offline for just a short event, like Epic Web Dev Conf, which has a PWA that you
can install and it works great, that's a great value for a lot of people. So let's come back to this map. We can see where things are going. The ideals of local-first are absolutely worth fighting for, at least I think so. And just because the web doesn't have what it takes now doesn't mean that it won't in the future. Certainly, the web has been through
bigger transformations. And that said, there are plenty of reasons not to build local-first apps, which have nothing to do with the limitations of the platform. Peter Van Hardenburg, who is one of those people who wrote the paper that coined local-first, he put it well on
the local-first podcast when he said, when users have a lot of agency over their data, local-first makes a lot of sense. ATM spitting out money when they're offline is probably not a good idea. But those situations where local-first shines, Sam Willis, he's building
a sync engine called ElectricSQL, and he wrote up a nice list on Twitter that includes all different kinds of things, like architects on the site of a job, or a video production company that might lose access while they're filming on location. All different kinds of
reasons why you might want to have local-first software in your tool belt. And that's really what this is all about. It's about levers and tools that you can apply to your apps when and where you think are appropriate for providing the best user experience. A lot
of times, servers, server-first, it's great. It is exactly what your users need. But hopefully today I've demonstrated that it is possible to layer in these local-first techniques into an existing app, adding that little bit of capability while still maintaining all of
the things that make server-centric apps great. Thank you very much.