A local-first, end-to-end-encrypted personal time asset manager. Your to-do list doesn't have to live on someone else's server.
Quick start · Architecture · Security · Docs · Report a bug
git clone https://gitcode.com/badhope/goto.git
cd goto/desktop && npm install && npm run dev
# → http://localhost:5173 · set a master password · start writing tasksOptional self-hosted relay (for cross-device sync):
cd goto/relay && docker compose up -dThat's it. No account, no cloud, no telemetry. Your data stays in your browser's IndexedDB, encrypted with Web Crypto under a master password only you know.
Most task managers pick one of two lanes: pure-local (great on one device, useless the moment you switch laptops) or cloud-synced (convenient, but every plan, habit and note you keep ends up on someone else's disk). Goto takes the third path — data lives on your devices by default, and when it syncs across devices it's end-to-end encrypted. The relay server only ever sees ciphertext frames.
I started it because I wanted a todo app I could actually use on the train (no network), on my laptop (full keyboard), without mailing my life to a SaaS. The interesting parts, in my opinion:
- Three components, one data model. A browser web app (the
desktop/directory — Vite + React), an optional Python backend, and a self-hostable relay. Every persisted object carriesupdatedAt, so the same record flows across local storage, the backend, and paired devices. - E2EE sync that's cross-platform. The web app uses the Web Crypto API; the relay is transport-only and never decrypts. Records are AES-256-GCM sealed under a Sync Master Key exchanged during pairing. The test suite includes sync-protocol and interop cases.
- Local-first by default, not as a marketing tag. No Goto account exists. The relay can't read your data. If you never pair a second device, nothing ever leaves the first one.
- Auto-tag suggestions (keyword-based). A built-in plugin matches keywords in task titles (shopping / work / health / study) and auto-applies tags. Statistical AI engine and LLM features described in earlier docs are not yet implemented — see roadmap.
flowchart LR
subgraph DeviceA["Device A — Web app"]
A_UI["React UI<br/>(8 pages + Mosaic)"]
A_IDB[("IndexedDB<br/>(local-first)")]
A_ENC["Web Crypto<br/>PBKDF2 600k + AES-256-GCM"]
A_UI <--> A_IDB
A_UI <--> A_ENC
end
subgraph DeviceB["Device B — Web app"]
B_UI["React UI<br/>(8 pages + Mosaic)"]
B_IDB[("IndexedDB<br/>(local-first)")]
B_ENC["Web Crypto<br/>PBKDF2 600k + AES-256-GCM"]
B_UI <--> B_IDB
B_UI <--> B_ENC
end
Relay["Relay<br/>(self-hosted)<br/>ciphertext-only<br/>7-day offline queue"]
Backend["Backend<br/>(optional)<br/>FastAPI · 22 endpoints"]
A_ENC -- "encrypted frames<br/>iv‖tag‖ct" --> Relay
Relay --> B_ENC
B_ENC -- "encrypted frames" --> Relay
Relay --> A_ENC
A_UI -. "REST (optional)" .-> Backend
B_UI -. "REST (optional)" .-> Backend
Three components, one data model — every record carries updatedAt, so the same object flows across local storage, the optional backend, and paired devices. The relay only ever sees ciphertext.
| Component | Stack | What it does |
|---|---|---|
Web app (desktop/) |
Vite · React 18 · Zustand 4 · TypeScript 5 | Local-first browser task manager. Persists to IndexedDB, encrypts the vault with Web Crypto (PBKDF2), and encrypts JSON backups with PBKDF2-SHA256 (600k iterations) + AES-256-GCM. 12 pages + Mosaic timeline view (Today / Calendar / Projects / Project detail / Categories / Tags / Search / Vault / Settings / Kanban / Insights / Weekly Review), reminders, recurrence, subtasks, NLP quick-add, bulk ops, drag-reorder, vim shortcuts, PWA installable, 6-section Settings (Security / Appearance / Shortcuts / Data / Sync / Danger zone). |
| Relay | Node.js 18+ (≥20) · WebSocket · express-rate-limit · Docker | Forwards ciphertext frames only, with an offline queue (7-day TTL). LAN-first, relay as fallback. |
| Backend | Python 3.11+ · FastAPI · PyGit2 | Optional: task/project/category/tag API, git management, plugin system. Decoupled from the sync path. 22 REST endpoints. |
The web app syncs across devices over an encrypted P2P session via the relay; the relay is just a porter that never decrypts.
The web app ships 12 pages — Today, Calendar, Projects, Project detail
(/projects/:id), Categories, Tags, Search, Vault, Settings, Kanban,
Insights (statistics dashboard with Karma score), Weekly Review —
plus a Mosaic timeline view. The Gantt / Timeline / TimeBlock / Table /
MindMap views and Templates / Automation / Notes / Analytics / Goals /
Habits pages described in earlier docs are not yet implemented — see
Goto Pivot Plan and
Product Evolution Plan for the roadmap.
Task dependencies (blockedBy / blocks) stop you marking a task done
when something it's waiting on is still open.
The Settings page is split into Security / Appearance / Shortcuts / Data / Sync / Danger zone:
- Security: unlock method (master password, biometric only when the platform exposes it), auto-lock duration (off / 1 / 5 / 15 / 30 / 60 min, upgraded from the old fixed 5-min toggle), screenshot/recording protection (effective inside the desktop shell, marked as best-effort on Web), change master password (validates the old password, derives a new verifier, clears the key cache; previously exported backups still need the old password), 3-strikes password cooldown (30 s lockout after three consecutive wrong passwords).
- Appearance: theme (light / dark / system), font size (small /
medium / large) — scales every rem-based element via a root
data-font-sizeattribute. - Shortcuts: a
?shortcut opens a help overlay listing every registered binding (?/Mod+L/Mod+B/Mod+K///Mod+N/Esc); the Settings page also has a "view all shortcuts" button. - Data: encrypted backup export/import (PBKDF2-SHA256 600k + AES-256-GCM) and plaintext JSON export/import (vault excluded from JSON for credential safety).
- Sync: relay URL, device identity, pairing (host / join), paired device list, revocation.
- Danger zone (red-bordered, two-step confirm): clear all data (tasks / vault / projects / categories / tags / search history / sync identity, but keeps the master password and existing backups) and factory reset (also deletes the master password and security settings, then reloads to first-run state; existing encrypted backups still recover with the old password).
This is where most of the time went. Two devices handshake before syncing, verify each other's identity, derive a pair of direction-isolated session keys, then every record flows under AES-256-GCM. The relay sees bytes, not content.
identity Ed25519 long-term keypair per device, private key in the
web app's secure storage (IndexedDB + Web Crypto)
deviceId = sha256(raw SPKI pubkey), first 16 hex
handshake X25519 ephemeral keys do ECDH; both sides Ed25519-sign the
transcript (MITM resistance)
derive HKDF-SHA256 → sendKey / receiveKey, info bound by direction
records Sync Master Key (SMK) does AES-256-GCM, wire = iv[12]‖tag[16]‖ct
conflict updatedAt (LWW) first, version vectors break same-ms ties
transport 9 message types, frame = mode[1]‖length[4 BE]‖payload
sequence number + sliding window for replay protection
Pairing is an 8-digit code, 5-minute TTL, one-shot. SMK is generated by the host and transferred to the joiner over the encrypted pairing session, then compared in constant time — if it doesn't match, pairing is rejected rather than silently continuing into a state where every later sync would fail to decrypt. Device revocation is a four-step orchestration: kill the runtime session, drop the device record, clear its outbox, and only reset the SMK when no paired devices remain.
The gory details (incl. places where the implementation diverged from the design spec, and why) live in docs/superpowers/specs/.
| Area | What you get |
|---|---|
| Local-first | Data in IndexedDB. No account, no cloud, no telemetry. |
| E2EE sync | AES-256-GCM under a Sync Master Key; relay only sees ciphertext. |
| Vault | Field-level AES-256-GCM for sensitive entries. Password generator included. |
| Backups | Encrypted JSON export: PBKDF2-SHA256 600k + AES-256-GCM. Plaintext export excludes vault. |
| Master password | PBKDF2 verifier only; password never persisted. Changeable in Settings. |
| Brute-force cooldown | 3 wrong passwords → 30 s lockout. |
| Auto-lock | Off / 1 / 5 (default) / 15 / 30 / 60 min, or lock-on-blur. |
| Privacy shell | Lock screen, privacy mode (hide vault), clipboard auto-clear (default 30 s, configurable). |
| Danger zone | Clear all data (keep password & backups) / Factory reset (wipe password, reload to first-run). |
| Appearance | Light / dark / system + font size (small / medium / large). |
| Shortcuts | ? opens a help overlay. Mod+L lock, Mod+B sidebar, Mod+K search, / focus new task, Mod+N new task, Esc close. Vim-style j/k/e/x/d/gg/G in task list. |
| Mosaic view | A "time tape" rendering of all tasks — timeline-as-mosaic. |
| Auto-tag | Keyword-based plugin (shopping / work / health / study). |
| Task deps | blockedBy / blocks prevent marking a task done while upstream is open. |
| Reminders | Per-task reminderDate triggers a browser Notification (PWA installable, SW wakes in background). |
| Recurrence | Full RecurrenceRule editor (daily / weekly / monthly / yearly + interval + daysOfWeek + end-by date/count). Completing a recurring task auto-generates the next instance. |
| Subtasks | Per-task subtask list — add / delete / check / rename in card or editor. |
| NLP quick add | Type "明天 3点 高! 30分钟 #工作 +项目" — parser fills dueDate / priority / estimatedTime / tags / project. |
| Bulk ops | Multi-select tasks → complete / delete / change priority / move to project. |
| Drag reorder | @dnd-kit-powered drag to reorder tasks (PointerSensor + KeyboardSensor for a11y). |
| Kanban view | 5 columns (todo / in-progress / waiting / delegated / completed) — drag across columns to change status. |
| Insights | Karma score (Todoist-style 7d×10 + 14d×5, capped at 1000), 14-day completion trend, breakdowns by priority / status / project. |
| Weekly review | Week-at-a-glance (completed / stalled / overdue / due this week / per-project progress) + reflection notes + archive-30d-old button. |
| PWA | Installable (manifest + SVG icons + shortcuts). Workbox caching: assets CacheFirst, app shell NetworkFirst, /api & /ws never cached. |
| Plugin system | Registry + built-in plugins. Backend extends with custom plugins. |
| Tests | 611 total: 494 unit + 108 e2e + 104 backend + 9 relay. |
cd desktop
npm install
npm run dev # Vite dev server, opens http://localhost:5173For a deployable static bundle:
npm run build # writes dist/renderer/ (static SPA) — serve that dirThe full user manual (master password, vault, sync, backup) is in docs/desktop-user-guide.md.
cd relay
docker compose up -d # uses relay/docker-compose.ymlTLS, Nginx reverse proxy and ACME notes are in docs/relay-deployment.md. The relay only needs to reach the public internet; it never sees plaintext.
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reloadOpenAPI spec is auto-generated and CI-validated — see backend/docs/openapi.json (22 endpoints).
desktop/ # pure-browser web app (Vite + React 18 + Zustand 4)
src/renderer/ # React UI (task pages, vault, sync settings, ...)
src/shared/
api/ # backend REST client (tasks/projects/categories/tags)
store/ # Zustand store, state lives in slices/
sync/ # E2EE sync stack (Web Crypto, talks to the relay)
utils/ # secure storage (IndexedDB + Web Crypto / PBKDF2)
src/renderer/lib/ # webAPI: local IndexedDB implementation
# (local-first data access, no native bridge)
relay/ # self-hostable WebSocket relay + Docker
backend/ # optional FastAPI service
docs/ # user guide, roadmap, security tracker, relay deployment
The web app keeps two data paths: shared/api/* talks to the optional
backend over REST, while lib/webAPI.ts persists locally to IndexedDB.
The E2EE sync stack in shared/sync/* pairs devices through the relay.
# web app (desktop/)
cd desktop && npm run typecheck && npm run lint && npm test
# backend
cd backend && python -m ruff check app && python -m mypy app && python -m pytest tests/ -q
# relay
cd relay && npx tsc --noEmit && npm testI aim for "0 errors" on all three. The test baseline is 611 tests
(web app unit 494 + 26 skipped | web app e2e 108 | backend 104 | relay 9),
all green on main. The 5000-record end-to-end sync benchmark lands at
649 ms (7699 rec/s) — that figure used to be 52 s before fixing a
REQUEST-chunking bug and batching the inserts into one transaction.
Two independent audits (TF-001019 and TF2-001017, 36 findings total)
are tracked in a single
SECURITY_TRACKER.md. Most are
fixed; the rest are tagged in the roadmap. Highlights of what's in
place: branch protection with required review, CodeQL, gitleaks with
push-protection, dependency review (rejects GPL-3.0/AGPL-3.0), OSSF
Scorecard, cosign-signed releases, property-based fuzzing with
Hypothesis. Vulnerability reporting is private — see
SECURITY.md.
The repo ships these GitHub Actions:
ci.yml— desktop web app lint/test/build + backend ruff/mypy/test/fuzzverify.yml— typecheck on every pushrelay-ci.yml— relay typecheck/build/testweb-deploy.yml— buildsdesktop/dist/rendererand publishes to GitHub Pagespages-intro.yml— publishes the static project intro indocs/to GitHub Pagesgitleaks.yml— secret scanningrelease.yml— cosign-keyless signed release + SHA256SUMS
The GitHub Pages site is only an introduction page unless web-deploy.yml
is triggered; the web app itself is a static SPA you can host anywhere.
| Doc | What it covers |
|---|---|
| ARCHITECTURE.md | Layering, state model, sync protocol stack |
| QUICK_START.md | Three paths to a running app |
| docs/desktop-user-guide.md | Web app manual: install, vault, sync, backup |
| docs/ROADMAP.md | What's done (Phase 1–8) and what's next |
| docs/DEVELOPER_GUIDE.md | Dev workflow, commands, sync strategy |
| docs/relay-deployment.md | Self-hosting the relay, TLS, Nginx |
| docs/fuzzing.md | Property-based fuzzing with Hypothesis |
| CHANGELOG.md | Per-phase change log |
| FAQ.md | Stuff people actually run into |
| SECURITY.md | Supported versions, private disclosure |
| SUPPORT.md | Where to get help, report issues, sponsor |
| PRIVACY.md | Privacy policy — local-first, E2EE, GDPR Article 9 |
| TERMS.md | Terms of service — UGC responsibility, E2EE safe harbor |
项目主要托管在 GitCode(国内可访问),所有 Issue 和 PR 请在 GitCode 提交。
| Host | Repo |
|---|---|
| GitCode | gitcode.com/badhope/goto |
- The web build is a single SPA bundle under
desktop/dist/renderer— fine for static hosting / GitHub Pages. Runnpm run buildindesktop/and checkdist/renderer/for the current size. - Voice input is web-only.
- The web app has a privacy mode (hides the vault) but no OS-level screenshot blocking — that requires a native shell, which this web build does not have.
- Cross-device sync requires a relay (self-hosted or official) for pairing. Once paired, LAN-direct is preferred when reachable; the relay is fallback.
- Can I recover a forgotten master password? No. The password is never persisted — only a PBKDF2 verifier is. Write it down somewhere safe, or use encrypted backups.
- What does the relay see? Ciphertext frames + your device ID. It cannot decrypt anything.
- What if I lose all my devices? Restore an encrypted backup on a new device (you still need the password). Without a backup or a paired device, the data is gone — that's the cost of E2EE.
- Can I sync without a relay? No — pairing requires a relay to ferry the handshake. Once paired, LAN-direct is preferred when both devices are on the same network.
- Why is "factory reset" in a red danger zone? It deletes your master password verifier and reloads to first-run state. Existing encrypted backups still recover with the old password.
Full FAQ: FAQ.md.
See docs/ROADMAP.md for the full roadmap. Highlights:
- ✅ Phase A — Local-first web app (Mosaic + 8 base pages), E2EE sync, vault, encrypted backups, 6-section Settings, 611 tests green.
- ✅ Phase 1 + 2 (2026-07-20) — Reminders, recurrence, subtasks, full-field editor, NLP quick-add, bulk ops, drag-reorder, PWA, vim shortcuts, project detail page, Kanban, Insights, Weekly Review. 511 unit tests green. See CHANGELOG.
- 🚧 Phase B — Multi-device sync hardening, plugin marketplace, Mosaic interactions, Filter DSL, Cmd+K, calendar drag-to-schedule.
- 🔮 Phase C — Cross-platform native shells, optional LLM-assisted tagging (local model).
CNCL-1.0. See LICENSE.
Quick start · Architecture · Security · Docs · Changelog · Roadmap
Made with care. Your data, your devices, your rules.