Everything an app needs to move from GitHub to Firebase without each one
reinventing the wiring.
Python:
- _config.py: FirebaseConfig.load() + firebase_client_for(app) +
mirror_client_for(app, github). One config file, read in one place.
Credential caches stay per-app (~/.config/<app>/firebase_auth.json) --
one file written by several processes is a corruption waiting to happen.
- tool/preflight_firebase.py: validates the config before any data moves.
Catches a wrong-region databaseUrl (which answers 404 with a correctUrl
body rather than an error, so it would otherwise pass every check and
break mid-migration) and confirms the uid in the ID token matches the uid
the rules pin -- sign_in() returns None and discards localId, so the uid
is decoded from the JWT.
- tool/interop_seed.py: the missing Python half of the cross-language
check. interop_check.dart asserts it can see a record written by Python,
but nothing ever wrote one, so it could only ever fail.
Dart:
- firebase_config.dart: the same config shape, kept free of dart:io so it
works on web and Android, where the file does not exist at all and
credentials belong in flutter_secure_storage.
- PersistedSyncStateStore: the revision cache, persisted. Without it a
phone forgets every peer revision on each cold start and re-downloads
everything -- the exact traffic the revision scheme exists to avoid, and
what the free-tier headroom depends on.
Verified live against the real database: preflight passes all five checks,
the Dart smoke test passes end to end, and Python <-> Dart interop is OK.
Python 226 tests at 100% branch coverage; Dart 218 tests, both new files
at 100% line coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJ9sxEw3ymzXNjL7SHNpJc
Extracts the three-method remote surface GitHub was reached through into a
`RemoteStore` interface, then implements it against Firebase Realtime
Database over REST, so apps can move backend with a constructor change.
Why REST and not the SDKs: official FlutterFire has no Linux desktop
support, and `wake_alarm`'s PC side is a dependency-free systemd job. The
RTDB REST endpoints are plain HTTPS and behave identically on Linux,
Android and headless. Firebase is a transport, never the datastore -- each
app keeps its local log as the source of truth, so offline-first survives.
Why RTDB and not Firestore: on the Spark plan RTDB bills only storage and
bandwidth with no per-operation quota, so a runaway loop cannot exhaust a
daily budget and silently stop syncing mid-day.
Two savings make the no-cost tier a guarantee rather than a hope. Measured
against the GitHub-backed history this replaces: 4894 of 5542 commits
(88.3%) were byte-identical no-ops, and every tick re-downloaded every
peer's whole log. Both are now driven by one sha256 revision -- an
unchanged log is not re-pushed, and an unchanged peer is not re-fetched.
That turns egress from "ticks x log size" (~700 MB/month, and growing with
the log) into "edits" (~40 MB/month against a 10 GiB cap).
Revisions are one node per device, written only by its owner, rather than
one shared map -- a whole-map write would erase every peer's entry, after
which those peers would look permanently unchanged and never be fetched
again. Per-device keys make that impossible rather than merely avoided.
`.` is illegal in an RTDB key and the REST `.json` is a format suffix, so
paths get a reversible `~XX` escape (`log.json` -> `log~2Ejson`). Lossy
extension-stripping would break `todo`, whose listings return filenames.
`MirrorStore` / `MirrorSyncClient` dual-writes during the cutover. Reads
consult *both* backends: with primary-only reads a migrated device would
never see an un-migrated peer's writes while still mirroring its own back
-- silent one-directional convergence with no error raised. Writes are
asymmetric on purpose: a primary failure fails the tick, a mirror failure
is logged and survived.
Auth is Firebase email/password over identitytoolkit REST, with mandatory
refresh-token rotation. No service-account key anywhere: it would bypass
the security rules and is trivially extractable from an APK.
Verified live against a real database, not only mocks: sign-in and refresh,
key escaping round-tripping byte-for-byte, an unchanged tick writing
nothing, two devices converging, a revoked session throwing rather than
returning null, and a log written by the Python client being read by the
Dart one.
No app is migrated by this commit; every app still runs on GitHub.
Dart: 194 tests. Python: 203 tests at 100% branch coverage. No lint
suppressions added.
Make the shared client a superset of what todo's and diet_guard-app's local
GitHubClient copies provide, so every app can drop its copy and sync through
one client. canAccessRepo() is a lightweight connection test (bare repo GET,
never throws); deleteFile() resolves the file's own sha and no-ops when
absent. Mirrored on the Dart and Python sides. 90 Dart + 78 Python tests, 100%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ
Both CRDT libs are now consumed (crdt_sync_dart: diet_guard app, workout_app,
phone_app, todo; crdt-sync: diet-guard, screen-locker). Reconcile the
sub-package READMEs' old standalone git URLs to the monorepo-subdirectory
form, and bump the crdt_sync examples to v0.2.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ
crdt_sync_dart: new domain-agnostic LogStore -- an in-memory Log with a
reactive changes stream and upsert/delete(sticky tombstone)/replaceAll,
behind an injectable LogPersistence port -- plus logToJson/logFromJson.
FileLogPersistence (atomic temp-file-then-rename, generalizing diet_guard's
LogStorageService) lives behind a separate crdt_sync_io.dart entrypoint so
the core barrel stays pure Dart / web-safe.
crdt-sync (Python): mirror dump_log/load_log/read_log/write_log sharing the
same on-disk JSON format so a log written on one side parses on the other.
Load/save only -- the reactive store is a Dart/Flutter convenience with no
Python equivalent by design (Python consumers are headless sync ticks).
Querying stays app-side: a Record's fields are opaque to the library, so
filtering/sorting/search is the caller's job over LogStore.values.
Both packages bumped to 0.2.0. 83 Dart tests + 71 Python tests (100% branch
coverage) green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ
Record.toJson() was writing the delete-clock field as deletedHlc while
the Python package's Record.to_dict() writes deleted_hlc -- an
undetected cross-language wire divergence: a tombstoned record synced
Python<->Dart would silently lose its delete clock on the receiving
side. Also widen syncLog's decode error handling from FormatException
alone to also catch TypeError, matching the Python side's
(ValueError, KeyError, TypeError) so a valid-JSON-but-wrong-shape push
from an incompatible writer is skipped per-device instead of aborting
the whole sync. Adds a fixture test shared verbatim with the Python
package's test suite so this class of divergence fails loudly going
forward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017McuA1nAi6jBT1EaS5BrVf
Pure-Dart mirror of the Python crdt-sync package, module-for-module and
test-for-test, so the merge algorithm canonically agrees on both sides of
a sync between a Dart client and a Python one. No Flutter SDK dependency
(package:http only) per Workstream D's scoping decision (2026-07-04).
59 tests, clean dart analyze.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017McuA1nAi6jBT1EaS5BrVf