feat: opt-in local-first client sync service — cross-device localStorage data (Paid+) #56

Open
opened 2026-04-26 09:01:38 -07:00 by pyr0ball · 0 comments
Owner

Problem

Several CF products store meaningful user state in localStorage only:

  • Kiwi: cook log (kiwi:cook_log), dismissed recipes, bookmarks
  • Peregrine: dismissed jobs, local draft state
  • Others TBD

This data is invisible to other devices and lost on browser clear. But syncing it unconditionally creates privacy surface — it turns ephemeral local state into a cloud record.

Proposed solution

A lightweight, opt-in sync service for localStorage-keyed data. Local-first by default; cloud sync is a user-initiated upgrade.

Principles

  • Off by default. Local state stays local until the user explicitly enables sync.
  • User-owned keys. Sync is keyed by the user's account ID and product slug. No cross-user visibility.
  • Selective. Users can choose which data classes to sync (e.g. "sync cook log" but not "sync dismissed recipes").
  • Deletable. One-click wipe of all synced data from the server. No retention after deletion.
  • Plain-language consent. The opt-in UI must clearly state what is synced, where it lives, and how to delete it.

Tier placement

  • Free: localStorage only, no sync
  • Paid+: opt-in sync enabled — positions as a convenience feature, not a data-collection feature
  • Framing: "Keep your cook history and bookmarks in sync across devices" — not "back up your data to the cloud"

Architecture sketch

  • POST /api/v1/sync/push — client uploads a JSON blob keyed by {product}:{data_class}
  • GET /api/v1/sync/pull — client fetches latest blob for each subscribed key
  • DELETE /api/v1/sync/{key} — wipe a specific data class from server
  • Server stores blobs as-is (no schema inspection) — dumb key-value store, no PII indexing
  • Conflict resolution: last-write-wins per key (simple; revisit if multi-device edits become common)
  • Could live in cf-core as a shared module used by all products

Products that would benefit

  • Kiwi: cook log, bookmarks, dismissed recipes (unblocks orbital recipe cadence cross-device)
  • Peregrine: dismissed jobs, draft state
  • Snipe: saved searches, dismissed listings
  • All future products: any product using localStorage for persistent state

Out of scope

  • Real-time sync (websocket / CRDTs) — last-write-wins is sufficient for this use case
  • Syncing DB-backed data (saved recipes, inventory) — those already sync via the cloud DB

References

  • Kiwi cook log currently localStorage-only: frontend/src/stores/recipes.ts:logCook()
  • kiwi#120 (orbital recipe cadence) deferred cross-device aspect to this ticket
## Problem Several CF products store meaningful user state in localStorage only: - Kiwi: cook log (`kiwi:cook_log`), dismissed recipes, bookmarks - Peregrine: dismissed jobs, local draft state - Others TBD This data is invisible to other devices and lost on browser clear. But syncing it unconditionally creates privacy surface — it turns ephemeral local state into a cloud record. ## Proposed solution A lightweight, **opt-in** sync service for localStorage-keyed data. Local-first by default; cloud sync is a user-initiated upgrade. ### Principles - **Off by default.** Local state stays local until the user explicitly enables sync. - **User-owned keys.** Sync is keyed by the user's account ID and product slug. No cross-user visibility. - **Selective.** Users can choose which data classes to sync (e.g. "sync cook log" but not "sync dismissed recipes"). - **Deletable.** One-click wipe of all synced data from the server. No retention after deletion. - **Plain-language consent.** The opt-in UI must clearly state what is synced, where it lives, and how to delete it. ### Tier placement - **Free**: localStorage only, no sync - **Paid+**: opt-in sync enabled — positions as a convenience feature, not a data-collection feature - Framing: "Keep your cook history and bookmarks in sync across devices" — not "back up your data to the cloud" ### Architecture sketch - `POST /api/v1/sync/push` — client uploads a JSON blob keyed by `{product}:{data_class}` - `GET /api/v1/sync/pull` — client fetches latest blob for each subscribed key - `DELETE /api/v1/sync/{key}` — wipe a specific data class from server - Server stores blobs as-is (no schema inspection) — dumb key-value store, no PII indexing - Conflict resolution: last-write-wins per key (simple; revisit if multi-device edits become common) - Could live in cf-core as a shared module used by all products ### Products that would benefit - Kiwi: cook log, bookmarks, dismissed recipes (unblocks orbital recipe cadence cross-device) - Peregrine: dismissed jobs, draft state - Snipe: saved searches, dismissed listings - All future products: any product using localStorage for persistent state ### Out of scope - Real-time sync (websocket / CRDTs) — last-write-wins is sufficient for this use case - Syncing DB-backed data (saved recipes, inventory) — those already sync via the cloud DB ## References - Kiwi cook log currently localStorage-only: `frontend/src/stores/recipes.ts:logCook()` - kiwi#120 (orbital recipe cadence) deferred cross-device aspect to this ticket
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Circuit-Forge/circuitforge-core#56
No description provided.