Skip to content

docs(roadmap): add v1.5 Resilience & offline play; renumber later releases#171

Open
code418 wants to merge 1 commit into
masterfrom
docs/roadmap-offline
Open

docs(roadmap): add v1.5 Resilience & offline play; renumber later releases#171
code418 wants to merge 1 commit into
masterfrom
docs/roadmap-offline

Conversation

@code418

@code418 code418 commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Why

The claim flow is a chain of live callable round-trips with no offline story at all — no connectivity detection (the app infers "offline" from FirebaseFunctionsException.code == 'unavailable' at three call sites), no timeouts, no retries, no local cache.

Two concrete failures today:

  1. A player can lose a claim they earned. The claim write is idempotent (deterministic doc ID claims/{uid}_{box}_{day}), but the response is not. If the response is dropped on a flaky link, the retry hits the "already claimed today" fast-path and the client renders an empty state — no points, no confetti. The claim landed; the player never sees it.
  2. A player in a notspot can't play at all.

What this adds

A new v1.5 — Resilience & offline play, in four phases:

  1. Survive a flaky linkattempts/{attemptId} idempotency doc, callable timeouts + jittered retry, stop tearing down the quiz sheet on network failure, getLastKnownPosition() fallback, offline banner. Ships alone and fixes the lost-claim bug; most of the user value is here.
  2. A backend that can accept a claim made in the past_claimCore.ts extraction, eventTime + backfill, two-sided travel-speed check, streakFromClaimDays.
  3. Offline claiming, warm path — replay a cached (coordinate-free) scan payload; full quiz preserved.
  4. Blind capture + offline discovery — marked explicitly conditional: it carries most of the attack surface and the least payoff. Ship 1–3, measure, and only build this if the warm path demonstrably doesn't cover real usage.

Three findings from tracing the flow shape the design, and are recorded so they don't have to be rediscovered:

  • The quiz is 100% client-side — the server never sees the answer — so it's no obstacle to offline play.
  • startScoring derives which boxes you claimed from position alone, so a claim can be represented as "I stood here, then" and adjudicated later.
  • The travel-speed check would reject a flushed queue: it floors elapsed time at 1s against Date.now(), so any two queued boxes >~32 m apart hard-reject the second.

Two traps written down explicitly

  • A naive offline claim is an armchair farm. Because the claim ID contains the day, an attacker can save the outbox, add 86,400,000 ms to every capture time, and resubmit tomorrow — forever. The chain validates, since a uniformly-shifted trace has identical implied speeds. Capture times must be bounded by a server-issued token whose issuedAt/serverNow bounds force the offline timeline to fit inside a wall-clock window the server actually observed.
  • Never pass a backdated today to updateUserLeaderboards. It would see a periodKey mismatch, set existing = [], and tx.set(..., { merge: false })wiping the daily leaderboard for every player. Silent cross-user data loss.

Schedule impact

App Check server-side enforcement moves up from Reach into v1.5 — it's a hard prerequisite for an endpoint that accepts client-supplied timestamps, and enforceAppCheck currently appears nowhere in functions/src. The iOS AppleProvider wiring stays in Reach, which makes it a launch blocker there (with enforcement already live, an iOS build without it fails every callable).

Renumbering: Engagement & avatars → v1.6, Collection & content → v1.7, Reach → v1.8. Growth stays v2.0.

Also

  • Gitignores /.worktrees/ (git worktree add inside the repo otherwise shows as untracked).
  • Records a drive-by bug found while tracing: ClaimAction.kt (Android Auto) calls FirebaseFunctions.getInstance() with no region, targeting us-central1 while every function deploys to europe-west2. The CI region guard only scans lib/, so Kotlin call sites are invisible to it — the Auto claim button may simply not work.

Docs only — no code changes, no CI risk.

🤖 Generated with Claude Code

…eases

The claim flow is a chain of live callable round-trips with no offline story:
no connectivity detection, no timeouts, no retries, no local cache. Two
concrete failures today — a claim whose response is dropped is silently lost
(the write is idempotent, the response is not), and a player in a notspot
cannot play at all.

Adds v1.5 as a four-phase release: flaky-link resilience (attempts/{attemptId}
idempotency doc, timeouts + retry, preserved sheet state), a backend that can
accept a claim made in the past (eventTime + backfill, two-sided travel-speed
check, streakFromClaimDays), warm-path offline claiming, and blind capture +
offline discovery — the last marked explicitly conditional, since it carries
most of the attack surface and the least payoff.

Records the design constraints that are easy to miss: a naive offline claim
lets an attacker shift the outbox by 24h and re-farm it forever, so capture
times must be bounded by a server-issued token; and a backdated `today` passed
to updateUserLeaderboards would wipe the daily board for every player.

Pulls App Check server-side enforcement up from Reach into v1.5 — it is a
prerequisite for accepting client-supplied timestamps — leaving the iOS
AppleProvider wiring in Reach, which now makes it a launch blocker there.

Renumbers: Engagement & avatars -> v1.6, Collection & content -> v1.7,
Reach -> v1.8. Growth stays v2.0.

Also gitignores /.worktrees/ (git worktree add inside the repo would otherwise
show as untracked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant