Server (schema v2, auto-migrates): every accepted overwrite snapshots the
superseded revision into note_history (capped at 50 per note); new
GET /api/v1/notes/{id}/history endpoint; compact purges orphaned history.
Client: notes get a synced_at stamp on every confirmed server exchange;
an info footer under the editor and a Ctrl/Cmd+I panel show created/
modified/last-synced plus the revision list. Restoring a revision applies
it as a normal edit through the sync path, so rollback is non-destructive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
135 lines
5.0 KiB
Markdown
135 lines
5.0 KiB
Markdown
# Tefter
|
||
|
||
Self-hosted, single-user notes with the classic **Notational Velocity** interaction model:
|
||
one omnibar that searches *and* creates, instant filtering on every keystroke, no save
|
||
buttons, no dialogs, everything on the keyboard. Markdown editing with preview,
|
||
offline-first sync across Linux/macOS/Windows/Android/web.
|
||
|
||
One Go binary (`tefterd`) + one SQLite file. The web app (PWA) is embedded in the binary;
|
||
the desktop app is the same frontend in a Wails shell.
|
||
|
||
## Quick start (server)
|
||
|
||
```sh
|
||
make server # builds frontend + tefterd into build/tefterd
|
||
./build/tefterd init # creates tefter.db, prints your auth token — save it
|
||
./build/tefterd # serves on :8420
|
||
```
|
||
|
||
Open `http://localhost:8420`, click `⋯` → set server URL + token → Save.
|
||
|
||
### Keyboard model
|
||
|
||
| Key | Action |
|
||
|---|---|
|
||
| type in omnibar | filter notes instantly (title > prefix > contains > body; diacritic-insensitive) |
|
||
| `Enter` | open selected note / create note titled with the query |
|
||
| `↑` `↓` | move list selection (editor live-previews) |
|
||
| `Esc` | editor → omnibar; omnibar → clear |
|
||
| `Ctrl/Cmd+L` | focus omnibar, select text |
|
||
| `Ctrl/Cmd+Delete` | delete note (undo toast, no confirm) |
|
||
| `Ctrl/Cmd+Shift+P` | toggle markdown preview (`[[wiki-links]]` open/create notes) |
|
||
| `Ctrl/Cmd+K` | cycle tag filter |
|
||
| `Ctrl/Cmd+J` / `+Shift` | next / previous note while editing |
|
||
| `Ctrl/Cmd+I` | note info (created / last synced) & version history |
|
||
|
||
## CLI
|
||
|
||
```sh
|
||
tefterd # serve (default), flags: --listen :8420 --db tefter.db
|
||
tefterd init # create db + print token
|
||
tefterd token rotate # new token (old one stops working)
|
||
tefterd import simplenote export.zip # idempotent Simplenote import (also in the web UI)
|
||
tefterd compact --days 90 # purge old tombstones
|
||
tefterd backup /backups/tefter.db # consistent snapshot (VACUUM INTO)
|
||
tefterd version
|
||
```
|
||
|
||
Environment: `TEFTER_DB`, `TEFTER_LISTEN` override the flag defaults. No config file.
|
||
|
||
## Deployment
|
||
|
||
`tefterd` listens on plain HTTP; put TLS in front of it.
|
||
|
||
**systemd** — `/etc/systemd/system/tefterd.service`:
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=Tefter notes server
|
||
After=network.target
|
||
|
||
[Service]
|
||
User=tefter
|
||
ExecStart=/usr/local/bin/tefterd --db /var/lib/tefter/tefter.db --listen 127.0.0.1:8420
|
||
Restart=on-failure
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
**Caddy** (two lines):
|
||
|
||
```
|
||
notes.example.com {
|
||
reverse_proxy 127.0.0.1:8420
|
||
}
|
||
```
|
||
|
||
**Backup** = one file:
|
||
|
||
```sh
|
||
tefterd backup /backups/tefter-$(date +%F).db --db /var/lib/tefter/tefter.db
|
||
```
|
||
|
||
## Clients
|
||
|
||
- **Web / Android:** open the server URL, install as PWA (Chrome: “Add to home screen”).
|
||
Fully offline-capable; edits sync on reconnect.
|
||
- **Desktop:** `tefter-desktop` (Wails). Single instance, closes to tray,
|
||
global shortcut `Ctrl+Shift+Space` (change with `--hotkey` / `TEFTER_HOTKEY`;
|
||
`--quit-on-close` disables the tray behavior). Works with no server at all —
|
||
configure sync later under `⋯`.
|
||
|
||
## Sync model
|
||
|
||
Pull-then-push, last-write-wins with **conflict copies** (Simplenote-style). Concurrent
|
||
edits of the same note never silently overwrite: the loser comes back as a new note
|
||
tagged `conflict` with “(conflicted copy …)” in its title. Delete-vs-edit: the edit wins.
|
||
Every client keeps a full offline copy in IndexedDB; a crash mid-sync loses nothing.
|
||
|
||
**Version history:** every time an accepted sync overwrites a note, the server keeps the
|
||
previous state (last 50 revisions per note). `Ctrl/Cmd+I` (or the “History” button under
|
||
the editor) shows a note’s revisions; restoring one applies the old text as a new edit,
|
||
so the replaced text itself stays in history — rollback is never destructive.
|
||
`tefterd compact` drops history along with the purged notes.
|
||
|
||
## Development
|
||
|
||
```sh
|
||
make dev # vite dev server (proxies /api to :8420)
|
||
make test # go vet + go test (uses notes.zip for a real-import test when present)
|
||
make server # build/tefterd for this machine
|
||
make server-all # linux/amd64, linux/arm64, darwin/arm64, windows/amd64
|
||
make desktop # Wails build (needs wails CLI + GTK/WebKit dev packages on Linux)
|
||
```
|
||
|
||
## CI / Releases (Gitea Actions)
|
||
|
||
`.gitea/workflows/ci.yml` runs tests on every push and, on a `v*` tag, builds
|
||
`tefterd` for all four platforms plus the Linux desktop app and attaches them
|
||
to a Gitea release. Requirements: Actions enabled on the repo and one Linux
|
||
runner whose `ubuntu-latest` label maps to a Debian/Ubuntu-based image (the
|
||
default `catthehacker/ubuntu:act-latest` works).
|
||
|
||
```sh
|
||
git tag v0.1.0 && git push origin v0.1.0 # cut a release
|
||
```
|
||
|
||
macOS/Windows desktop shells need a native machine: install Go + Node + the
|
||
Wails CLI there and run `make desktop` (the result lands in
|
||
`desktop/build/bin/`). The server binaries for those platforms come out of the
|
||
Linux runner via cross-compilation.
|
||
|
||
Repo layout: `frontend/` (Svelte + TS, all app logic incl. offline store + sync engine),
|
||
`server/` (Go, stdlib HTTP + modernc.org/sqlite, no CGO), `desktop/` (Wails shell).
|