bvandeusenandClaude Opus 5 bbb2fd9b1c
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 27s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 1m55s
M10.7a: link/unlink a server — device auth + sync_state (task 2104)
The pairing step. Nothing else in the sync arc can move until this works.

sync/state.rs owns the link record in the sync_state row M10.4 already put
in the local schema. Two safety properties are the reason it isn't just
three setters:

- Linking a DIFFERENT server resets the change-feed cursor. A cursor is only
  meaningful against the server that issued it; carrying one across would
  silently skip every change on the new server below that watermark — data
  loss wearing the costume of a successful sync. Re-linking the SAME server
  (a token refresh) keeps it, so a routine re-auth doesn't force a full
  re-download.
- Unlink clears the cursor too, so a later link can't inherit a watermark
  from a server that never issued it.

An unparseable or absent cursor reads as 0 (full sync). That direction is
always safe: a redundant re-sync costs time, a too-high cursor costs notes.
Likewise a half-written row (server but no token) reports NOT linked.

state::Status deliberately has no device_token field — it crosses into the
webview, and a long-lived bearer token has no business reachable from page
scripts. A test asserts the token never appears in its serialization.

Token lives in the app-data SQLite file, not an OS keyring: the keyring
crate needs libsecret/DBus on Linux, which adds a C dependency to a binary
that has to cross-compile and fails outright on headless/minimal-WM setups —
the same class of environment assumption behind the black-window bug.

sync_link runs the M10.6 handshake FIRST and refuses an incompatible server
before any credential is sent. Two credential paths, because neither covers
everyone: device-login (a fresh install has no session to mint a token from)
and a pasted token (some users would rather not type a password into a
desktop app). A pasted token is verified against /api/auth/me before being
stored — auth.py's login_required accepts bearer — since an unverified paste
would turn a copy/paste slip into a failure surfacing at the next sync, far
from its cause.

The store lock is taken only after all network work: a std MutexGuard isn't
Send so it cannot cross an await, and holding the store for a round-trip
would freeze every note operation in the UI.

Unlink is LOCAL only — the token stays valid server-side until revoked under
Account -> Linked devices. A pasted token arrives without its device id, so
a reliable remote revoke isn't possible from here; the UI must say so rather
than imply a revoke that didn't happen. Follow-up filed.

No UI yet — that's M10.7e.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
2026-07-25 23:35:51 -04:00
2026-07-19 12:34:56 -04:00

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_URL is required. THOUGHTSYNC_SECRET_KEY is optional; if unset, a signing key is generated and persisted in the database (sessions survive restarts).
  • Uploaded images live under the thoughtsync-data volume at /var/thoughtsync.
  • The app waits for the database and runs migrations (alembic upgrade head) automatically on start.
  • Image tags: :latest (stable, built from main) · :dev (latest dev build) · :<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) — plain http://<host>:5000 won'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_URL is 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).

S
Description
Self-hosted personal thought-capture web app (FabledSword family) — a Google-Keep-style masonry post-it board for fast idea capture, growing into a lightweight second brain. Quart + Vue 3 + PostgreSQL.
Readme
3.2 MiB
2026-08-23 16:50:53 -04:00
Languages
Python 26.3%
Rust 23.9%
Kotlin 22.7%
Vue 13.3%
TypeScript 6.6%
Other 7.2%