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.
2.9 KiB
firebase/
Deployable configuration for the Firebase project backing crdt_sync /
crdt_sync_dart sync. Language-neutral, so it lives here rather than under
either library.
This repository is public. Nothing here names a concrete project, database
URL, account or uid. Those live in a local, 0600 config file — see
Local configuration.
Why Realtime Database, over REST
- No SDK. Official FlutterFire has no Linux desktop support, and one
caller (
wake_alarm's PC side) is a dependency-free systemd job. The REST endpoints are plain HTTPS and behave identically on Linux, Android and headless. - No per-operation quota on the Spark (no-cost) plan. RTDB bills only storage and bandwidth, so a misbehaving sync loop cannot exhaust a daily budget and silently stop working mid-day. Firestore's 20k-writes/day cap would be a second way to hit a wall, for querying this workload never does.
- Path model matches the existing layout —
<prefix>/<deviceId>/<file>— so no data remodelling.
Deliberately not used: Cloud Storage (requires a linked billing account since 2026‑02‑03) and Cloud Functions. Avoiding both keeps the project structurally incapable of generating a bill.
database.rules.json
Scopes the whole database to a single Firebase Auth uid.
auth != null alone would not be safe: email/password sign-up is open
through the identitytoolkit REST API to anyone holding the Web API key, and
that key ships inside the Android APKs. Pinning the uid means a stranger who
registers an account still reads and writes nothing.
To deploy, substitute the real uid (Firebase console → Authentication → Users → User UID) and paste into Realtime Database → Rules → Publish.
Verify the rules are live — an unauthenticated request must be refused:
curl -s "$DATABASE_URL/.json"
# {"error" : "Permission denied"} <- expected
Local configuration
Everything environment-specific lives in ~/.config/crdt-sync/, mode 0600,
outside version control:
| File | Contents |
|---|---|
firebase.json |
apiKey, databaseUrl, projectId, uid, email |
password |
the sync account's password, no trailing newline |
database.rules.json |
the deployed rules, with the real uid filled in |
The Web API key is not a secret — it is a public project identifier that ships inside client apps. The password and refresh tokens are the real secrets.
Credentials at runtime
No service-account key is used anywhere: it would bypass the security rules entirely and is trivially extractable from an installed APK. Devices sign in as the single user and hold a refresh token:
- Android —
flutter_secure_storage - Linux —
~/.config/<app>/firebase.json, mode0600
ID tokens last about an hour, so the refresh exchange is mandatory rather than
optional — diet_guard's PC timer alone runs 96 times a day.