bvandeusen 668f7faf03
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 2m30s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m47s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 6m45s
core: the body is the checklist, and checklist_items is gone
M304 steps 2 and the client half of 4, together — they cannot be separated. A
commit where the store writes items into the body while push.rs still reads them
from a table is one that silently pushes the wrong list, and dev publishes to the
dev channel on every green build.

Store:
  * load_items becomes items_of(body) — a parse, not a query. An item's id is its
    ORDINAL, which is all it ever amounted to: push.rs sent text and checked and
    never an id, and both sides replaced the whole list on every sync.
  * add_item / update_item / delete_item route through update_note, so they get
    revision snapshotting, #tag re-derivation and the dirty/updated_at bookkeeping
    without any of it being written a second time.
  * create_note folds its items: input into the body, and syncs tags from the
    FOLDED body — an item can carry a #tag too.
  * display_title no longer takes items, because items ARE body lines now. It
    strips the task marker instead: a list-only note is still named by its first
    item, and calling that note "- [ ] milk" would show someone the storage.

Wire: items leave it. A second copy of data already in the body field of the same
message is how the two come to disagree. CLIENT_PROTOCOL_VERSION and
MIN_SERVER_PROTOCOL_VERSION go to 3, which is what makes this safe to land before
the server: a v3 client refuses a v2 server outright rather than pushing a body
whose list the old _apply_note_items would then delete.

Schema v8 folds every existing row into its note's body before dropping the
table. Written in Rust, not SQL: the fold has to produce exactly what
derive::append_item produces, and group_concat only gained a guaranteed ORDER BY
in SQLite 3.44 — a checklist that quietly reordered itself during a migration
would be a poor way to learn that. updated_at and dirty are deliberately left
alone, because the server's migration folds the same rows the same way and both
sides land on identical bodies; marking every note dirty would push a body the
server already has, from every device at once.

NOT deployable yet. The server still speaks v2 and still has note_items, so a
client built from this will refuse to sync until the server half lands.
2026-08-24 00:41:23 -04:00
2026-08-23 21:47:01 -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).
  • Putting it on the public internet: there are four things to do first — close registration, terminate TLS and forward X-Forwarded-Proto, stop publishing the app port, and back up the attachment volume as well as the database. See docs/public-hosting.md, which also lists what the app hardens on its own and what it deliberately doesn't.
  • 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%