Why this project

Most teaching examples are toys — a counter, a todo, a hello-world. They're so small that the architecture vanishes; you can't see how the pieces connect because there are no pieces. Tasklane is deliberately one notch bigger: enough surface area to need a database, an auth flow, an API, a deployment pipeline, and a real domain — but small enough to read end to end.

It's a multi-user task tracker. You sign in with email. You create projects. You add tasks. You move them through todo / doing / done. Other people on your team see the same board.

The shape of the app

Browser React UI CLIENT Cloudflare Pages Next.js (App Router) /app — UI routes /api — server routes SERVER Neon Postgres DATA Resend magic-link emails EMAIL HTTPS SQL API GitHub → CI → Pages
Tasklane — the runtime layout. The whole thing is in one repo on GitHub, deployed automatically on push.

That's the whole thing. A browser talks to a Next.js app deployed on Cloudflare Pages. The app talks to a Postgres database hosted on Neon for data, and to Resend for sending magic-link emails. Pushing a commit to GitHub triggers CI which builds and ships the app. There are no other moving parts.

The stack — and why each piece

PieceChoiceWhy
Frontend frameworkNext.js (App Router)One framework for both UI and API routes. Industry standard. Extensive agent training data.
UI libraryshadcn/ui + TailwindComponents you own (copy-pasted into your repo, not imported), well-known design tokens.
DatabasePostgres (on Neon)Boring, battle-tested, free tier on Neon, scales beyond what we'll ever need.
ORMPrismaType-safe queries. Migrations as code. Excellent error messages.
AuthAuth.js (NextAuth) with email magic linksNo passwords to manage; one less attack surface. Auth.js handles the heavy lifting.
EmailResendCleanest dev experience. Free tier covers a small project comfortably.
HostingCloudflare PagesFree for static + functions. CDN built in. Auto-deploys from GitHub. Drag-and-drop also works.
CICloudflare's built-in pipelineNo GitHub Actions needed for the basic flow; Pages does it on push.

None of these choices are sacred. Vercel + Supabase, Render + Postgres, Fly + SQLite — all viable. The reasoning is what matters; the names are interchangeable.

How a feature gets built

Here's the actual flow we'll use to add anything to Tasklane — from "I want a feature" to "it's live in production":

  1. Write a one-page PRD

    Three sections: What problem, How we'll know it works, What we won't build. Lives in /docs in the repo. The PRD is short on purpose — it forces clarity.

  2. Convert the PRD into tickets with the agent

    Use the prompt from the library: PRD → tickets handoff. Each ticket is small, has acceptance criteria, names the files likely to touch.

  3. For each ticket: branch, prompt, review, commit

    Cut a feature branch. Hand the ticket to the agent. Read the diff. Push back where it's wrong. Apply when it's right. Commit with a message a stranger could read.

  4. Open a pull request

    The PR description states what changed and how to test it. CI runs. If it's green, you (or a collaborator) approve and merge.

  5. Cloudflare Pages deploys automatically

    Within a couple of minutes, the change is live. You smoke-test the affected path. If something looks off, you can roll back in one click.

  6. Watch logs and metrics for an hour

    Especially after a risky change. The cost is low; the ability to catch problems before users do is high.

The data model

Tasklane has six tables. Knowing them by heart isn't the point — knowing why each exists is.

PRISMA
model User { id String @id @default(cuid()) email String @unique name String? image String? createdAt DateTime @default(now()) memberships Member[] tasks Task[] @relation("TaskAssignee") comments Comment[] } model Workspace { id String @id @default(cuid()) name String slug String @unique members Member[] projects Project[] createdAt DateTime @default(now()) } model Member { id String @id @default(cuid()) userId String workspaceId String role Role @default(MEMBER) user User @relation(fields: [userId], references: [id]) workspace Workspace @relation(fields: [workspaceId], references: [id]) @@unique([userId, workspaceId]) } enum Role { OWNER ADMIN MEMBER } model Project { id String @id @default(cuid()) name String workspaceId String workspace Workspace @relation(fields: [workspaceId], references: [id]) tasks Task[] } model Task { id String @id @default(cuid()) title String notes String? status Status @default(TODO) dueAt DateTime? projectId String assigneeId String? project Project @relation(fields: [projectId], references: [id]) assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id]) comments Comment[] createdAt DateTime @default(now()) } enum Status { TODO DOING DONE } model Comment { id String @id @default(cuid()) body String taskId String authorId String task Task @relation(fields: [taskId], references: [id]) author User @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) }

Don't read every field. Notice the shape: users belong to workspaces through a join table (a user can be in many workspaces, a workspace has many users). Workspaces have many projects, projects have many tasks, tasks have many comments. The Member table with a Role enum is how we'll do permissions later. Reading a schema like a sentence is a fluent skill you'll build.

Environment variables

Every service we use needs a key, and those keys never go in the code. They live in .env.local on your laptop and are entered separately on the deployment platform.

ENV
# Database DATABASE_URL="postgres://user:pass@host/tasklane" # Auth AUTH_SECRET="<random 32-byte hex>" # used to sign session tokens AUTH_URL="http://localhost:3000" # dev only; the platform sets prod automatically # Email RESEND_API_KEY="re_..." EMAIL_FROM="Tasklane <hi@tasklane.example>"

The pattern is universal: every project has a .env.example file checked into git that lists the variables without their values, and a real .env.local that's git-ignored.

How this project threads through the levels

L1 — Mental Models
Tasklane is a web app. The browser is the client. Cloudflare's servers are the server. Every click is a request and the server responds. Same shape as anything you'll ever build.
L2 — Vocabulary
The Tasklane repo on GitHub. Every change is a commit on a branch, opened as a PR, merged to main. We say "deployed" when it's on the live URL.
L3 — The Stack
Next.js (frontend + backend). Postgres on Neon (database). Prisma (ORM). NextAuth (auth). Resend (email).
L4 — Lifecycle
Local on your laptop. Preview deploys on every PR. Production at tasklane.example. CI runs lint and types on every push. CDN is Cloudflare by default.
L5 — Prompting
Every Tasklane feature was built by handing a ticket to an agent. The prompt library examples are excerpted from this build.
L6 — Production
Sentry catches errors. Logs land in Cloudflare. We watch p99 latency on the dashboard. We have a rollback plan and we've used it.