Task manager, web and native iOS · In development
FocusStack
A personal task manager where the handful of things that actually matter get lifted out of their lists into one view.
Role: Product design, flow design, and direction of the agents that wrote it.
Timeline: Built 2026
- Database tables
- 9
- API endpoints
- 53
- Source files
- 273
Table definitions across lib/db/src/schema. Counted in the repository on 8 August 2026.
Route handlers across 8 route modules. Counted in the repository on 8 August 2026.
TS/JS files across all workspace packages, excluding node_modules. Counted in the repository on 8 August 2026.
What was actually broken
Every list-based task manager has the same failure. You make sensible lists — work, house, the thing you keep meaning to do — and then the three genuinely urgent items are sitting in three different lists, and the only place they exist together is your anxiety about them.
The usual fixes are a priority field nobody maintains, or one giant list that is just the same problem with extra scrolling.
The flow I designed
Starring is a lift, not a label. A starred task doesn't get a small badge; it appears in a Priority view that spans every list. The star means "this one comes up out of its list", which is a spatial idea rather than a metadata idea, and people understand it without instruction.
Today, Snoozed and All are separate answers, not filters on one screen. Each corresponds to a distinct thing a person is actually doing: working now, deferring deliberately, or reviewing everything.
The web and native apps are the same product, not a port. They share an API contract, so a behaviour change lands in both by construction. The native app is genuinely native — real gestures, haptics, iOS blur — rather than a wrapped website.
Every interaction is optimistic. Ticking a task is instant, then reconciles. Task management is high-frequency and low-stakes; a spinner on a checkbox is an unforced error.
The hard part
One contract, three consumers.
The API is defined as an OpenAPI spec, and both the validation schemas and the typed React Query hooks are generated from it. Those generated directories are never hand-edited.
The value shows up when something changes. Add a field, regenerate, and the compiler immediately points at every place in the web app and the native app that now needs attention. Without that, the failure mode is a mobile app that has quietly disagreed with the server for two weeks and nobody notices until a user reports something impossible.
There's a real cost: it is more setup than "just write the endpoint", and it makes quick changes slower. It pays for itself the moment there are two clients. With one client it would be over-engineering, and I'd have skipped it.
Stack
| Monorepo | pnpm workspaces, TypeScript strict, Node 24 |
|---|---|
| Web | React 18 + Vite, Wouter, TanStack Query, Tailwind, shadcn/Radix, dnd-kit |
| Mobile | Expo + React Native, expo-router, Reanimated, haptics, glass effects |
| Backend | Express 5 + TypeScript |
| Database | PostgreSQL + Drizzle ORM |
| API contract | OpenAPI spec → Orval codegen → Zod schemas + typed React Query hooks — The spec is the source of truth; the generated packages are never edited by hand. |
| Auth | Sessions in Postgres, with email verification and 2FA behind a flag |
Outcome
A pnpm monorepo with a web app, a native iOS/Android app, and a shared generated client — lists, tasks, subtasks, sections, reminders, and the cross-list Priority view, working on both surfaces.
Runs locally; not deployed publicly.