Your day, one list. A to-do app for what you are doing today — offline-first, and private in the only way that counts: there is no server to send anything to.
Run it from a conversation too — npx skills add maxgfr/today (details).
Nothing.
Today is a static page. It has no account system, no database, no analytics, and no backend of any kind. Your tasks are written to your browser's IndexedDB and stay there.
That claim is worth exactly as much as your ability to check it, so:
- Watch it. Open DevTools → Network, then use the app. Add tasks, complete them, reload. After the initial page load, nothing appears.
- Cut the cord. Turn on airplane mode, or DevTools → Network → Offline. Everything keeps working, because nothing needed the network to begin with.
- Read the policy. The page ships a Content-Security-Policy with
connect-src 'self'. Even if a dependency decided to phone home, the browser would refuse. - Read the gate.
scripts/check-no-network.mjsscans the built bundle for outbound origins, network APIs, and a weakened CSP — and fails CI if it finds any. It runs on every pull request and again before every deploy.
The trade is real and worth stating plainly: nothing is backed up anywhere. Clearing your browser data deletes your tasks. That is what Export JSON in Settings is for.
The day is the product. Today is the whole app. There is no inbox, no backlog, no "someday" list. A task belongs to a day or it does not exist.
- Write the day. Quick capture with
#tagto label,!1–!3to rank,~30mor~2hto estimate. Priorities, tags, steps, notes, and drag to reorder. - Nothing rolls over on its own. Whatever you did not finish is waiting the next morning, with its age printed beside it, for a decision: keep it today, push it to tomorrow, or drop it. A pile can never grow behind your back.
- Repeats. Daily, chosen weekdays, or every N days/weeks. Instances appear as days are opened — switching one on today never invents a month of tasks you never saw.
- Week. Seven days side by side. Drag anything onto another day.
- Finished work stays reachable. Completing a task takes it out of the list — the list is what is left to do — and into a Done drawer for that day. From there you can put it back, or delete it for good.
- Undo and redo on everything, including triage and drag.
- Keyboard first.
⌘Kfor commands and search,?for the full map. - Stats. Weekly completion, streak, lifetime totals.
- Installable. Add it to your dock or home screen; it works with no connection.
| Key | Does |
|---|---|
N |
Write a new task |
⌘K or / |
Commands and search |
⌘Z / ⇧⌘Z |
Undo, redo |
W |
Week |
S |
Stats |
, |
Settings |
[ ] |
Previous day, next day |
T |
Back to today |
? |
The full map |
Esc |
Close whatever is open |
Inside a task row: X completes it, 1–3 set priority, 0 clears it, ⌫ deletes it. To reorder
without a mouse, tab to a row's handle, press Space to pick it up, move with the arrow keys, Space
to drop.
Everything lives in one JSON document. Settings → Export JSON hands you the whole thing, indented and readable — no proprietary envelope, no compression. Import restores it. Delete everything wipes it, locally, with nothing sent anywhere on the way out.
{
"app": "today",
"exportedAt": "2026-08-03T10:00:00.000Z",
"version": 1,
"tasks": { "…": { "title": "Water the plants", "day": "2026-08-03", "status": "open" } },
"templates": {},
"settings": { "theme": "system" }
}The importer is deliberately forgiving: a malformed task is dropped, not treated as grounds for rejecting the file. Losing one bad row beats losing a year of history.
pnpm install
pnpm dev # http://localhost:5173/today/
pnpm verify # typecheck, lint, tests, build, privacy gateDeploying your own copy: fork it, change BASE in vite.config.ts to /<your-repo>/, and enable
Pages with GitHub Actions as the source. The workflow in .github/workflows/deploy.yml does the
rest.
Vite, React 19, TypeScript, Tailwind v4. No backend, no state-management library, no component kit, no router — the app is small enough that each of those would cost more than it saved.
src/domain/— every state change, as pure functions. Undo/redo is a snapshot wrapper around the reducer, so there is no second implementation of each mutation to drift out of sync.src/store/— one versioned blob in IndexedDB, written debounced and flushed onpagehide.migrate.tsis the single door untrusted data comes through, for both stored state and imports.src/lib/date.ts— day arithmetic onYYYY-MM-DDstrings in local time, DST-safe. A task written at 23:50 belongs to that evening, not to the UTC day that already rolled over.src/features/— one folder per surface.- 151 unit tests cover carry-over, recurrence, undo/redo, ordering, date arithmetic, and import repair — the parts where a bug quietly eats someone's day.
Design decisions live in PRODUCT.md and DESIGN.md.
The repo ships a skills.sh agent skill, so you can run your day from a conversation — "what's on today", "add X", "mark X done", "what did I not finish".
npx skills add maxgfr/today
The app keeps its data in the browser's IndexedDB, which nothing outside that browser can reach — that is the whole point of it. So the skill works on the export file: Settings → Export JSON, work in the conversation, Settings → Import JSON. Import replaces the app's data with the file, so export again before editing if you have been adding tasks in the browser.
It is also a plain CLI, with no dependencies and nothing to install:
node skills/today/scripts/today.mjs list
node skills/today/scripts/today.mjs add "Call the bank #admin !1 ~20m"
node skills/today/scripts/today.mjs carried
node skills/today/scripts/today.mjs carry "signed form" tomorrowThe file is ~/.today/today.json unless $TODAY_FILE or --file says otherwise. It keeps the two
rules the app is built on: nothing is ever carried forward on its own, and every task belongs to a
day.
MIT. The font is Archivo by Omnibus-Type, under the SIL
Open Font License — vendored into src/ui/fonts/ rather than loaded from a CDN, because a CDN font
is a request to somebody else's server.




