Tasklane
The running project
A small but real task tracker — landing page, auth, database, deployed URL — narrated end to end. Every concept in Shipyard maps onto something you'll see here.
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
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
| Piece | Choice | Why |
|---|---|---|
| Frontend framework | Next.js (App Router) | One framework for both UI and API routes. Industry standard. Extensive agent training data. |
| UI library | shadcn/ui + Tailwind | Components you own (copy-pasted into your repo, not imported), well-known design tokens. |
| Database | Postgres (on Neon) | Boring, battle-tested, free tier on Neon, scales beyond what we'll ever need. |
| ORM | Prisma | Type-safe queries. Migrations as code. Excellent error messages. |
| Auth | Auth.js (NextAuth) with email magic links | No passwords to manage; one less attack surface. Auth.js handles the heavy lifting. |
| Resend | Cleanest dev experience. Free tier covers a small project comfortably. | |
| Hosting | Cloudflare Pages | Free for static + functions. CDN built in. Auto-deploys from GitHub. Drag-and-drop also works. |
| CI | Cloudflare's built-in pipeline | No 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":
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
PRISMAmodel 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.