Local-first apps
A business application whose rules live on the server is an application that stops working when the connection does. Not degrades — stops. This page is about building the other way round, and the two working applications that show what it costs and what it buys.
Where do the rules of your application live?
Not the screens — the rules. Whether this customer may take more on credit. What price this customer pays. Whether this discount applies. Whether this form is complete.
In most applications those rules sit on a server and the client asks permission for everything. A sales representative standing in a shop with no signal cannot find out whether the shop may order 40,000 more on credit, so either they promise and hope, or they leave without the order. A nurse, a teacher, a stock clerk, an inspector — all the same.
The network is not a component of your application. It is a guest, and a guest that is sometimes late and sometimes does not come.
This is obvious to anyone who has shipped software in West Africa. It is becoming obvious everywhere else for a different reason: customers increasingly want to know their data is theirs, held on their device or their premises. Bad connections and data ownership are different arguments that point at exactly the same architecture.
Three working applications
Both run on this site, on the real Ring virtual machine compiled to WebAssembly. Neither is a mock-up.
The pattern, complete
Route Orders
A field-sales order pad. Price tiers, the full-case discount, tax, the stock check and the credit limit all decided on the device; the server used exactly twice — pull the reference data, push the finished orders. There is a switch marked “Cut the connection”. Press it and keep working.
It is laid out as the web project it is: index.html,
app.css, app.js, orders.ring.
Six files, no build step.
The load question
A savings-circle register
Once the rules are on the device, the next question is whether the device can carry the data. This one holds 20,000 records inside the VM and is filtered, sorted and totalled live — every interaction timed against how people actually perceive delay, every answer checked against an independent computation.
Building it found four things worth fixing, two of them bugs in the Ring VM itself.
Getting it onto the phone
Stock Count
Rules on the device are only half of it — if the app is a web page, the device does not really have it. This one installs: a service worker caches the Ring runtime itself, so it opens with no network, and Background Sync pushes the finished count even when the app is closed.
Stop the server, reload the page, and it still opens. Under half a megabyte in total.
What it measures out at
A session with Route Orders — loading the route, searching, taking two orders with one of them entirely offline, then syncing:
Where each piece goes
The division is the whole idea, and it is smaller than people expect.
- HTML and CSS
- the entire interface — ordinary markup and an ordinary stylesheet, the ones you already write. Ring draws nothing.
- JavaScript
- the wires:
fetch,localStorage, the DOM, the event handlers - Ring
- the business logic and the data processing — the part that decides, and therefore the part that must not need a network
- Your server
- two HTTP endpoints that speak JSON. Django, Laravel, Spring, Rails, Node, Go, .NET or Ring — it stays exactly where it is
Ring never touches the DOM and has never heard of
localStorage. It answers questions; the page decides how the
answers look:
const view = ask("OrderView"); // what does this order come to?
renderOrder(view); // ...and the page decides how that looks
Rewrite the front end in React, Vue or Svelte tomorrow and the Ring file does not change. Neither does your back end.
The three ideas worth stealing
An outbox, with ids the device generates. Finished work goes into a queue before it goes anywhere near the network. The id existed before the first attempt, so a retry cannot double-book — idempotency stops being a distributed-systems problem and becomes one line of code.
A send that never arrived is not a send. When the request fails,
everything marked sent goes back to pending and
will be tried again. Nothing is ever in a state where the device believes it
is done and the server never heard.
One verdict per order, not per batch. The server answers each item separately, so one rejection cannot lose the other nine — and the device reconciles each answer, including handing back credit an order had provisionally taken.
What this pattern does not solve
It is not a synchronisation framework, and you should be suspicious of anything claiming to be one in four hundred lines. Real conflict resolution — two people editing the same record, merges, vector clocks — is a larger subject. The honest position is that this handles the append-only case completely and says nothing about the rest.
Fortunately an enormous share of business software is append-only: orders, readings, attendance, deliveries, payments, visits, inspections. If your application is one of those, the two applications above are the whole answer.