A quiet home for the things you need to do.
A self-hosted todo app you live in from the terminal — TUI by default, CLI for scripts. One Go binary, one SQLite file, optional multi-user via invite.
Self-hosted, so it's yours. On every device, so it's always there. Open to a few, when you'd like the company. Small enough to forget you're running it.
- TUI by default, CLI for scripts — same server, no separate daemon
- Projects + Inbox, markdown descriptions, done/undone
- Due dates + reminders — exact instants or all-day dates, with a durable in-app reminder ledger (no mail/push required)
- Search across todos and projects
- Multi-user — first to register owns the server, others join by invite link
- Activity feed — see who changed what, when
- One container, one SQLite file — back it up with
cp
Run the server — one container, one persistent volume:
docker run -d --name dox \
-p 6278:6278 \
-v /opt/dox/data:/app/data \
sn0wl1n/dox:latestOr with Docker Compose — see docker/docker-compose.yml:
docker compose -f docker/docker-compose.yml up -dThen install the client and point it at the server:
npm install -g @l1nsn0w/dox # or: bun add -g @l1nsn0w/dox
doxThe TUI handles onboarding (register · login · accept invite) and everything after.
| Variable | Default | Purpose |
|---|---|---|
DOX_DB_PATH |
./data/dox.db |
SQLite file location |
DOX_LISTEN_ADDR |
:6278 |
HTTP listen address |
DOX_LOG_LEVEL |
info |
debug, info, warn, or error |
DOX_EVENT_RETENTION |
360h |
Activity-feed retention; 0 disables the sweep |
DOX_REMINDER_INTERVAL |
30s |
How often the due-date reminder sweep runs; 0s disables it |
DOX_REMINDER_GRACE |
1h |
How far back a sweep replays reminders missed while the server was down |
DOX_TRUST_PROXY |
false |
Trust X-Forwarded-For for auth rate limiting — only enable behind a proxy you control |
The first account to register becomes the owner. After that, registration is
invite-only by default; the owner can open it with
dox server set-registration true. Login and registration are rate-limited per
client address, and passwords are hashed with argon2id.
GET /healthz is unauthenticated: it returns 200 {"status":"ok"} when the
process can reach SQLite, 503 otherwise. The container image uses it as its
HEALTHCHECK.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ CLI │ │ TUI │ │ any │
│ │ │ │ │ client │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ HTTP/JSON │ HTTP/JSON │ HTTP/JSON
└──────────────┼──────────────┘
▼
┌─────────────────┐
│ dox-server │
│ (Go + SQLite) │
└─────────────────┘
just gen # proto → Go + TS, sqlc → Go DB bindings
just serve # run the server locally
just cli # run the CLI against the local serverA thank-you wall for the people who support my work, alongside a "buy me a coffee" link. It lives at sponsor.sn0w.fyi.

