Baseline app. The goal here isn't the todo list — it's practicing the full eleven-step process (see How to Plan a Full-Stack Project) on the simplest possible domain, so the process itself becomes automatic before app 2 adds real complexity on top of it.
1. Problem framing
People track tasks in scattered places — sticky notes, notes apps, the backs of receipts — and lose track of what's actually left to do. The user wants one place, reachable from any device, to write down a task and mark it done. No collaboration, no reminders, no project management — just a reliable personal list.
2. PRD
Goal: Let a signed-in user create, complete, and manage a personal task list from any device.
Non-goals (explicitly out of scope for v1):
- Sharing or collaborating on lists with other users
- Due dates, reminders, or notifications
- Subtasks, tags, or projects/folders
- Native mobile app (responsive web only)
- Drag-to-reorder (manual ordering)
User stories:
- As a user, I want to sign up and log in, so my tasks are private and persist across devices.
- As a user, I want to add a task with a title, so I can capture something I need to do.
- As a user, I want to mark a task complete/incomplete, so I can track progress.
- As a user, I want to delete a task, so my list doesn't fill up with junk.
- As a user, I want to filter by All / Active / Completed, so I can focus on what's left.
Success metrics:
- Create → see task on screen in under 500ms (p95)
- Zero data loss on refresh or device switch
- First contentful paint under 1.5s on 4G
MVP cut line: auth + create/read/update(complete)/delete + filter. Everything in non-goals is deferred to a hypothetical v2.
3. Wireframes & UX flow
Screen inventory:
| Screen | Purpose | Key elements |
|---|---|---|
| Landing | Unauthenticated entry point | Value prop, "Log in" / "Sign up" |
| Auth | Sign up / log in | Email + password form, error state |
| Task list (main) | The whole app | Add-task input, list, filter tabs, empty state |
Everything after login happens on one screen — that's a deliberate MVP decision, not laziness: a single-screen app has no routing edge cases, no "where does the user end up after X" questions to design around.
4. HLD
No external services, no cache, no queue. Stack rationale: this app's entire read/write pattern is "fetch my rows, mutate my rows" against a dataset that's at most a few thousand rows per user — Postgres handles that without help. Reaching for Redis or a queue here would be solving a problem this app doesn't have (see the methodology chapter's stack philosophy).
5. Data model / ERD
Index: TASK(user_id, completed) — every query on this table filters by
owner and usually by completion state, so that's the composite index that
matters.
6. API design + flow mapping
| Method | Path | Body | Response | Auth |
|---|---|---|---|---|
| GET | /api/tasks?filter=active|completed|all | — | Task[] | required |
| POST | /api/tasks | { title } | Task (201) | required |
| PATCH | /api/tasks/:id | { completed? , title? } | Task | required, must own task |
| DELETE | /api/tasks/:id | — | 204 | required, must own task |
Flow: create a task (optimistic UI)
If the POST fails, the client rolls back the optimistic row and shows an inline error — this failure path is exactly the kind of thing that's easy to forget if you go straight to code without drawing the flow first.
7. LLD
Module breakdown:
app/api/tasks/route.ts — GET (list), POST (create)
app/api/tasks/[id]/route.ts — PATCH (update), DELETE
lib/db.ts — Prisma client singleton
components/TaskList.tsx — fetches + renders, owns filter state
components/TaskItem.tsx — checkbox, delete button, optimistic toggle
components/TaskForm.tsx — the add-task input
Edge cases worth naming explicitly:
- Empty title — reject server-side (never trust client validation alone); trim whitespace before checking.
- Double-submit — a fast double Enter-press before the optimistic row settles; disable the input while a create is in flight rather than de-duping server-side, since a single-user single-device create has no real concurrent-write race to worry about.
- Ownership check on every mutation — PATCH/DELETE must filter by
WHERE id = ? AND user_id = ?, not justWHERE id = ?. This is the single most common authz bug in CRUD apps: it works fine until someone edits the URL. - Empty list state — distinguish "no tasks yet" from "no tasks match this filter" in the UI; they're different messages.
8. NFRs
- Performance: task list (≤1,000 rows) loads in under 300ms server-side; indexed query, no N+1.
- Security / authz: every mutation checks
user_idownership server-side; passwords hashed via Auth.js's credential provider (bcrypt); session cookie ishttpOnly+Secure+SameSite=Lax. - Observability: log 4xx/5xx from the tasks API with user id + route; no need for tracing/metrics infra at this scale — a log line and Vercel's built-in function logs are enough.
9. ADRs
ADR-1: Postgres over client-only storage (localStorage/IndexedDB)
Context: user wants tasks available across devices.
Decision: persist server-side in Postgres from day one.
Alternatives: localStorage-only MVP, sync later.
Consequence: requires auth + a backend from the start, but avoids a
painful "add a server" migration later.
ADR-2: Route Handlers (REST) over Server Actions for the tasks API
Context: Server Actions would mean less boilerplate for this single client.
Decision: expose a stable REST contract under /api/tasks anyway.
Alternatives: Server Actions called directly from components.
Consequence: slightly more code now; the same API is reusable by a future
mobile client or third-party integration without a rewrite.
ADR-3: No manual ordering in v1
Context: drag-to-reorder needs either a float "position" column with
fractional reinsertion or a full reindex on every move.
Decision: defer — sort by created_at only for MVP.
Alternatives: implement fractional-index ordering now.
Consequence: users can't manually reorder tasks until v2; acceptable
because it wasn't in any user story.
10. Deploy plan
- Environments: local (
.env.local+ local Postgres), Preview (one per PR, Vercel + a branch database), Production (mainbranch). - CI: typecheck + lint on every push;
prisma migrate deployruns as part of the Vercel build step against the target database. - Definition of done: a user can sign up, add/complete/delete tasks, refresh and see them persist, on the deployed URL.
- Rollback: Vercel's instant rollback to the previous deployment; database migrations for this app are additive-only in v1, so rollback never needs a down-migration.
11. Retro — "at 10x"
If this suddenly had 10x the users, the first thing to break is the
unindexed assumption that "a user's task list is small" — a power user with
tens of thousands of completed tasks turns GET /api/tasks into a slow
sequential-ish scan without pagination. Second: nothing currently rate-limits
task creation, so a misbehaving script could hammer inserts. At 10x, I'd add
cursor-based pagination to the list endpoint and a basic per-user rate limit
on POST — both are the kind of thing that's cheap to add proactively but
easy to forget until an incident forces the question.