database.rules.json carries YOUR_FIREBASE_UID on purpose -- this repo is public and the uid is the one value that must not be in it. But nothing stopped anyone publishing the template as-is, and rules pinned to a uid that matches no account deny every read and write. The symptom is every device failing at once with a bare permission error, which reads like an outage rather than a bad paste. render_rules.sh substitutes the uid from ~/.config/crdt-sync/firebase.json and fails closed on anything unsafe: a surviving placeholder, missing `auth != null`, a missing uid pin (that one matters -- `auth != null` alone lets any stranger who self-registers through the public Web API key read everything), or output that is not JSON. Verified: its output is byte-identical to the deployed ~/.config/crdt-sync/database.rules.json, and the live database still answers an unauthenticated read with 401. All three failure paths exercised. Also corrects the README, which described the in-repo copy as "the deployed rules, with the real uid filled in" -- it never was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG |
||
|---|---|---|
| .. | ||
| database.rules.json | ||
| README.md | ||
| render_rules.sh | ||
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.
The uid here is a placeholder on purpose — this repo is public, and the uid is the one value that must not appear in it. Publishing this file verbatim yields rules matching no account, which locks every app out of the project until someone notices; the failure looks like a permission error from every device at once.
So do not hand-substitute. Render it:
./render_rules.sh # prints deployable rules, real uid filled in
./render_rules.sh --check # validates without printing the uid
It reads the uid from ~/.config/crdt-sync/firebase.json and refuses to emit
anything still containing the placeholder, missing auth != null, missing a
uid pin, or not parsing as JSON. Paste its output 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 rendered rules, real uid filled in (see render_rules.sh) |
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.