utils/firebase/README.md
Krzysztof Rudnicki 02e2e13790 crdt_sync: add a Firebase Realtime Database transport behind a RemoteStore seam
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.
2026-08-03 18:14:25 +02:00

73 lines
2.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.