flock's privacy rests on end-to-end encryption — but the one court order that
bypasses it entirely is a compelled targeted, backdoored build shipped to one
user (see docs/PRIVACY.md, "When a court comes knocking", and the plan in
docs/plans/2026-07-06-verifiable-builds.md). We cannot make ourselves unable to
comply with a lawful order. We can make compliance detectable: the release
APK is reproducible, so anyone can rebuild it from a tagged commit and confirm
the shipped bytes match this public source. A build that didn't match — a build
carrying something this source does not — would stand out.
This page is the procedure. It needs no flock context and no trust in us beyond the source you are reading.
The unsigned release APK is byte-for-byte reproducible from a clean commit.
Measured 2026-07-06: four independent clean builds (two raw Gradle, the full
build-apk.sh release pipeline, and npm run apk:verify) produced an identical
app-release-unsigned.apk (sha256 d663c61f… for that dev tree). The build carries
no wall-clock timestamp — both the build id and the "built at" date derive from the
commit (vite.config.ts), so the same commit yields the same bytes whoever builds
it, whenever, in any timezone.
The signed APK (flock-release.apk, what you install) is not something a third
party can reproduce, because only we hold the signing key. That is expected: you
verify the unsigned content matches this source, then separately trust our
signature over it (Android enforces that every update is signed by the same key).
Reproducibility is only guaranteed against the same major toolchain. Pinned by the
repo where possible (package-lock.json, the Gradle wrapper, patch-android.mjs):
| Component | Version | Pinned by |
|---|---|---|
| JDK | 21 (verified 21.0.11) | build-apk.sh (Homebrew openjdk@21) |
| Node / npm | 24 / 11 (verified 24.14.0 / 11.9.0) | — (use the same major) |
| Capacitor | 8.4.1 | package-lock.json |
| Android Gradle Plugin | 8.13.0 | @capacitor/android template |
| Gradle | 8.14.3 | android/gradle/wrapper |
| Kotlin Gradle plugin | 2.1.0 | native/patch-android.mjs |
| Android SDK | platform 36, build-tools 35.0.0 | android/variables.gradle |
| minSdk / compileSdk / targetSdk | 24 / 36 / 36 | android/variables.gradle |
# 1. Get the exact source for the release you want to check.
git clone https://github.com/forgesworn/flock && cd flock
git checkout <release-tag-or-commit> # MUST match the build you're verifying
git status --porcelain # must be empty — a dirty tree is marked +dev
# 2. Exact dependencies, and a JDK 21.
npm ci
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home # or your JDK 21
# 2b. Regenerate the self-hosted basemap assets (gitignored, deterministic):
node scripts/fetch-basemap-assets.mjs # glyphs + sprite
# The town PMTiles under app/public/basemap/ are build-host local state
# (gitignored, produced with go-pmtiles — see scripts/fetch-basemap-assets.mjs).
# A release built WITH them bundled (e.g. 7363e51) will not byte-match a
# rebuild without the same files; restore them before building.
# 3. Build the unsigned release APK and print its hash.
npm run apk:verify
# (on a fresh clone this first runs `npx cap add android` to generate the
# gitignored native project from the committed scripts, then builds.)The command prints:
reproducibility anchor (sha256):
<64-hex> app/build/outputs/apk/release/app-release-unsigned.apk
Compare the printed hash to the released anchor, published in two places so a compromised or compelled host cannot swap both the APK and its hash unnoticed:
-
On the download host —
https://flock.forgesworn.dev/downloads/flock.apk.unsigned.sha256(emitted bydeploy/deploy.shalongside the APK). -
Off-host — an SSH-signed git tag
release/<build>whose message embeds the same hash, plus the append-only ledgerdocs/transparency/RELEASES.jsonl. This rides git (not our web host) to every clone and the forge, so a targeted build absent from this signed history is anomalous on its face. Verify it:git config gpg.ssh.allowedSignersFile docs/transparency/allowed_signers git verify-tag release/<build> # → Good "git" signature for releases@flock.forgesworn.dev
The tag message carries
unsigned APK sha256: …— it must equal the hash you rebuilt above and the on-host anchor. (A project-key Nostr note is the intended third, fully-independent channel; seedocs/transparency/README.md.)
Match across channels → the shipped release is built from this exact source. Then confirm the APK you actually downloaded is properly signed and by the expected key:
apksigner verify --print-certs flock.apk # exit 0; certificate SHA-256 must equal
# the canonical key in docs/SIGNING.md
# (320ab5bc…25869877)- Reproducibility ≠ immunity. It makes a silent, targeted build detectable. It does not make a published, universal change impossible — that is what reading the source and the transparency log is for.
- Toolchain drift breaks byte-identity. A different AGP/Gradle/JDK major
version can change output. Verify with the versions above; CI should rebuild and
diff on each release tag so a regression is caught, not discovered later.
(Observed 2026-08-08: even same-major toolchains on different hosts emitted a
byte-different
AndroidManifest.xml— identical permission set, different element ordering from the manifest merger. Compare entry-by-entry CRCs, not just the whole-file hash, when localising a mismatch.) - Map assets are inputs too. A rebuild on 2026-08-08 of the deployed
7363e51APK confirmed every dex/resource entry byte-identical; the only payload differences were the gitignored basemap inputs (glyphs/sprite + five town PMTiles) absent from the fresh clone. Verifiability of the map bundle therefore rests on pmtiles provenance (the go-pmtiles commands) or on committing those inputs — an open decision recorded indocs/ROADMAP.md. - Transitive dependency drift. Direct versions are pinned; a far-future rebuild could still resolve different transitive artefacts. Gradle dependency locking is a future hardening step (plan doc).
- The signing key is trust, not proof. Reproducibility proves the content
matches source; that the update is signed by our key is a separate trust
anchor (
native/release.keystore, backed up out-of-band, never in the repo).
docs/PRIVACY.md— the threat this answers ("When a court comes knocking").docs/plans/2026-07-06-verifiable-builds.md— the full plan (PWA side, transparency log).native/build-apk.sh(verifymode),deploy/deploy.sh(publishes the anchor),vite.config.ts(commit-derived, timezone-independent build stamp).