Engineering
November 22, 2023
November 22, 2023
(updated)

Local-First Software is a Big Deal, Especially for the Web

Conrad Hofmeyr

Developers are increasingly getting excited about local-first as the app architecture of the future. [1] [2] 

In the local-first paradigm, app code works directly with a client-side embedded database, which optionally automatically syncs with a backend database in the background. This is in contrast to cloud-first apps which primarily use a cloud datastore via APIs.

Working with a local database (e.g. SQLite) means that apps feel instant to use because of low latency, and are functional even if the user’s network connection is unreliable or offline. In addition, local-first typically offers built-in multi-user real-time collaboration by automatically syncing data in the background bi-directionally.

The application architecture of the future

While some apps will always necessarily have to rely only on synchronous network communication with a server (e.g. banking/payments, video streaming, etc.), it is likely that local-first will become the default architecture for the majority of apps in the future, because of its substantial benefits for developers (simplifying distributed state management) and end-users (a delightful user experience).

Local-first has gradually been gaining popularity for mobile apps, but with SQLite in the browser using Wasm becoming more robust (combined with using service workers to pre-cache assets/resources so that web apps can load offline), it’s something that will increasingly revolutionize web apps too. Once local-first architecture becomes mainstream on the web, the impact of this architectural shift will be much bigger. 

From differentiator to table stakes

Apps like Figma, Linear and Superhuman are widely admired for their stellar user experience powered by a local-first or similar architecture under the hood. Importantly, they’ve used it as a differentiator to disrupt their respective markets and wrest customers from incumbent competitors.

As local-first gains more momentum, the features/benefits that it provides to users (instant responsiveness, network resilience, offline use and real-time collaboration) could easily become “table stakes” rather than differentiators.

Here be dragons?

Many developers may instinctively shy away from local-first (and previously offline-first [1]) and stick to cloud-first apps, because it’s pretty complex and painful to build a robust active-active sync engine (challenges are numerous and include dynamic partial replication, conflict resolution, consistency guarantees, and more. Add in the need to support web apps and the complexity escalates further). 

That is not to say that cloud-first apps are without complexity traps either: In an API-powered app front-end, caching and application state management can quickly get hairy too.

Fortunately, drop-in sync engines like PowerSync are now starting to make local-first architecture even simpler to build than cloud-first apps. We’re excited to be part of this movement and our aim is to help accelerate the adoption of this architecture, especially since announcing support for web.

Deeper dive: Why local-first matters

Lower backend compute load, dependency and cost

  • In a typical cloud-first app, clients are constantly hitting the backend with API requests to read or write data. The backend becomes a performance bottleneck. 
  • Local-first apps move the vast majority of read operations to the client-side, using the local embedded database. Because all the relevant data is right there in the local database, many computations can also be done locally. This reduces backend complexity, load and scale-out needs — thereby reducing cloud computing costs. [3] In addition, because these apps are not reliant on the network and backend at all times, backend uptime and performance is much less of a factor in app performance.

Simplified backend development: Reduced API burden

  • In a typical cloud-first app, every added feature results in a need for new or expanded backend APIs. Developers often have to invest significant effort in implementing and maintaining API functions.
  • An offline-first architecture drastically reduces the API development burden: By replicating the backend database to the client-side proactively, we already have all the data we need right there in the client without having to consult a backend API. Quoting Tuomas Artman of Linear: “We don’t have to make REST calls, we don’t have to make GraphQL calls. We modify the data, we save it, and everything always updates.” Dynamic partial replication is a key piece of functionality here, since we only want to replicate the part of the backend database that is relevant to the specific user. 

Simplified state management, simplified app codebase

  • Many developers have learned the hard way that distributed state management can be very complex and painful. As mentioned above, this is no less true for cloud-first apps, where developers typically respond to app performance issues by implementing some variation of caching and optimistic updates. This can very quickly lead to spiralling complexity around cache invalidation and inconsistent states.
  • In local-first apps, the global state is simply stored in the local database. This substantially simplifies application code since queries can be done synchronously and the developer does not have to handle loading states or network failure states. On the web, the benefits go even further: State can be shared across multiple browser tabs on the same user’s device, all accessing the same local database. By utilizing an active-active replication sync engine working in the background, the developer also doesn’t have to think about state transfer.

At Meta, once the Project Lightspeed team migrated their Messenger app to a SQLite-centric architecture, the benefits were clear: “We leveraged the SQLite database as a universal system to support all the features. [...] The UI merely reflects the tables in the database.” 

Ink & Switch says about the benefits of local-first: “[It] allows app developers to focus on their application rather than the challenges of data distribution”. This is echoed by Tuomas Artman of Linear: “Once we’ve put in the effort on putting the synchronization engine in, our task of developing this application became a lot easier and a lot faster.”

To the user, everything feels instant. No loading spinners.

  • In a cloud-first app that relies on APIs (REST, GraphQL), most data operations require a round-trip to a server. This always results in some latency — typically requiring the user to stare at a loading spinner.
  • By contrast, a local-first app that uses a local embedded database in the application front-end (with a sync engine working in the background) dramatically cuts down on the latency of reading and writing data, which means that the app is much faster for users — the UI can react near-instantaneously to user input. [4]

Offline use / network resilience / high availability

  • In a cloud-first app, the user is at the mercy of the network — even a momentary loss of mobile network coverage can result in the app failing to load or save data.
  • In a local-first app, the network is no longer on the critical path for user interactions —  meaning that the app is resilient to any interruptions in network service quality, or even complete downtime of the network connection or backend. Users can use the app while offline for brief periods of time (e.g. passing through a tunnel) or for extended periods of time (in the subway, in the mountains, in the basement of a building, etc). [5] With conflict resolution and consistency guarantees, the intricacies of combining offline usage with multi-user collaboration can also be managed.

Real-time collaboration and reactivity

  • With a typical cloud-first app architecture, the developer has to invest significant effort into building real-time streaming functionality in order to enable collaboration between different users. This may require building or otherwise bringing in a whole new system.
  • On the other hand, sync engines such as PowerSync used for local-first apps come built-in with key components that naturally enable real-time multi-user collaboration, since they are fundamentally designed to monitor for data updates and keep users in sync. [6]

Designing for local-first

A local-first architecture will result in much of your app functionality living on the client-side rather than on the backend. [7] Much of your business logic, querying, filtering and computation will happen client-side. 

That being said, for security reasons you still want to enforce certain business logic, authorization and validation on the backend. In this sense, PowerSync’s architecture is beneficial because it sends writes through your backend, putting you as the developer in control of the write process. This protects your backend database as the source of truth / system of record. And because PowerSync uses server reconciliation for consistency, your backend has the ability to reject writes based on your business logic / authorization / validation, and this will be kept consistent across clients.

What about Firebase offline support?

A common misconception is that Google Firebase’s Cloud Firestore provides a local-first or offline-first architecture. It’s more accurate to describe Cloud Firestore as a cloud-first system: By default, queries run against the cloud (online) datastore and the results of those specific queries are cached on the local device (and may be removed from the cache later). Cloud Firestore does not preemptively sync a database to the local user device for offline-by-default access. 

In addition, by default Cloud Firestore will try to reach the server first before falling back to the local cache, which can result in frustration and significant latency for the user if their network connection is patchy.

Ushering in the local-first future 

Hopefully it’s clear from the above that local-first architecture will increasingly be a game-changer for both developers and end-users. And by making this architecture easier to implement for web apps, the total potential impact on the software world is expanded enormously.

Questions? Comments? Join us on Discord

If you have any thoughts to share or questions, please join us on our Discord server.

Footnotes

[1] We see “local-first” as the logical successor to “offline-first” app architecture. Local-first builds on the idea of offline-first with additional ideals. The industrial research lab Ink & Switch defined local-first in 2019 as a software application architecture where the cloud/server is merely a peer and not on the critical path to supporting client applications. Their vision is that local-first applications should have “ultimate ownership and control” over their own data. Not all software that is described as local-first today adhere to all of the ideals espoused by Ink & Switch, but many developers aspire to get closer and closer to those ideals over time, instead of merely providing equivalent capabilities to what would be considered “offline-first”.

[2] Often when this topic comes up, there are folks who comment “what was old is new again” or “we’ve come full circle”. Indeed, there was a time when most software applications were thick clients running locally, utilizing a local datastore and allowing you to work offline. The rise of the web has meant that we’ve sacrificed that instant reactivity and offline use for the benefits of the cloud (collaboration, accessible from anywhere in the world, always up-to-date). The good news is that we now can have the best of both worlds: The benefits of the web/cloud while working local-first, even in web apps.

[3] In the case of PowerSync, the backend compute is further reduced by the PowerSync service, which handles read operations related to syncing. The PowerSync service pre-processes and indexes data to be synced for different users — enabling high-performance dynamic partial replication.

[4] Typically, the only instance where a local-first app may be slower than its online counterpart is on first start-up, since the app may need to perform an initial sync to download data into the local database. As the bandwidth of internet connections improves over time, this will become less and less of a factor. Once the initial sync is done, data updates can be synced incrementally in the background and subsequent startup time is typically instantaneous. This is a net benefit to users since they will typically save far more time while using the app than they spent waiting for the initial sync to complete.

[5] This is valuable given how users’ goalposts have shifted in recent years: Now that a significant share of the population have smartphones filled with useful apps, they expect to do a wide and rich range of things on their smartphones, no matter where they are, 24/7. They don’t want to be left hanging due to a lapse in network connectivity. As Ink & Switch put it, “while other applications […] threw up errors (‘offline! warning!’) and blocked the user from working, local-first [applications] function normally regardless of network status. [...] There is never any anxiety about whether the application will work or the data will be there when the user needs it.”

[6] In the case of PowerSync, the sync engine hooks into a change log on the backend database, and streams data to keep the local client-side database in sync in real-time. In addition, data flows from the client to server in real-time whenever connectivity is available: Writes to the local client-side database are also placed in an upload queue which are immediately written to the backend database (unless connectivity is unavailable or interrupted, in which case it is retried later). Lastly, the client-side allows for “live queries'' which can immediately update the UI whenever data in the local database changes (whether from syncing remote changes, or from any local changes anywhere in your code). 

[7] It is easier to implement a local-first architecture using a sync engine during the upfront development phase, than retrofitting it into a “brownfield” application. Putting logic on the client-side and working with the syncing data flow might not easily fit into your original architecture. It can certainly still be done, but requires a bit more work.