Files
thoughtsync/docs/android-distribution.md
T
bvandeusenandClaude Opus 5 a0c789b3ba
Android / Build, or is the channel already serving this? (push) Successful in 2s
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
docs: the image tag list said something step 6 stopped being true
`:<git-sha>` is on `main` only now — a sha tag per dev push was a rollback
target nobody had ever pulled — and `:<version>` never existed as an image tag
after rule 145 was narrowed. Both were still documented.

`docs/android-distribution.md` also said `:dev`, `:latest` and `:<version>` all
ship a client, which is now two-thirds true and misses the more useful fact: the
channel IS the image you run, so a stable server serves a stable client. Worth
saying because until step 3 it was hard-wired to the dev release on every branch
and did the opposite.

This push is also the skip-if-exists verification. It touches neither client's
file set, so both `decide` jobs should report the channel already serving the
current version and skip a 6- and a 9-minute build — while the guard still runs
on that path (§6.3).

#3146

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 00:27:43 -04:00

107 lines
4.5 KiB
Markdown

# Getting the Android app onto your server
ThoughtSync's server hands out the Android client it syncs with. Once an APK is in
place, anyone with an account on that server can download it from **Account →
Linked devices**, and linked phones can update themselves from it.
This is deliberate rather than incidental. The build is not on an app store and the
Fabled-Git instance is private, so a release page is no use to a self-hoster — but
the server holding their notes is something they already trust and already reach.
It also keeps the two in step: client and server negotiate a sync protocol version
before linking, so a server that serves the client cannot hand out a phone it
cannot talk to.
## Where it comes from
**Normally: nowhere. It is already in the image.**
CI fetches the published Android build into every server image it builds, so
`:dev` and `:latest` both ship a client. `docker compose pull && docker compose
up -d` delivers a new server and a new client together, and there is nothing to
copy.
**The channel is a property of the image you run.** A `:dev` image bakes in the
dev-channel APK, `:latest` the stable one — so pointing a phone at a stable
server gets it a stable client, with no second place holding that decision. (Until
M314 step 3 the fetch was hard-wired to the dev release on every branch, so a
stable server served a dev client.)
An image therefore carries the *newest* client on its channel rather than one
pinned to a version. That is deliberate: the two negotiate a sync protocol version
before they link, so a mismatch is caught by the handshake rather than by
pinning.
## Overriding it
If you want a specific build — testing something, or holding back — drop it in
`/var/thoughtsync/client/` and it wins over the image's copy.
Two files, both required:
| File | What it is |
| --- | --- |
| `thoughtsync.apk` | the client |
| `thoughtsync-android.json` | `{version_name, version_code, size, sha256}` |
The sidecar exists because an APK keeps its version in a binary manifest that
needs the Android build tools to read. CI writes it beside the APK, where the
real values are already known.
`/var/thoughtsync` is the same volume that holds attachments (`Config.DATA_DIR`),
so a build dropped there survives container recreation — and survives an image
upgrade, which is the point of an override.
Both files are published to the rolling `dev` release on every green Android
build:
```sh
REPO=https://git.fabledsword.com/bvandeusen/thoughtsync
TOKEN=... # a Fabled-Git token with read access; the instance is private
for f in thoughtsync.apk thoughtsync-android.json; do
curl -fsSL -H "Authorization: token $TOKEN" \
-o "/tmp/$f" "$REPO/releases/download/dev/$f"
done
# Copy the sidecar LAST. A sidecar that does not match the APK beside it is not a
# client, so a half-finished copy falls back to the image's build rather than
# advertising a lie.
docker compose cp /tmp/thoughtsync.apk app:/var/thoughtsync/client/
docker compose cp /tmp/thoughtsync-android.json app:/var/thoughtsync/client/
```
To go back to whatever the image ships, delete both files.
## Checking it took
```sh
curl -s http://localhost:5000/api/client/android
```
A server with a client answers with the version, size and digest. A server without
one answers `404` — and the download card in the web UI is hidden rather than
offering a button that fails.
## What happens if you get it wrong
All of these describe an override in `/var/thoughtsync/client/`. A broken
override does not take the feature away — it falls through to the build the image
shipped with, which is the whole reason precedence runs in that direction.
- **Only the APK, no sidecar** — not a client. The server cannot state a version
it has no way to read.
- **Mismatched pair** (new APK, old sidecar) — not a client. It will not serve
one build while describing another.
- **Neither, and no baked copy either** — the server reports no client, the UI
hides the card, and `/api/client/android` returns 404. That is the state of any
image built before the first Android build ever published, and nothing about it
is an error.
## Signing, and why replacing the APK is safe
Every release build is signed with the same key, so a phone can install a newer one
straight over the old one and keep its notes. That was not true before August 2026
— builds until then were signed with a throwaway key per CI run, and each install
required uninstalling the last (Scribe #2803). If you are carrying a build from
before that, expect to uninstall once more and sync anything you care about first.