Skip to content

Latest commit

 

History

History
240 lines (159 loc) · 11.1 KB

File metadata and controls

240 lines (159 loc) · 11.1 KB

Clear-Site-Data: Client Reset Kill Switch

Quick Reference

What it does: Forces all clients below a version threshold to wipe IndexedDB, SW caches, and web storage, then reload with fresh assets.

When to use it: Bad service worker deploy, broken DB schema, corrupted cache — any situation where clients can't self-recover.

How to trigger a reset:

Edit public/clear-manifest.json:

{
  "version": "0.0.2",
  "clear_below": "0.0.2"
}

This forces every client running a version below 0.0.2 to clear everything and reload. Set clear_below to null to disable.

How to revert: Set clear_below back to null and deploy. Clients that already cleared are unaffected (they're on the new version). Clients that haven't loaded yet won't clear.

What gets cleared:

Target What's wiped
IndexedDB All databases (message cache, search index, drafts)
SW caches All Workbox precache + runtime caches
Service worker Unregistered (fresh SW installs on reload)
localStorage Cleared (settings, preferences)
sessionStorage Cleared (tab-scoped state)
Cookies Not touched (auth sessions preserved)

CI safety net: On every PR, CI checks if changed files match patterns in .clear-site-data.json. If they do, it posts a warning comment reminding you to consider updating the manifest.

Files involved:

File Purpose
public/clear-manifest.json The manifest — edit this to trigger/disable clears
src/main.tscheckClearManifest() Boot check — runs before any DB/store init
.clear-site-data.json Maps file patterns to clear targets (used by CI)
scripts/check-clear-manifest.js CI script that warns on PRs
workbox.config.cjs Excludes manifest from SW precache

How It Works

Boot Check

checkClearManifest() in src/main.ts runs at the top of bootstrap(), before any database or store initialization:

  1. Fetches /clear-manifest.json with cache: 'no-store' (bypasses HTTP cache)
  2. Reads clear_below from the manifest
  3. Compares the client's VITE_PKG_VERSION (semver, e.g. 0.0.1) against the threshold
  4. If the client is below the threshold:
    • Deletes all IndexedDB databases (with Safari fallback for indexedDB.databases())
    • Deletes all SW caches via caches.keys() + caches.delete()
    • Unregisters the service worker
    • Clears localStorage and sessionStorage
    • Calls window.location.reload()
  5. If the fetch fails or clear_below is null, continues normally — the app boots as usual

Why It Doesn't Loop

After clearing and reloading, the browser fetches fresh assets from the CDN (Cloudflare cache is purged on every deploy). The fresh JS bundle has the current VITE_PKG_VERSION baked in at build time. Since the current version is >= clear_below, the check passes through. No cleared_at tracking needed.

Service Worker Isolation

The manifest is excluded from the service worker's control:

  • globIgnores in workbox.config.cjs prevents precaching clear-manifest.json
  • navigateFallbackDenylist includes /clear-manifest\.json$/ so the SW won't serve index.html for it
  • cache: 'no-store' on the fetch request bypasses the HTTP cache

This means even a broken SW won't intercept the manifest fetch. Combined with updateViaCache: 'none' on SW registration, the browser always checks for a fresh SW file and the manifest is always fetched from the network.

CDN Cache

The existing CI pipeline already purges the entire Cloudflare cache on every deploy to main. This ensures the new clear-manifest.json is served immediately after deploy — no stale copies at the edge.


CI Warning System

Impact Map

.clear-site-data.json in the repo root maps file patterns to clear targets:

{
  "triggers": {
    "cache": ["public/sw-sync.js", "workbox.config.cjs", "src/workers/sync.worker.ts"],
    "storage": [
      "src/utils/db-constants.ts",
      "src/utils/db.js",
      "src/utils/db-recovery.js",
      "src/workers/db.worker.ts"
    ]
  }
}
  • cache triggers — files that affect SW behavior or cache strategy
  • storage triggers — files that affect IndexedDB schema or database recovery

PR Check

On every pull request, CI runs scripts/check-clear-manifest.js which:

  1. Diffs changed files between the PR branch and the base branch
  2. Matches against glob patterns in .clear-site-data.json
  3. If any match, posts a PR comment:
## Clear-Site-Data Warning

The following changes may require updating `public/clear-manifest.json`.
If this release changes DB schema, service worker behavior, or cache strategy,
set `clear_below` to the current version to force client resets.

### Target: `storage`
- `src/utils/db-constants.ts`

The check uses continue-on-error: true — it warns but never blocks the build. Existing comments are updated rather than duplicated.

Manual Override

When automation underfires (refactors, renames, generated files that don't match patterns), manually edit clear-manifest.json in the same PR. The CI warning is advisory, not the only path to a reset.


Runbook

Triggering a Reset

  1. Decide the version threshold — typically the version you're about to release
  2. Edit public/clear-manifest.json:
    {
      "_docs": "Kill switch for bad releases. Set clear_below to a semver version to force all older clients to wipe local data and reload. Set to null to disable. See docs/clear-site-data-spec.md",
      "version": "0.0.2",
      "clear_below": "0.0.2"
    }
  3. Commit and deploy to main
  4. All clients below 0.0.2 will clear on their next page load

Emergency Reset (post-deploy)

If a bad release is already live and you need to force resets for clients on that version:

  1. Bump the version in package.json
  2. Set clear_below to the new version
  3. Push to main — CI builds, deploys, and purges CDN cache
  4. Clients on the bad version clear and reload with the fix

Disabling

Set clear_below to null and deploy:

{
  "version": "0.0.2",
  "clear_below": null
}

Verifying

After deploying a reset, check browser dev tools console for:

[clear-manifest] Client version 0.0.1 is below 0.0.2 — clearing site data

If you don't see this, check:

  • Is the manifest being served fresh? (curl -I https://your-domain/clear-manifest.json)
  • Is the SW intercepting it? (check Network tab — should show (disk cache) or network, not SW)
  • Is VITE_PKG_VERSION correct in the build? (check import.meta.env.VITE_PKG_VERSION in console)

Threat Model

Threats

T1. False positive — accidental mass data wipe

Innocent refactor touches a trigger file, someone bumps clear_below without understanding impact. Impact: users lose offline data, drafts, cached settings. Mitigations: CI posts PR warnings when trigger files change; clear_below change is visible in PR diff; data re-syncs from server after clear. Residual risk: medium (human error), but auditable and reversible.

T2. False negative — missed clear, unrecoverable client

DB schema change in an unmapped file. No CI warning, no one updates the manifest, clients brick. Impact: silent breakage, support tickets. Mitigations: directory-based globs in .clear-site-data.json, periodic audit of IndexedDB usage vs. impact map, manual override always available. Residual risk: medium — but this is the status quo today, and this system strictly improves on it.

T3. Infinite reload loop

Client clears, reloads, clears again. Impact: app unusable. Why it can't happen: after clearing, the reload fetches fresh assets with the current VITE_PKG_VERSION baked in. Current version >= clear_below, check passes. The only scenario is if the CDN serves stale JS after the manifest — but the CI pipeline purges the entire Cloudflare cache atomically.

T4. Malicious manifest injection

Attacker compromises R2 bucket, deploys { "clear_below": "999.0.0" }. All users lose local data. Impact: data loss (but recoverable from server). Mitigations: R2 write access restricted to CI via scoped API credentials; manifest is a static JSON file, not executable; auth cookies preserved so users stay logged in. Residual risk: low if infra credentials are secured.

T5. Broken SW blocks manifest fetch

Old SW intercepts /clear-manifest.json and serves stale copy. Impact: recovery never triggers. Mitigations: manifest excluded from SW precache via globIgnores; excluded from navigateFallback via denylist; fetched with cache: 'no-store'; SW registered with updateViaCache: 'none' so browser always checks for fresh SW. Residual risk: very low.

T6. Race during deploy

User has tab open, CDN serves new manifest before new JS assets. Boot check runs with old VITE_PKG_VERSION, triggers clear. Impact: user loses local data on next page load — but this is the intended behavior. The clear is correct; the client needs the new version. Active sessions are unaffected (check only runs on page load, not mid-session).

Risk Profile

Category Risk
Data loss (false positive) Medium — auditable and reversible
Availability (reload loops) Very low
Security compromise Low
Operational error Medium — mitigated by CI warnings
Recovery failure (false negative) Much lower than status quo

Future Considerations

These are not implemented. They're documented here for when/if the simple approach proves insufficient.

Granular targets — instead of clearing everything, clear only cache or only storage based on what changed. Would require a targets field in the manifest and conditional clearing in checkClearManifest().

Version ranges — instead of a single clear_below, support multiple ranges for cumulative clears across releases. Would require a ranges array and semver comparison logic.

Cloudflare Worker (Option B) — if a broken SW ever prevents the client-side check from running, a thin Cloudflare Worker could read a version cookie and inject a Clear-Site-Data HTTP header at the edge, bypassing all client-side code.

Manifest signing — embed a SHA256 hash of the manifest in the build to detect tampering. The boot check would verify the hash before acting on the manifest.

Automated manifest updates — have CI automatically set clear_below when trigger files change, removing the manual step. Deferred because the manual step is a safety feature, not a burden, at current release velocity.