bvandeusenandClaude Opus 5.5 ac4427f834
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 1s
CI & Build / Web typecheck and unit tests (push) Successful in 11s
CI & Build / Python tests (push) Successful in 12s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / integration (push) Successful in 45s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 11m6s
android: shows and attaches files, and the share sheet takes images
The phone downloaded every attachment and drew none of them, so a photo note
looked empty. Now:

- Cards show a note's first image and name its other files; the editor shows
  every image at full width and every file as a row. Tapping one opens it in
  whatever app handles its type (a cache copy under its real name, through a
  FileProvider that serves only those copies). Each can be removed, and a file
  the server refused says why under it.
- The editor's toolbar has an Attach button (any type, several at once). Files
  are stored on the phone straight away and upload on the next sync that
  reaches a server, through the core's step-6 path. Link previews show in the
  editor too, and can be dismissed.
- Share → Inkwell accepts one or several images, with or without a caption,
  finishing #1899's deferred image/* target.
- The FFI gains add_attachment, delete_attachment, delete_preview and
  blob_path. The sync summary counts uploads and failed uploads.

Images decode at the size they are drawn (BitmapFactory sampling plus EXIF
rotation, small LRU cache), so no image library is added. Files over 50 MB are
refused on the phone before they are read whole into memory.

Task #5169.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:59:36 -04:00

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 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/inkwell/         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 INKWELL_DATABASE_URL):

pip install -e ".[dev]"
alembic upgrade head
hypercorn 'inkwell.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: inkwell
      POSTGRES_PASSWORD: CHANGE_ME          # change this
      POSTGRES_DB: inkwell
    volumes:
      - inkwell-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U inkwell"]
      interval: 5s
      timeout: 5s
      retries: 10

  app:
    image: git.fabledsword.com/bvandeusen/inkwell:latest   # :dev for the current dev build
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      INKWELL_DATABASE_URL: postgresql+asyncpg://inkwell:CHANGE_ME@db:5432/inkwell
    volumes:
      - inkwell-data:/var/inkwell   # uploaded images; omit if you don't use attachments
    ports:
      - "5000:5000"

volumes:
  inkwell-db:
  inkwell-data:

Then open http://<host>:5000 and register — the first account becomes the admin.

  • 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 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 no version-shaped tags: nothing pins one, and the build reports its own version at /api/config and /health.
  • 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): 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.

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.9 MiB
2026-08-23 16:50:53 -04:00
Languages
Python 28%
Rust 27%
Kotlin 20.4%
Vue 12.7%
TypeScript 6.1%
Other 5.7%