All posts
Release Notes7 min read

Filarr Mobile, Week in Progress: Making Sync Honest

This week on the Filarr mobile app: five sync fixes, two new permanent error states, a home screen that finally shows your real storage usage, and 3,326 tests green.

MB

Mathis Belouar-Pruvot

This week on the mobile app: sync tells the truth. Five fixes, two new permanent error states, a home screen that finally shows your storage usage, and 3,326 tests green. The Filarr mobile app ships in roughly one to two months.

The Filarr mobile app (React Native) is in the final stretch before release. Before it ships, the sync layer has to be honest, not just functional, but honest: when something is wrong, the app should say what it knows, only what it knows, and stop pretending it will fix things it cannot fix.

That was the theme of this week.

Stopping the eternal "sync interrupted" banner

The most visible bug on mobile was a banner that would not go away: "sync interrupted." Digging into device logs, I found three separate causes, each feeding into the others in a nasty loop.

Phantom manifest entries. A file would exist in the sync manifest with all the right metadata, but its R2 object was gone. 404, confirmed. The sync cycle was treating that 404 as a transient network error ("fetch-failed"), which meant: I'll retry. But a 404 from the server is not a temporary blip. It is a fact. The file is not there.

The fix introduces a new permanent error state: missing-remote-object. It fires only when the absence is proven (the server responded 404, not silence or a timeout). The banner clears. The file gets an honest line in the vault. No purge: removing the entry would be an assertion read by other devices, and I have nothing to assert, only to observe. I also checked the desktop code and confirmed that a remote-absent entry goes to upload rather than delete, so no local copy elsewhere is at risk.

Stale manifest checksums. Another cause of the eternal banner: a checksum mismatch. The manifest had a checksum that no longer matched the object in R2, a leftover from a migration where entries were written with the encrypted checksum, then the object was rewritten on the same deterministic key. The integrity check was doing its job, but the mismatch was permanent, not transient.

The fix degrades toward cryptographic proof instead of rejecting. The bytes are already downloaded (no second round-trip needed). They go through AES-GCM tag verification under the candidate keys. If they pass, the entry is repaired: both checksum and size updated, in a compare-and-swap so a concurrent republication keeps the last word. If they fail, the old behavior is preserved exactly, and nothing is concluded from unauthenticated bytes. The manifest checksum is a cheap pre-check we wrote ourselves. The GCM tag is a cryptographic proof of both origin and integrity. When they disagree, trust the proof. For a deeper look at how Filarr's encryption layers stack in practice, that post walks through the full defensive architecture.

A deadlock between phantom entries and sync authority. The third cause was a true deadlock. Purging phantom entries required sync authority. Gaining sync authority required a cycle with no corruption. The only "corruption" left was the phantom entries themselves. The banner would show all three faces: corruption, transfer interrupted, unplaceable entry. None would clear.

The fix has three parts. A new permanent error type, unplaceable-entry, covers remote-only entries with no local path. This is a schema defect, not an integrity violation. The red corruption banner does not light up. Authority becomes reachable again. Migration now publishes localPath on every blob entry, so no new unplaceable entries will be born. And redundant entries can be purged under authority when a proven duplicate exists in the same manifest, with a specific guard: two unplaceable twins never purge each other.

Reader capability versioning: old verdicts do not outlive the reader

Separate from the banner loop: fourteen files on a desktop profile were staying marked "unreadable" on mobile, even after an update that added the exact reading capability they needed. No fresh attempt, no retry. The cycle was replaying a persisted verdict from an older version of the app.

Caching unavailability verdicts is sound in principle: do not re-download a file that will fail for the same reason. But "this version of the mobile app cannot read this file" is a judgment rendered by a specific reader. It must carry its version.

The persisted verdict is now wrapped with READER_CAPABILITY_VERSION. A verdict from a prior version, or a bare pre-versioning verdict, is treated as stale and retried once. The rule for incrementing: any time reading capability expands (a new container family, a new transport layout, a new key candidate). Not for bug fixes that change nothing about what can be read. Each increment costs one download per previously-unavailable entry, so it is not free, but it is honest.

Home screen: your storage quota, now actually fetched

The storage usage block on the home screen always showed "usage unavailable" on cloud accounts. Every single time.

The API was there. The worker returns storageUsed and storageLimit on GET /billing/status. The client had a typed getBillingStatus. The display logic, resolveStorageBlock, knew how to compute the gauge and the alert threshold, with tests. The call was just missing. HomeScreen was calling resolveStorageBlock({ accountMode, tier }) without ever passing the bytes. The label was saying the truth, unintentionally, indefinitely.

The quota is now fetched on mount, on account mode change, and at the end of each complete sync cycle. It moves when you upload, so a number frozen at mount would grow stale under your eyes. Best-effort and never blocking: offline or server silent, the block falls back honestly to "unavailable." A three-day-old figure presented as current would be worse than no figure at all. No call on local accounts: the block is not rendered there.

A freshness window of two minutes sits on top of that. Measured on device before the fix: seven calls to /billing/status in twenty seconds, because the home screen remounts on every tab return. The gesture that moves the quota (pull to refresh) now forces an immediate re-read. Two minutes between automatic reads: short enough that a return to home after an import shows the right number, long enough that ordinary navigation costs nothing. The timestamp is set only on success, so a passing outage does not condemn the block to "unavailable" for the entire window.

For context on how the optional cloud sync stays zero-knowledge while all this sync logic runs, that post covers what actually travels to the server: encrypted blobs, opaque to us.

3,326 tests, all green

Every fix this week shipped with tests. The sync error suite covers missing-remote-object detection across both axios response morphologies (wrapped and raw), reader capability versioning and legacy cache adoption, stale checksum degradation with GCM fallback, the deadlock fix including the twin-unplaceable guard, and folder cycle behavior on 404. Internationalization parity sits at 1,477/1,477 strings.

One addition worth noting: a build marker now goes into the diagnostic log on startup. It sounds trivial. It is not. A lost diagnostic session once cost real debugging time because I could not confirm the device was running the code I thought it was. The marker closes that gap.

What is coming next

The sync layer is now at a point where I would call it reliable. Next on mobile:

  • UI polish pass: the vault list, the editor, and the onboarding flow still have rough edges from rapid iteration
  • Onboarding screens: the key derivation and recovery phrase screens need a final design pass before any first-time user sees them (the per-file encryption model they set up is the same one running on desktop today)
  • End-to-end testing on a real device across the full sync lifecycle: upload from desktop, read on mobile, edit on mobile, conflict resolution back on desktop
  • A small beta build for early testers

If you want to follow the build or get early access, the milestone post at 1,000 users has the full picture of where we are and what comes after mobile ships. The desktop client is already open source under BSL 1.1, and the mobile app inherits the same encryption architecture.

Roughly one to two months out. Building in public the whole way.

#changelog#mobile#react-native#sync#build-in-public#filarr

Related articles