Wed. May 13th, 2026

Kill the loading spinner with local-first data and reactive SQL

The topic of Kill the loading spinner with local-first data and reactive SQL is currently the subject of lively debate — readers and analysts are keeping a close eye on developments.

This is taking place in a dynamic environment: companies’ decisions and competitors’ reactions can quickly change the picture.

It’s not every day that a radically new architecture comes along, but here we are: in-browser SQLite, combined with reactive SQL and auto-syncing. The promise is instant interactivity on the front end, while maintaining data symmetry with the back end. As a direct challenger to the RESTful group-think that has dominated web development for a decade, it is well worth a look. 

This idea isn’t brand new. Developers have been doing this kind of thing in one fashion or another for years (think of how some apps work offline). But this new-generation stack feels different, and it’s starting to see broader appeal.

It’s called local-first data. I recently covered the idea at a high level. Now it’s time for a look at the nitty gritty.

The concept is simple. Instead of asking a remote server for permission to change a number, your app writes state directly to a local SQLite database running in the browser (via WebAssembly). A sophisticated background engine then handles the hard work of syncing those changes to the cloud and other devices.

When you use Spotify, you don’t download their entire multi-million song catalog. You just download your list and, even if you are offline, you listen to your music lag-free. This is the same kind of model we are building with local-first data, but with a lot of extra power. We can make changes to our local state, and those changes will be synced with the back end when a connection is available. And correspondingly, we will automatically receive important updates from the world.

Let’s start by setting up the cloud services we need: Supabase and PowerSync, both of which will be free-tier. For our client-side UI and database, we’ll make use of a React+SQLite demo app provided by Supabase.

Supabase is a managed Postgres service with a lot of niceties. Our example is going to be very simple, with only one row required. Go to Supabase.com, create a free account, and start a new project. Clicking on the project will bring you to its details:

Next we create a database schema to hold our data, which will be a simple one. On the left side menu, open the SQL Editor and run the following code.

Each step is annotated within the code above. The first step is standard SQL, creating a table with a few columns including a random UUID primary key. The second step turns on row level security. The third step creates an access policy allowing users access to their own counter row (as determined by their logged in id: auth.uid() = owner_id). The fourth step creates a link between the Supabase instance and PowerSync.

Before we leave Supabase, click the “+ Connect” button at the top and grab the connection string, which looks something like this:

Now let’s jump into PowerSync. Go to PowerSync.com, create a free account, and start a new project. Open the project details:

In the PowerSync dashboard, we need to create a bridge to Supabase. Follow these steps:

PowerSync will now reach out to the Postgres instance we created before. If you ran the CREATE PUBLICATION SQL command correctly in the Supabase setup, this will succeed, and you’ll see a green “Connection Successful!” status message.

We told PowerSync how to read our Supabase data (Postgres). Now we need tell it how to trust our users. Here are the steps:

This configuration tells PowerSync, “Only trust tokens that are signed by Supabase AND are meant for the ‘authenticated’ user group.”

The sync rules are the essential architectural element to understand. This is where we tell Powersync what part of the data the user is privy to. This is sometimes called the “data shape” for the user. Each user has their own shape of the overall data, based on their unique profile.

We do not want to sync the entire database to the user’s laptop. That would be bad on many levels. Instead, we define a sync rule (or “bucket” in PowerSync), which is a filter that determines what data belongs to which user.

Navigate to “Sync Rules” on the left. You’ll get a YAML editor. Replace the default code with the following code.

You can click “Validate” to check that this is working. Click “Deploy” to make it live.

We have done a lot of administrative work here, but it is giving us an entire reactive architecture based on SQL. Let’s push ahead with a client that can use it.

To get a quick look, let’s clone a demo app from Supabase. At the command line, run:

Once the npm command finishes, we want to point the app at the infrastructure we just created. We can do that using the .env.local environmental variable file. Open .env.local and change the vars to point to your services. Here is the code:

The demo app is a simple counter—a web page that presents an “Increment” button and shows the count. None too exciting, except for the data syncing magic behind the scenes.

Run the above command, and a browser window will open showing a “Create Counter” button. When you click it, the demo will give you a counter in a new window, along with a user ID and a panel that shows connection and sync status. You can create as many counters as you like, each in a separate browser window.

You can verify your counter is working by going to the Supabase dashboard, looking at the “Table” pane and seeing that a row has indeed been inserted for your user in the counter table. Another good check is to log in using another browser/device (or incognito tab). 

Log in and create another counter in the second browser window, so that you have two different Supabase-synced session counters side by side:

Whenever you create a new counter, a new row will be inserted into Supabase. The screenshot below shows a Supabase table with two rows, each with its own user ID and counter value.

We are moving quickly, but there are some interesting things to make note of in the src/App.tsx file. This code is the main React code for the app, and demonstrates the shift from “asking the server” to “interacting with local database state.”

There are two landmarks here that every React developer needs to see: the reactive read and the instant write.

In a standard React app, we would retrieve the data by using a useEffect to call fetch(‘/api/counters’). In our local-first React app, we use raw SQL:

Notice what happens when we increment the counter. There is no await api.post(…) and no loading spinner state.

This code is interesting on its own, because it’s like a reactive SQL statement. We write to the local file (Wasm). The write completes in milliseconds, and the UI updates immediately via the useQuery hook. The PowerSync syncing engine picks up the change asynchronously and pushes it to Supabase.

If you are building a simple dashboard or a form-based application, the traditional JSON API (REST or GraphQL) approach is still king. In those models, the server is the single source of truth, and the client is just a dumb terminal. It is simple, stateless, and easy to debug. It’s also familiar.

But that simplicity comes with an unavoidable latency cost. Every interaction requires a round-trip ticket to the server. If the network hiccups, your app freezes. JSON APIs force you to manage loading states, error boundaries, and optimistic UI rollbacks manually.

Local-first flips the calculation. You pay a higher cost up front: you have to define a schema, manage a local database, and think about syncing rules. But in exchange, you get an application that feels like a native piece of software. Local-first creates data continuity, the ability to walk out of Wi-Fi range, keep working, and have your data follow you across devices when you reconnect.

Architecturally, we used three major components: the database, the syncing engine, and the client. This is actually similar to your conventional RESTful stack. In the local-first structure, the syncing engine takes the place of the JSON API server. In short, you have a similar amount of high-level complexity, but with different actors on the ground.

Local-first architecture is a fascinating development for JavaScript and the web in general. There is a huge inertial mass of JSON APIs to overcome, but here is a real countercurrent. Local-first may never rise to the level of adoption of RESTful architecture, but local-first data and reactive SQL constitute one of the most important trends to be watching closely right now.

Why it matters

News like this often changes audience expectations and competitors’ plans.

When one player makes a move, others usually react — it is worth reading the event in context.

What to look out for next

The full picture will become clear in time, but the headline already shows the dynamics of the industry.

Further statements and user reactions will add to the story.

Related Post