mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-13 00:10:59 +01:00
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.
73 lines
2.9 KiB
Markdown
73 lines
2.9 KiB
Markdown
# 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](#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:
|
||
|
||
```bash
|
||
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`, mode `0600`
|
||
|
||
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.
|