AGPL-3.0-only, a source link in the app, and everything on dev since #5 #6
+2
-2
@@ -209,5 +209,5 @@ android/local.properties
|
||||
# The Android client CI bakes into the server image. Fetched fresh on every image
|
||||
# build, so it is never worth 55 MiB of git history. client/.keep IS tracked, so
|
||||
# the Dockerfile's COPY always has a directory to copy.
|
||||
client/thoughtsync.apk
|
||||
client/thoughtsync-android.json
|
||||
client/inkwell.apk
|
||||
client/inkwell-android.json
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ThoughtSync
|
||||
# Inkwell
|
||||
|
||||
Self-hosted personal thought-capture web app in the **FabledSword** family — a
|
||||
Google-Keep-style **masonry post-it board** for capturing disparate thoughts in
|
||||
@@ -23,7 +23,7 @@ docker-compose.yml local app + Postgres stack
|
||||
|
||||
## Development
|
||||
|
||||
Backend (needs a Postgres reachable at `THOUGHTSYNC_DATABASE_URL`):
|
||||
Backend (needs a Postgres reachable at `INKWELL_DATABASE_URL`):
|
||||
|
||||
```sh
|
||||
pip install -e ".[dev]"
|
||||
@@ -64,40 +64,40 @@ services:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: thoughtsync
|
||||
POSTGRES_USER: inkwell
|
||||
POSTGRES_PASSWORD: CHANGE_ME # change this
|
||||
POSTGRES_DB: thoughtsync
|
||||
POSTGRES_DB: inkwell
|
||||
volumes:
|
||||
- thoughtsync-db:/var/lib/postgresql/data
|
||||
- inkwell-db:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U thoughtsync"]
|
||||
test: ["CMD-SHELL", "pg_isready -U inkwell"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
app:
|
||||
image: git.fabledsword.com/bvandeusen/thoughtsync:latest # :dev for the current dev build
|
||||
image: git.fabledsword.com/bvandeusen/inkwell: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
|
||||
INKWELL_DATABASE_URL: postgresql+asyncpg://inkwell:CHANGE_ME@db:5432/inkwell
|
||||
volumes:
|
||||
- thoughtsync-data:/var/thoughtsync # uploaded images; omit if you don't use attachments
|
||||
- inkwell-data:/var/inkwell # uploaded images; omit if you don't use attachments
|
||||
ports:
|
||||
- "5000:5000"
|
||||
|
||||
volumes:
|
||||
thoughtsync-db:
|
||||
thoughtsync-data:
|
||||
inkwell-db:
|
||||
inkwell-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
|
||||
- **Only `INKWELL_DATABASE_URL` is required.** `INKWELL_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`.
|
||||
- Uploaded images live under the `inkwell-data` volume at `/var/inkwell`.
|
||||
- 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>` on `main` only (immutable, the rollback unit). There are
|
||||
@@ -108,7 +108,7 @@ Then open `http://<host>:5000` and register — **the first account becomes the
|
||||
port, and back up the attachment volume as well as the database. See
|
||||
[docs/public-hosting.md](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
|
||||
- **Install as an app (PWA):** Inkwell 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.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Alembic single-database async configuration for ThoughtSync.
|
||||
# Alembic single-database async configuration for Inkwell.
|
||||
|
||||
[alembic]
|
||||
script_location = %(here)s/alembic
|
||||
|
||||
+8
-7
@@ -1,4 +1,4 @@
|
||||
# CI Requirements — ThoughtSync
|
||||
# CI Requirements — Inkwell
|
||||
|
||||
> Spec lives in [`docs/process.md`](https://git.fabledsword.com/bvandeusen/CI-runner/src/branch/main/docs/process.md)
|
||||
> in the CI-Runner repo.
|
||||
@@ -39,7 +39,7 @@ entirely on `ci-python:3.14`.
|
||||
install` cold cost is a non-blocker.
|
||||
- Build gates on `typecheck` + `lint` only. The `test` job runs in parallel for
|
||||
visibility but does not block the dev image push. DB-backed / integration tests
|
||||
run against the dev image manually — ThoughtSync's unit tests are DB-free (no
|
||||
run against the dev image manually — Inkwell's unit tests are DB-free (no
|
||||
Postgres service lane in CI yet).
|
||||
- `dev` push -> `:dev` + `:<sha>`; `v*` tag -> `:latest` + `:<version>` + `:<sha>`
|
||||
(family rule 46).
|
||||
@@ -87,7 +87,7 @@ parts. Three of them are family rules for a reason:
|
||||
`docker ps` by it. A spaced or underscored name breaks the filter.
|
||||
- **Service hostnames are not routable** on this runner (rule 79), so the step resolves
|
||||
the Postgres container's bridge IP with `docker ps --filter` + `docker inspect` and
|
||||
builds `THOUGHTSYNC_DATABASE_URL` from it. `postgres:5432` will not connect.
|
||||
builds `INKWELL_DATABASE_URL` from it. `postgres:5432` will not connect.
|
||||
- **`run:` is busybox sh** (rule 81) — no `/dev/tcp` — so the readiness wait is a small
|
||||
Python heredoc. Its terminator must dedent to column 0 after YAML strips the block
|
||||
indent; check with `yaml.safe_load` and print the `run` string if you edit it.
|
||||
@@ -205,7 +205,7 @@ backend/frontend push.
|
||||
## Android lane — being rebuilt (M12)
|
||||
|
||||
The Tauri-mobile Android lane is gone. Android is a native Kotlin/Compose client
|
||||
over the shared `thoughtsync-core` crate instead — see Scribe note 2730 for the
|
||||
over the shared `inkwell-core` crate instead — see Scribe note 2730 for the
|
||||
decision and milestone M12 for the arc.
|
||||
|
||||
The image it will run on already exists: **`ci-rust-android:1.97`**, repurposed
|
||||
@@ -218,7 +218,7 @@ second image, and JDK 25 (which requires **Gradle 9.1+** in this repo's wrapper
|
||||
the old JDK 17 pin existed only because Tauri generated a Gradle 8.x project).
|
||||
|
||||
The Rust pin is in LOCKSTEP with `ci-tauri` and `ci-tauri-win`. All three build
|
||||
`thoughtsync-core` from one workspace `Cargo.lock` under `--locked`, so a
|
||||
`inkwell-core` from one workspace `Cargo.lock` under `--locked`, so a
|
||||
mismatched Rust minor across the lanes would mean divergent resolution for no
|
||||
reason. Bump the three together or not at all.
|
||||
|
||||
@@ -333,8 +333,9 @@ differs from CI is worse than none.
|
||||
|
||||
**This reproduces CI exactly, not approximately.** On the 2026-08-18 run the
|
||||
local test binary hashes (`thoughtsync_core-bbaae79723888ad1`,
|
||||
`thoughtsync_desktop_lib-9d162263f8d0aca3`, `thoughtsync_ffi-fc557b96dc795e27`)
|
||||
matched CI run 3931's byte for byte. Same image, same lockfile, same units.
|
||||
`thoughtsync_desktop_lib-9d162263f8d0aca3`, `thoughtsync_ffi-fc557b96dc795e27`,
|
||||
named for the crates as they were before the rename to Inkwell) matched CI run
|
||||
3931's byte for byte. Same image, same lockfile, same units.
|
||||
|
||||
`target/` persists on the host between runs, so after the first cold build these
|
||||
take seconds (~30s for clippy). It is gitignored and reaches ~1.4 GB; delete it
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
# ThoughtSync desktop (Tauri v2)
|
||||
# Inkwell desktop (Tauri v2)
|
||||
|
||||
Local-first desktop client. The window loads the shared **Vue 3 frontend** from
|
||||
`../frontend`; the Rust core (`src-tauri`) owns the on-device store and the opt-in
|
||||
sync engine (built out across the M10 milestone). Works fully offline; optionally
|
||||
syncs to a self-hosted ThoughtSync server.
|
||||
syncs to a self-hosted Inkwell server.
|
||||
|
||||
## Layout
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ThoughtSync desktop — Arch package
|
||||
# Inkwell desktop — Arch package
|
||||
|
||||
A **prebuilt** native pacman package, published as an asset on every ThoughtSync
|
||||
A **prebuilt** native pacman package, published as an asset on every Inkwell
|
||||
release. Nothing to compile, no toolchain to install.
|
||||
|
||||
Installing natively on Arch matters for more than tidiness: pacman pulls
|
||||
@@ -15,22 +15,22 @@ Easiest — the one-command installer picks this package automatically on any
|
||||
pacman system:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.fabledsword.com/bvandeusen/thoughtsync/raw/branch/dev/desktop/packaging/install.sh | sh
|
||||
curl -fsSL https://git.fabledsword.com/bvandeusen/inkwell/raw/branch/dev/desktop/packaging/install.sh | sh
|
||||
```
|
||||
|
||||
That installs the newest build from `main`. To follow the rolling development
|
||||
channel instead, pass the flag through the pipe:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.fabledsword.com/bvandeusen/thoughtsync/raw/branch/dev/desktop/packaging/install.sh | sh -s -- --channel dev
|
||||
curl -fsSL https://git.fabledsword.com/bvandeusen/inkwell/raw/branch/dev/desktop/packaging/install.sh | sh -s -- --channel dev
|
||||
```
|
||||
|
||||
Or grab the `.pkg.tar.*` from the
|
||||
[releases page](https://git.fabledsword.com/bvandeusen/thoughtsync/releases)
|
||||
[releases page](https://git.fabledsword.com/bvandeusen/inkwell/releases)
|
||||
and install it directly:
|
||||
|
||||
```sh
|
||||
sudo pacman -U thoughtsync-*-x86_64.pkg.tar.*
|
||||
sudo pacman -U inkwell-*-x86_64.pkg.tar.*
|
||||
```
|
||||
|
||||
The compression suffix depends on what the build image provides — `.zst` when
|
||||
@@ -39,17 +39,18 @@ all of them; only the filename differs.
|
||||
|
||||
Either way you get:
|
||||
|
||||
- `/usr/bin/thoughtsync` — the app
|
||||
- `/usr/share/applications/thoughtsync.desktop` — the menu entry
|
||||
- `/usr/share/icons/hicolor/*/apps/thoughtsync.png` — themed icons
|
||||
- `/usr/bin/inkwell` — the app
|
||||
- `/usr/share/applications/inkwell.desktop` — the menu entry
|
||||
- `/usr/share/icons/hicolor/*/apps/inkwell.png` — themed icons
|
||||
|
||||
Launch **ThoughtSync** from your app menu, or run `thoughtsync`.
|
||||
Launch **Inkwell** from your app menu, or run `inkwell`.
|
||||
|
||||
Uninstall: `sudo pacman -R thoughtsync`.
|
||||
Uninstall: `sudo pacman -R inkwell`.
|
||||
|
||||
The package was called `thoughtsync-desktop` before; it declares `replaces`/
|
||||
`conflicts` on that name, so an upgrade from it is a normal `pacman -U` and
|
||||
leaves nothing behind.
|
||||
The package was called `thoughtsync` until the app was renamed Inkwell, and
|
||||
`thoughtsync-desktop` before that. It declares `replaces`/`conflicts` on both
|
||||
names, so an upgrade from either is a normal `pacman -U` and leaves nothing
|
||||
behind.
|
||||
|
||||
## How the package is built
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ fi
|
||||
echo "==> Creating release for $TAG"
|
||||
BODY=$(cat <<JSON
|
||||
{"tag_name":"$TAG","name":"Inkwell $CHANNEL_LABEL","draft":false,"prerelease":$RELEASE_PRERELEASE,
|
||||
"body":"Inkwell $CHANNEL_LABEL.\n\n**Desktop**\n\n- **Debian / Ubuntu** — native \`.deb\`\n- **Arch / CachyOS** — native \`.pkg.tar.*\`\n- **everything else** — \`.AppImage\` (de-bundled graphics: renders on any GPU/Wayland setup)\n\nInstall / update — picks the right one for your system:\n\`\`\`\ncurl -fsSL $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/raw/branch/dev/desktop/packaging/install.sh | $INSTALL_TAIL\n\`\`\`\n\n**Android** — \`thoughtsync.apk\`. Copy it and \`thoughtsync-android.json\` into your server's \`/var/inkwell/client/\` and the server will offer it to your devices; see docs/android-distribution.md.$CHANNEL_NOTE"}
|
||||
"body":"Inkwell $CHANNEL_LABEL.\n\n**Desktop**\n\n- **Debian / Ubuntu** — native \`.deb\`\n- **Arch / CachyOS** — native \`.pkg.tar.*\`\n- **everything else** — \`.AppImage\` (de-bundled graphics: renders on any GPU/Wayland setup)\n\nInstall / update — picks the right one for your system:\n\`\`\`\ncurl -fsSL $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/raw/branch/dev/desktop/packaging/install.sh | $INSTALL_TAIL\n\`\`\`\n\n**Android** — \`inkwell.apk\`. Copy it and \`inkwell-android.json\` into your server's \`/var/inkwell/client/\` and the server will offer it to your devices; see docs/android-distribution.md.$CHANNEL_NOTE"}
|
||||
JSON
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Getting the Android app onto your server
|
||||
|
||||
ThoughtSync's server hands out the Android client it syncs with. Once an APK is in
|
||||
Inkwell'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.
|
||||
|
||||
@@ -34,7 +34,7 @@ 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.
|
||||
`/var/inkwell/client/` and it wins over the image's copy.
|
||||
|
||||
That directory is shared with the desktop clients the server hands out, and
|
||||
**precedence is decided per platform**: dropping in an APK overrides the baked APK
|
||||
@@ -44,14 +44,14 @@ Two files, both required:
|
||||
|
||||
| File | What it is |
|
||||
| --- | --- |
|
||||
| `thoughtsync.apk` | the client |
|
||||
| `thoughtsync-android.json` | `{version_name, version_code, size, sha256}` |
|
||||
| `inkwell.apk` | the client |
|
||||
| `inkwell-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`),
|
||||
`/var/inkwell` 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.
|
||||
|
||||
@@ -59,10 +59,10 @@ Both files are published to the rolling dev-channel release (tag `dev-rolling`)
|
||||
on every green Android build:
|
||||
|
||||
```sh
|
||||
REPO=https://git.fabledsword.com/bvandeusen/thoughtsync
|
||||
REPO=https://git.fabledsword.com/bvandeusen/inkwell
|
||||
TOKEN=... # a Fabled-Git token with read access; the instance is private
|
||||
|
||||
for f in thoughtsync.apk thoughtsync-android.json; do
|
||||
for f in inkwell.apk inkwell-android.json; do
|
||||
curl -fsSL -H "Authorization: token $TOKEN" \
|
||||
-o "/tmp/$f" "$REPO/releases/download/dev-rolling/$f"
|
||||
done
|
||||
@@ -70,8 +70,8 @@ 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/
|
||||
docker compose cp /tmp/inkwell.apk app:/var/inkwell/client/
|
||||
docker compose cp /tmp/inkwell-android.json app:/var/inkwell/client/
|
||||
```
|
||||
|
||||
To go back to whatever the image ships, delete both files.
|
||||
@@ -88,7 +88,7 @@ offering a button that fails.
|
||||
|
||||
## What happens if you get it wrong
|
||||
|
||||
All of these describe an override in `/var/thoughtsync/client/`. A broken
|
||||
All of these describe an override in `/var/inkwell/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.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Putting ThoughtSync on the public internet
|
||||
# Putting Inkwell on the public internet
|
||||
|
||||
ThoughtSync is built to run on a LAN and works fine there with no ceremony. Exposing
|
||||
Inkwell is built to run on a LAN and works fine there with no ceremony. Exposing
|
||||
it changes the threat model: anyone can now reach the login form, and any account is
|
||||
one guessed password away from someone's whole note history.
|
||||
|
||||
@@ -65,7 +65,7 @@ Which fix depends on where your proxy runs:
|
||||
the `ports:` block from `docker-compose.yml`. The proxy reaches the app over the
|
||||
compose network; no published port is needed at all, and this is the safest of the
|
||||
two because there is no host port to reach even from the host.
|
||||
- **Proxy on the host**: set `THOUGHTSYNC_BIND=127.0.0.1` in `.env`, so the port
|
||||
- **Proxy on the host**: set `INKWELL_BIND=127.0.0.1` in `.env`, so the port
|
||||
exists but only the host itself can use it.
|
||||
|
||||
To check which you have: `docker compose ps` shows the published ports, and
|
||||
@@ -73,12 +73,12 @@ To check which you have: `docker compose ps` shows the published ports, and
|
||||
app is still answering around the proxy. It should not be.
|
||||
|
||||
**5. Have a backup that includes the files.** Attachments are files on the
|
||||
`thoughtsync-data` volume, not rows — a `pg_dump` restores notes whose images are all
|
||||
`inkwell-data` volume, not rows — a `pg_dump` restores notes whose images are all
|
||||
gone. Back up both:
|
||||
|
||||
```
|
||||
docker compose exec -T db pg_dump -U thoughtsync thoughtsync > notes.sql
|
||||
docker run --rm -v thoughtsync-data:/d -v "$PWD":/out alpine tar czf /out/media.tgz -C /d .
|
||||
docker compose exec -T db pg_dump -U inkwell inkwell > notes.sql
|
||||
docker run --rm -v inkwell-data:/d -v "$PWD":/out alpine tar czf /out/media.tgz -C /d .
|
||||
```
|
||||
|
||||
## What the app already does
|
||||
@@ -132,7 +132,7 @@ know. They are the reason not to hand out open registration to strangers.
|
||||
|
||||
The app allows plain HTTP so a self-hosted server on a LAN is usable at all — Android
|
||||
blocks cleartext by default from API 28, and `http://192.168.1.10:8000` is exactly the
|
||||
case ThoughtSync is built for. Over the public internet, link the phone to the
|
||||
case Inkwell is built for. Over the public internet, link the phone to the
|
||||
**HTTPS** hostname. The sync screen shows a warning before any credential field
|
||||
whenever the address it probed was `http://`; on a public network that warning means
|
||||
what it says.
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
# ThoughtSync sync protocol
|
||||
# Inkwell sync protocol
|
||||
|
||||
The contract the local-first native clients (Tauri desktop, Android) implement
|
||||
against. The server is the **sync hub**: each client keeps a full local store
|
||||
@@ -38,8 +38,8 @@ token, or even has an account:
|
||||
```
|
||||
|
||||
The client identifies itself on every request with
|
||||
`X-ThoughtSync-Client: thoughtsync-desktop/<app version>` and
|
||||
`X-ThoughtSync-Protocol: <n>`.
|
||||
`X-Inkwell-Client: inkwell-desktop/<app version>` and
|
||||
`X-Inkwell-Protocol: <n>`.
|
||||
|
||||
### `sync_features` — why versions alone aren't enough
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "thoughtsync-frontend",
|
||||
"name": "inkwell-frontend",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "thoughtsync-frontend",
|
||||
"name": "inkwell-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"pinia": "^2.2.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "thoughtsync-frontend",
|
||||
"name": "inkwell-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// and avoid deploy staleness, this SW does NOT cache the app shell, hashed build
|
||||
// assets, or any /api response — everything but the offline fallback goes
|
||||
// straight to the network.
|
||||
const CACHE = "thoughtsync-shell-v1";
|
||||
const CACHE = "inkwell-shell-v1";
|
||||
const OFFLINE_URL = "/offline.html";
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
|
||||
Reference in New Issue
Block a user