Two separate reasons updates were impossible, both fixed here. **Every CI build was signed with a different key** (issue #2803, measured with `apksigner --print-certs` across two runs). No signing config meant AGP's debug keystore, which AGP GENERATES when absent — and every job starts from a fresh container. So no build could ever be installed over another: the only way through was uninstall-then-install, which deletes the app's database and every local note with it. **versionCode was hardcoded to 1.** `build.gradle.kts` has read a `THOUGHTSYNC_VERSION_CODE` property since the skeleton landed; nothing ever passed it. Even with signing fixed, every APK would have claimed to be the same version and nothing could tell a newer one existed. It now comes from `GITHUB_RUN_NUMBER` — the same monotonic counter the desktop's version scheme already uses, needing no state between runs and immune to the shallow checkout that makes a commit count useless here. The version NAME comes from the desktop's `build-version.sh`, so both surfaces report one product version rather than two that can disagree. **The alias is hardcoded, not a secret.** It is fixed for the life of the app and already written into the certificate every install carries; hiding it would buy nothing and stop this file describing its own signing. Two secrets, not three — and PKCS12 cannot hold a key password distinct from the store password anyway, so `keyPassword` is the same value by necessity rather than by shortcut. **The lane now builds RELEASE when it can sign, debug when it cannot.** That is not cosmetic. A debug APK is `debuggable`, which on a phone holding personal notes and a device sync token means anyone with adb can read both. Which meant confronting something the release path would have shipped quietly: `cargoNdkDebug` was hardcoded to the debug Cargo profile and every variant took its `.so` from it, so `assembleRelease` would have packaged an UNOPTIMISED store and sync engine. Now one `cargoNdk` task takes its profile from a property, and the whole run uses one profile. A debug/release task pair would have been the tidier shape and would have made a run that both type-checks and packages pay the four-minute cross-compile twice — this runner has no working Gradle or Cargo cache, so that cost is real on every push. The run prints the signing certificate after assembling, so the fingerprint can be compared against the one recorded at generation. Signing with the wrong key produces a perfectly valid APK that simply refuses to install — a failure that otherwise surfaces on the device, long after the run is green. `.gitignore` learns `*.jks`, `*.keystore`, `*.p12`, `*.b64` first, so generating a keystore anywhere near this tree cannot go wrong. Also corrects the record: the comment this replaces cited "Scribe task 2136" as though it were a standing rule. It is not one — none of the 46 always-on rules mentions signing keys. 2136 is a desktop-updater task whose REASONING got repeated until it sounded like policy. The reasoning holds, and holds harder on Android where a key cannot be rotated without the original, so the practice is unchanged; the citation is now honest about what it is.
ThoughtSync
Self-hosted personal thought-capture web app in the FabledSword family — a
Google-Keep-style masonry post-it board for capturing disparate thoughts in
under a second, designed to grow into a lightweight second brain (labels, search,
[[wiki-links]], graph).
- Stack: Quart (async Python) + Vue 3 + PostgreSQL, Docker, Fabled-Git CI.
- Auth: native email + password (signed-cookie sessions). Multi-user with the family owner + direct-share + group-share ACL.
- Web-first. An Android companion is a later, conditional milestone.
Layout
src/thoughtsync/ Quart app (app factory, auth, models, ACL, config, db)
alembic/ async migrations (schema built via `alembic upgrade head`)
tests/ DB-free unit tests (pytest)
frontend/ Vue 3 + Vite + TypeScript + Tailwind SPA
Dockerfile 2-stage build (Vue -> python:3.12-slim runtime)
docker-compose.yml local app + Postgres stack
Development
Backend (needs a Postgres reachable at THOUGHTSYNC_DATABASE_URL):
pip install -e ".[dev]"
alembic upgrade head
hypercorn 'thoughtsync.app:create_app()' --bind 0.0.0.0:5000
Frontend (proxies /api to :5000):
cd frontend
npm install
npm run dev # http://localhost:5173
Or run it in Docker. Hot-reload dev stack (edit code, changes reload live) — Postgres + backend + Vite dev server, app at http://localhost:5173:
docker compose -f docker-compose.dev.yml up
Or the full production image (SPA baked in) at http://localhost:5000:
docker compose up --build
Deploy (self-host)
A complete two-container stack (app + Postgres) you can copy and run. Save it as
docker-compose.yml, change the two CHANGE_ME passwords (they must match), then
docker compose up -d:
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: thoughtsync
POSTGRES_PASSWORD: CHANGE_ME # change this
POSTGRES_DB: thoughtsync
volumes:
- thoughtsync-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U thoughtsync"]
interval: 5s
timeout: 5s
retries: 10
app:
image: git.fabledsword.com/bvandeusen/thoughtsync:latest # :dev for the current dev build
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://thoughtsync:CHANGE_ME@db:5432/thoughtsync
volumes:
- thoughtsync-data:/var/thoughtsync # uploaded images; omit if you don't use attachments
ports:
- "5000:5000"
volumes:
thoughtsync-db:
thoughtsync-data:
Then open http://<host>:5000 and register — the first account becomes the admin.
- Only
THOUGHTSYNC_DATABASE_URLis required.THOUGHTSYNC_SECRET_KEYis optional; if unset, a signing key is generated and persisted in the database (sessions survive restarts). - Uploaded images live under the
thoughtsync-datavolume at/var/thoughtsync. - The app waits for the database and runs migrations (
alembic upgrade head) automatically on start. - Image tags:
:latest(stable, built frommain) ·:dev(latestdevbuild) ·:<git-sha>(immutable, for pinning / rollback). - Install as an app (PWA): ThoughtSync is installable ("Add to Home Screen" / the
browser's install button) for an app-like window. Browsers only offer install over a
secure context, so put the app behind a reverse proxy terminating HTTPS (or reach
it via
localhost) — plainhttp://<host>:5000won't show the install prompt.
Milestones
- M0 — Foundation & Identity ✅: Quart+Vue+Postgres skeleton, native auth, sharing-ACL spine, Fabled-Git CI.
- M1 — Capture Core ✅: note model + masonry board (quick-add, colors, pin, edit-in-place, archive, trash+restore).
- M1.5 — Roles & DB-backed Settings ✅: first user is admin, admin Settings UI,
DATABASE_URLis the only required env. - M2 — Organize: labels/tags, search, checklists, attachments.
- M3 — Second-brain seeds:
[[wiki-links]], backlinks, graph view, reminders. - M4 — Launch hardening: responsive/PWA, settings UI, polish.
Work happens on dev; main is protected (PR-only).