The pet app has one transformation left, and it's the big one. Right now it's a frontend with amnesia: anyone can swipe, nobody is anybody, and every like evaporates on refresh. By the end of this project it will be a full-stack application — your React frontend, your own Express backend, a real database, and real user accounts — where you sign up, log in, like some pets, close everything, come back, and find your pets waiting for you. Everything this section taught, load-bearing, in one build.

Fair warning and honest framing: this is the most ambitious prompt-target of the course so far, which makes it the best possible rehearsal for the capstone. The skills being tested aren't new — they're the spec discipline, reading duties, and git habits you've been compounding all along — but the scale is new, and scale changes how you work with an agent.

Build in milestones, not one mega-prompt

Here's the first workflow lesson this project exists to teach: big builds go to the agent in milestones, with a commit and a verification between each. A single mega-prompt asking for backend-plus-auth-plus-database-plus-UI produces a mega-diff — hundreds of lines across a dozen files that you can't meaningfully read, which means you can't meaningfully check, which means you're not doing your job. Milestones keep every diff readable and every failure isolated.

And here's the second workflow lesson, which makes the first one nearly free: you don't have to architect the plan yourself. The skill is prompting for your prompt. Instead of cowboying a carefully structured spec by hand, you brain-dump — everything you want, every constraint you care about, in whatever order it falls out of your head:

"Okay: I want to add user accounts to my pet tinder app so people can sign up and log in and their liked pets get saved permanently, per user, with a My Pets screen and an unlike button. Requirements: use Better Auth for all auth (do NOT hand-roll any session or password logic), Drizzle as the ORM, Postgres — I have a connection string ready. Keep it Express + Vite React, no Next.js or other frameworks. Email and password login only, no social logins. No deployment, this runs locally."

— and then, instead of sending that as the build prompt, you send it with this on top:

"Don't write any code yet. First, organize everything below into a clear build plan, broken into milestones. Each milestone should be small enough that I can read its diff, and each should end with something I can verify before we move on. Show me the plan for approval before doing anything."

The agent is excellent at this — turning your mess into structure is arguably the thing it does best — and notice what stayed yours: every decision in the brain dump (the stack, the doctrine, the scope) came from you. The agent organized your intent; it didn't replace it.

Then read the plan like it's a diff, because it's about to become several. A healthy plan for this build looks something like: milestone one, the backend exists (Express serving the built frontend, one /api/health route, nothing else); milestone two, the database is connected (Drizzle plumbing and a schema file, no features yet); milestone three, auth works (Better Auth in, its tables migrated, you can sign up, log in, and log out); milestone four, the feature (per-user likes saved to the database, a My Pets screen, an unlike button). If the plan that comes back skips your doctrine (where's Better Auth?), crams too much into one milestone, or invents features you never asked for — edit it or push back before anything gets built. Approving the plan is the review, and you're the editor-in-chief of a document assembled from your own brain dump, which is the easiest possible document to have opinions about.

One thing no plan can do for you — the database milestone has a manual step. Before the agent can connect anything, go rent the database — by hand, in the browser, on purpose: sign up at Render (this is also the account the capstone needs — two birds), click New → Postgres, pick the free tier, and when it's provisioned, get its connection string into your .env as DATABASE_URL — either paste it in yourself, or simply hand it to the agent: "here's my database connection string; put it in my .env as DATABASE_URL and make sure .env is gitignored." Both are fine, and the second is a legitimate delegation, not a cheat. (One hygiene note for the future: pasting a throwaway dev credential into a chat is fine; develop a flinch before ever doing the same with a production one.) I want you doing the provisioning manually because it's the last time you'll have to: later in the course, your agent gets hands of its own and does exactly this step for you — and delegation only feels like power, rather than magic, when you've personally done the thing being delegated.

Then execute: one milestone per prompt round, verify what the plan said to verify, read the diff, commit, next. Each round's prompt should constrain the blast radius — "we're on milestone two; do only milestone two; don't change anything else." That sentence is the senior move.

The anti-goals, project-wide — these went into your brain dump, and they're restated here because they're doctrine: Express + Vite React (no Next.js or other meta-frameworks — the pieces you can read, assembled), Better Auth (the agent must not hand-roll any session or password logic — you know the doctrine), Drizzle + Postgres (rented free from Render), no deployment. The app runs on your laptop; the database lives in the cloud from day one — the exact arrangement the databases lesson promised, and the reason there will never be a database switch in this project's future. (One honest note: Render's free Postgres is a thirty-day trial, which is fine — this is a learning artifact, and "free dev databases expire" is itself a true thing worth learning.)

Reading duties: the section, cashed

Schema first, always. After the auth milestone, open the schema and meet Better Auth's tables — user, session, and friends — created by a migration, exactly as promised. After the feature milestone, find your table: something like likes, with a userId column wearing a .references() — the wiring diagram stitching your feature to Better Auth's users. One schema file now tells the app's whole story: who exists, who's logged in, who liked what.

Route table plus the middleware column. Scan the server for the API's table of contents, then read between path and handler: which routes have a bouncer? The likes routes should; the health route shouldn't. Then prove it from the outside — open Bruno and POST to your likes route with no cookie: 401, live, from your own bouncer. (Bonus reading: find where Better Auth mounted its own /api/auth/ routes.)

Find the wristband. Log in, open dev tools, find the session cookie (Application tab → Cookies), then watch the Network tab as you like a pet: there's the cookie riding along on the request, automatically, exactly as the wristband model said. You are watching the auth lesson happen.

The full-stack trace. The course ritual, at its maximum length — trace one Like click across the entire stack and write the route in your notes: click → onClick → handler → fetch("/api/likes", { method: "POST", ... }) → across the wire → middleware glances at the wristband → req.user exists → await db.insert(likes).values(...) → response → state setter → re-render → the pet appears in My Pets. Eleven-ish steps, three machines' worth of concepts, and you can account for every single one. At the start of this course you couldn't read a console.log. Take a second.

The restart test, reversed. You know what comes next, because you've been on the other side of it: Ctrl+C the server. Restart it. Log back in. Your pets are still there. Log out, log in — still there. Close the laptop, reboot, come back — still there, because the memory doesn't live on your machine at all; it lives in the rented Postgres, unbothered by anything that happens to your laptop. And you can go look at it: connect the PostgreSQL extension in VS Code with your DATABASE_URL and browse the tables right in your editor — there are your users, there are your likes, rows in a real database, the spreadsheet analogy rendered literal. (Drizzle has a viewer of its own too — npx drizzle-kit studio opens a browser-based one — if you ever want a second window into the same truth.) The exact loss you felt in the API project, answered by an artifact you can see with your own eyes.

The two-account test. One more, and it's the authorization half of auth made real: sign up a second account (a fake email is fine locally) and like some different pets. Verify the two accounts' pets are separate — your likes are yours, theirs are theirs, because every query in the likes routes filters by req.user. Find the line that does it — it'll read something like where(eq(likes.userId, req.user.id)), and it is the single most important line in the app: it's the difference between "a pile of likes" and "your likes." If you can delete another account's like from Bruno, you've found a real authorization bug — the 403 that should exist doesn't — and that's a genuinely valuable follow-up prompt.

🤖 Ask your AI Assistant. The security audit, now with stakes: point it at the finished app and ask "List every credential and secret in this app, where sessions are stored, where passwords are stored, and whether any of it could reach the client." Read the answer against the whole section: passwords should be hashed in the user table (never plain — if the audit says otherwise, stop everything), sessions in the session table, nothing sensitive in frontend code. Then the follow-up that tests the deepest thing you've learned: "Walk me through what happens if someone sends a POST to /api/likes with no session cookie — every step, in this code." The answer should walk the bouncer's hallway to a 401, and you should be able to follow every step of it, because you built the hallway.

Commit the final state and take stock of the artifact: the pet app was born as a hardcoded array, learned to fetch, was reborn in React, and now it has users, a memory, and a bouncer. That's the entire modern web stack, held in a project you've personally watched grow one capability at a time — which means when the codebase walk and the capstone hand you someone else's full-stack app, you won't be meeting a stranger. You'll be recognizing an old friend with different furniture.