The local-first set is complete

Three libraries, one registry, and every piece consumed by a shipped sample before its tag settled.

For app programmers August 2026 · from the RingScript project

When the library ecosystem opened, we sketched the set a local-first developer actually needs: the machinery of working offline, the machinery of working with rows, and the machinery of working with money. All three are now published, and the claim this post exists to make is not “they exist” — it is that every one of them is consumed by a shipped sample application, which is the only review a library API respects.

librarywhat it decidesconsumed by
pwa 2.0.0 what a queued unit of work is, when it counts as sent, what a degraded mode is called Route Orders, Stock Count
table 1.2.0 filtering, sorting, paging, grouping rows on the device — with the index that keeps a sorted read from going quadratic the Ledger
money 1.0.0 what an amount is, where a rounding decision happens, how a total splits without losing a centime Route Orders

The outbox grew up: partition-tolerant by default

The big change since the ecosystem post is pwa 2.0, and it was not designed at a whiteboard. It was designed against field evidence from a real restaurant: on 15 August a faulty refrigerator kept tripping a breaker in a restaurant in France, every router reboot changed the server’s address, and because browsers bind storage to the origin, two hours of the owner’s notes became unreachable. The design that came out of that incident is written up in PARTITION-FOUNDATIONS.md, and 2.0 is that document executed:

Replay is ordered and idempotent. Entries transmit strictly in queue order, one request in flight, and a failure stops the replay — a later entry must never overtake an earlier one’s retry. The entry id is minted on the device before the first send, so a server that sees it twice returns the original verdict instead of creating a second order.

A full store refuses loudly. queue() persists the entry before answering ok; a store that cannot hold it says storage-full by name instead of accepting work it cannot keep. 1.x could lose that quietly. 2.0 cannot.

The degraded mode has a name, and Ring can read it. The library maintains a rung — alone, streaming, unreachable — and pushes it into the VM, because a rule like “card payment needs the server; pay cash, or wait” is a business rule, and business rules live in Ring, not in UI glue. The connection-lost banner fires only after eight seconds of true silence — the exact number a restaurant owner’s complaint tuned.

And it is tested the way outages actually happen: a harness drives a real world, on the real VM, through a real TCP proxy whose sever() destroys live sockets mid-stream. Twenty checks in 0.7 seconds, including the one the first run failed — which found a genuine design gap about what a reconnecting device may forget. The fix is a rule now, not a patch.

Money, because doubles are not an accounting policy

The newest library is the smallest and the most opinionated. An amount is integer minor units plus a currency{"m":1250,"c":"EUR"} — and four rules carry the rest: a typed price is parsed exactly while a computed value is rounded on purpose; there is exactly one rounding function in the whole library; currencies never mix silently; and an allocation always sums to the whole — 100 F split three ways is 34 + 33 + 33, and no centime has ever gone missing between three people who can count.

Route Orders now prices with it, and the one visible change is the honest one. The tax on a 137 750 F order used to be 26 172.4999… in a double, passing its test on tolerance; it is now 26 173 F, by a half-up rule with a name, applied where you can see it. It knows XOF has no subdivision and Tunisian dinars carry three decimals, and it will refuse "12.50" as an XOF amount rather than round somebody’s typed price.

mLine  = MoneyMul(mPrice, 10 * 0.95)     # a case of ten, 5% off: rounds ONCE
mTax   = MoneyPercent(mLine, 19)         # integer arithmetic, half-up at the end
mTotal = MoneyAdd(mLine, mTax)
aParts = MoneySplit(mTotal, 3)           # the parts sum EXACTLY

The habit that built all three

Publish, then immediately consume, then let the tag rest. Every release in this set moved a real sample onto itself before anything downstream could pin it, and the habit has paid three times: the second application found the duplicate-send bug that became pwa 1.0.1; moving Route Orders onto 2.0 caught a batch API the refactor had dropped, while the tag was still cheap to re-cut; and the partition harness’s first run corrected the reconcile contract itself. A library with no consumer moving against it is a library whose regressions are all still in the future.

The other half of the habit is that the installer keeps the promises. Every download is verified against the registry’s sha256 before a byte is unpacked; update fetches and verifies the new version before deleting the old, so a failed download leaves the project exactly as it was; remove unwires every page an install ever touched and leaves index.html byte-identical. The second project on a laptop installs from the package cache with no network at all — which, for tooling that exists to build offline-capable applications, is not a nice-to-have.

Try the set

From any folder with an index.html, using the binary already in the starter kit:

ringscript add pwa
ringscript add table
ringscript add money

Route Orders The Ledger Stock Count

The format and the tooling are documented in LIBRARIES.md, the hands-on tutorial in using-libraries.md, and the partition design — the storage survey, the outbox contract, the rung, the test story — in PARTITION-FOUNDATIONS.md. The registry is still one JSON file, and a pull request is still the whole of the review.