Status: Living — update whenever any contract version bumps. Mixed-version sync policy: ADR-0016.
Crosscue is no longer a single binary: it is an app, a sync format, a server, and a widget that evolve at different speeds. This page is the one place that states every versioned contract, who depends on it, and what must stay compatible with what.
| # | Contract | Current version | Written by | Read by | Compatibility rule | Bump procedure |
|---|---|---|---|---|---|---|
| 1 | Local DB schema (Drift) | v7 (app_database.dart → schemaVersion) |
The app | The same app | Migrations are additive and run forward-only on upgrade; each historical step has a migration test | Increment schemaVersion, add migration + test (checklist in app_database.dart) |
| 2 | Sync blob envelope | schema 1 (SyncBlob.currentSchemaVersion) |
Every synced device | Every synced device | Readers ignore unknown payload fields; blobs with a newer schema are skipped (decode() → null). Prefer additive payload fields over schema bumps — see ADR-0016 for the mixed-version policy |
Bump currentSchemaVersion only under ADR-0016’s flag-day rules |
| 3 | Worker D1 schema | migrations 0001–0007 (0007 = ops_meta, the retention heartbeat store, #262) |
Worker deploys | Deployed Worker | Migrations must be backward-compatible with the currently deployed Worker (additive), because migrate runs before deploy; never edit an applied migration | New numbered SQL file; migrate → deploy, staging before prod (DEPLOYMENT.md) |
| 4 | Worker HTTP API | unversioned paths (contract: API.md) |
Worker | Every shipped app version | Worker changes must stay compatible with the oldest app version still in the field, unless that version is deliberately retired via the min-client lever (see below) | Additive request/response changes by default; removals require raising MIN_SUPPORTED_CLIENT first |
| 5 | Widget payload | crosscue_widget_v1, "version": 1 |
App (HomeWidgetService) |
iOS widget extension + Android CrosscueWidgetProvider (#223) — either may be older than the app, or newer after a partial update |
Additive only: new optional keys/rows; never repurpose version for additive fields |
New key name (_v2) only for breaking shape changes |
Related but unversioned: the app↔extension App Group route token
(pendingIntentRoute, additive string tokens by design — see
ios-app-intents.md) and the export/import backup file from the privacy
screen (treat as contract #1’s serialized form; verify import of old exports
when bumping #1).
These are the guarantees to preserve when changing anything above:
Contract #4’s “support the oldest fielded client” rule now has a deliberate escape hatch:
X-Crosscue-Client: <platform>/<semver> on every Challenge
Boards request (since v1.4.3).MIN_SUPPORTED_CLIENT var (per-env in
wrangler.toml; unset everywhere by default = no enforcement) rejects
missing/unparsable/lower versions with a structured 426 client_too_old
on every route.PRODUCT.md), and queued results are held, not dropped.Rules for using it: raising the minimum is a deliberate, logged decision — staging first, release-notes mention, and remember it also cuts off app versions that predate the header (< v1.4.3). Until the dual-host/v1.4.3 generation dominates the field, leave it unset.
CrosshareDownloader is an HTTP + HTML scraper, and three user-visible
features sit on it: the home “Past puzzles” section, daily auto-download, and
Challenge Boards eligibility (crosshare_daily_mini is the only accepted
result source). A Crosshare markup change degrades all three at once.
Degradation policy (stale-but-honest):
ChallengeResultOutbox)
and flushes later; a download outage means no new eligible puzzle, which
boards experience as a quiet day, not an error.Risk-reduction tasks:
.github/workflows/crosshare-canary.yml
runs crosscue/tool/crosshare_canary.dart against the live site weekly
(Mondays 06:17 UTC, also dispatchable) — month listing + newest-mini
download + parse. On failure it opens (or comments on) a deduplicated
“Crosshare canary failing” issue. The degradation rendering is locked by
test/features/home/presentation/widgets/past_puzzles_section_test.dart.CONVENTIONS.md source table). (owner action, untracked)../challenge-boards-159-breakdown.md)
to decouple board eligibility from client-side scraping success.crosscue/lib/core/database/app_database.dart schemaVersion (Drift)
crosscue/lib/core/sync/models/sync_blob.dart currentSchemaVersion (sync envelope)
crosscue/backend/challenge_boards/migrations/ D1 schema (numbered SQL)
crosscue/backend/challenge_boards/API.md HTTP contract (prose)
docs/architecture/ios-widget-setup.md widget payload schema