ReadAware

Lake Baikal: sync that had to be earned

Six weeks ago, in the post about why your library lives on your device, we made a quiet promise: "Sync, eventually." Version 0.5.0 — Lake Baikal — is that eventually. It took eighteen days without a stable release, which is the longest this project has ever gone quiet, and the gap deserves an explanation.

Why sync is the hard promise

For a normal app, sync is a feature. For a local-first app, it is a test of everything you claimed to believe.

We had committed to two things that make sync brutally unforgiving. First, your data is end-to-end encrypted: the relay server stores ciphertext sealed with a key derived from your passphrase, and nothing else. That sounds like a bullet point until you realize what it costs — the server can never merge your data, never repair it, never even look at it to help you debug. Every hard problem must be solved on your devices, by construction.

Second, ReadAware's storage is an append-only event log; the tables the app reads are projections rebuilt from it. So the unit of sync is not "your books table" — it is every event that ever happened, in an order that must converge no matter which device wrote what while offline. Getting a second device to replay your whole reading life and arrive at the same library is less like copying files and more like proving a theorem about your own past.

So Lake Baikal took the slow road: hybrid logical clocks for ordering, batched replay so a big backlog rebuilds projections once instead of once per page, chunked uploads so a 200 MB scanned book travels in resumable pieces, sign-in that opens the app from a browser link, and a live push channel so a highlight made on your desktop is on your phone before you pick it up. Even your preferences roam — themes, typography, AI settings, API keys — so a fresh install feels like yours in a minute.

The ghost books

The first real two-device run produced our favorite bug of the release: books that existed on the new device as covers with no pages. Ghosts.

It turned out to be three separate failures stacked so neatly that each one hid the next. The bootstrap copied manifest rows for files whose bytes had never finished uploading. The webview's security policy silently blocked the app from reaching the relay on one platform. And when the relay refused a file for being over the free tier's size cap, the refusal was recorded — silently — in a column nothing displayed. Three quiet failures, one spooky symptom, days of forensics.

The fix was partly code and partly philosophy: sync now has a per-book panel that shows exactly what has and hasn't reached the cloud, and why. A system allowed to fail silently will; the only cure is making its state visible.

The audit the ghosts demanded

That "silently recorded refusal" bothered us enough that, once sync stabilized, we audited error handling across the entire app. The findings were humbling: fourteen places showed users raw internal errors, eight places swallowed failures a user deserved to know about, and one migration path could quietly delete data after a failed import.

Lake Baikal ships the rebuild: every failure now carries a stable code, surfaces as a localized, actionable message — never a stack trace — distinguishes "empty" from "broken", and offers retry only when retrying can honestly help. And if the app ever crashes, the next launch offers (asks — never auto-sends) a diagnostics report.

The wall that was one array long

A user reported that Android couldn't import TXT files. We were confused: TXT support shipped months ago. Both things were true. The format support lived in the app; the wall lived in a hardcoded list of MIME types handed to Android's system file picker, written before TXT support existed and never updated. Every .txt file on every Android device was being greyed out by six lines of configuration.

It is the most instructive bug of the release: a feature is not what the code can do, it is what a user can reach. We now test the reach.

What else is in the lake

The book memory got dramatically faster — the character and concept graph that used to build over hours of idle time now races you, building in the first minutes after you open a book, and the assistant queries it like a tool instead of having it stapled to every prompt. There are two update channels now (Beta, if you like the fast lane), a real OpenRouter model picker with prices, plugins that can theme the whole app and update themselves with rollback, and page turns on the mouse wheel.

Sync ships with a free tier, and paid plans raise the storage when your library outgrows it — that, plainly, is what funds the relay.

Lake Baikal holds a fifth of the world's fresh water, unfrozen under a meter of ice, and has quietly kept everything ever given to it. That felt like the right name for the release where your library learned to survive anything happening to any one device. The lake is deep. Toss your books in.

← All posts