Skip to content

Build1 publisher3 min readPublished

Removing the server erases sync conflicts but creates a new offline update problem

A pictogram app for a child who does not speak has no backend and no conflicts to merge, leaving a precache list that decides what works in airplane mode and a service worker that has to land a new build on a tablet in someone else's hands.

The Engineer · Build desk

Illustration accompanying Removing the server erases sync conflicts but creates a new offline update problem

What happened

  • Mes mots is a pictogram communication app built for a child who does not talk yet, and it has to work in airplane mode on a tablet that might not see wifi for days.
  • Leaving mp3 out of the Workbox precache pattern left the app installing and rendering tiles normally offline while the speaker stayed silent, with no console exception and no error screen.
  • An earlier release's cache had already served a bundle that was gone from disk and quietly hid a shipped feature, so the config now sets cleanupOutdatedCaches to true.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint The glob pattern is the only place the app's offline capability is written down, so any file type missing from it becomes a feature that disappears in the field without raising an error.
  • exposure The first person to find a bad build is a parent in airplane mode, and the only channel back to that tablet is the screen the app is already drawing.
  • decision Choosing autoUpdate forces a call on the session already running: prompt for a reload when control changes hands, or accept that a tablet left open keeps executing yesterday's bundle.
  • capability With no backend, the whole update surface fits in a service worker config, and no engineering time goes to token refresh or partial sync state in the interface.

The precache list is the spec for what the app can do without a network, and nothing validates it. Automated checks do not listen to the speaker. vite-plugin-pwa generates a Workbox service worker from declarative config, and Workbox copies every file matching `globPatterns` into the cache at install time [8][9]. Anything the pattern misses falls back to a runtime fetch. In airplane mode that fetch does not happen, so the app installs and opens with tiles rendering normally, nothing comes out of the speaker, and there is no exception in the console [12].

The pattern that works lists seven extensions [9]. Six of them, js, css, html, png, svg and woff2, are what a typical frontend build emits [10]. The seventh is mp3, and every tile in this app speaks its word by playing one [11]. One entry out of seven separates a working app from a mute one [1].

"There's nothing to sync, because there's no one on the other end," the app's developer wrote on dev.to [7]. Conflict resolution, vector clocks and three-way merges are the standing cost of the offline-first apps that do have a server behind them, and none of them apply here [3][4]. Neither does refreshing an auth token, nor displaying partial sync state [6].

What replaces them is delivery. `registerType: 'autoUpdate'` means the service worker downloads a new build as soon as it sees a network, installs it in the background, and takes control with no "an update is available" banner to click [19]. The page already open keeps running the old JavaScript until a full reload [20]. The browser fires `controllerchange` when the new worker takes over [21], and that event is the app's only notice that its own code has been replaced underneath the running page.

Scope is the other thing the config decides. The tablet gets the app at the root of a domain, anime-sanctuary.net, and the public demo lives under `/mes-mots/` on GitHub Pages [15]. A service worker and its precached assets are scoped to a base URL, so a build made for the root looks one level too high when it is served from a subfolder [16]. The result is a blank page with no readable error in devtools [18]. One environment variable, `BASE_PUBLIQUE`, read once at build time, decides which of the two it is [17]. The developer's comment records the reason it has to be right first time: the tablet belongs to someone with no terminal [18].

This holds only while the content ships with the code. The only thing that ever arrives from the network here is a new version of the app itself [5]. Let the tablet author a pictogram that a second device has to see, and the merges come back, along with the server that makes them necessary [3][4].

What to watch

  • Whether the handler on controllerchange prompts a reload or leaves a long-lived tab running the old JavaScript.
  • Whether the app ever lets the tablet author content that a second device must see, which would put merges and a server back in scope.
  • Whether cleanupOutdatedCaches alone prevents a repeat of the stale-bundle incident on the family tablet.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories