AI Can Write the Code. You Still Have to Guide the App.
kangwijen

People open Cursor, Claude Code, or Codex and type something like this:
Then they act shocked when it looks finished and falls apart the moment a second user shows up, someone sends a hostile request, or they try a real deploy. The model did what it was asked. They asked for a feature list. They did not ask for a product.
AI is good at writing code. It is bad at deciding what should exist. That gap is where most AI-built apps break, and it's worth being precise about what "guidance" actually means before going any further.
Guidance is you drawing the box: what the system may do, where the truth about your data lives, who is allowed to touch what, and how you will know the thing actually works. The agent's job is to fill that box in with code. Skip the box and you get a terrible app wearing a good-looking UI.
Specifically, you decide the parts that are expensive to get wrong. That means what entities exist and who owns them, how people prove who they are (their authentication method), where sessions live, what your API looks like well enough to test, which outside services and libraries are allowed in, and which threats matter most for version one. The model can fill in boilerplate route handlers, database migrations that match a schema you already approved, screens for a flow you already specified, and test scaffolding once you name the cases you care about. If it proposes a library or a pattern you never asked for, that's a design change, not a convenience. Approve it or reject it out loud. Silent dependency creep is how a habit tracker quietly grows a vector database "just in case."
I'm going to walk through how I actually do this, using one example the whole way through. The running example is hypothetical. Call it Habit Board: personal habits, optional shared groups, accounts, a small API. I made it up on purpose so I can talk about authentication and authorization mistakes without dragging a real product into it.
Feature dumps are not specs
A feature dump tells the agent what to paint. It does not say what must never happen, where the database is the one source of truth, how identity works after someone logs in, or what "done" means in a test you can actually run. Left with those gaps, the agent fills them in with demo defaults, because a demo default is the fastest thing that looks correct: open CORS so nothing blocks a request, a role flag sitting in the browser, a getById function with no check on who owns the row, a secret pasted straight into .env.example, and tests that only confirm the happy path the model just wrote for itself.
That last part matters more than it sounds. A language model does not know your product. When your prompt leaves a gap, it does not stop and ask what you meant. It predicts the most plausible-looking thing that could fill that gap, based on the huge pile of demos and tutorials it trained on, and most of those are throwaway projects with no real users. Plausible-looking, in that world, usually means insecure and unfinished but pretty.
Rendering diagram...
Here's the more grown-up-sounding version of the same bad prompt, the kind that fools people into thinking they gave real direction:
"Production ready" is not a requirement. It's a mood. It tells the model nothing about what production actually means for your app. Compare that to something that still fits in one message but genuinely steers the work:
Same product idea both times. Completely different job for the model. The second prompt did not ask it to invent Habit Board. It asked it to operate inside a box that was already drawn, down to the authentication method and what a passing test suite is supposed to prove.
| Feature dump | Guided prompt | |
|---|---|---|
| Scope | Everything at once | v1 boundaries and must-nots |
| Authentication | "add auth" | Magic link only, checks on the server |
| Done | "production ready" | Named tests and a threat note |
| Agent role | Invent the product | Implement under constraints |
You still need the boring engineering vocabulary
You don't need a computer science degree to use these tools well. You do need enough shared vocabulary to reject bad output when you see it, and to say what you want instead of "just build it." If you can't explain Habit Board's scope, its data, and how people prove who they are in plain words, that's the thing to fix before you send another prompt like this one:
Start with three short lists: must, must not, later. Version one of Habit Board needs signup, login, personal habits, and group invites. It does not need push notifications, payments, or a way to browse other people's profiles. That last kind of call is a security decision too, not only a scope one, because every feature you add is more attack surface and more room for the model to invent a shortcut you never asked for.
Then name the nouns in your system and say who owns each one. Users own habits. Groups have memberships, and each membership has a role. Invites are single-use tokens with an expiry date. If the agent's answer to "who owns what" is a variable called isAdmin: true sitting in the browser's localStorage, that is not a data model. It's cosplay: it looks like a permission system, but anyone with the browser's developer tools open can rewrite it in five seconds.
| Entity | Owner | Notes |
|---|---|---|
| User | self | verify email before sensitive actions |
| Habit | user_id | private by default |
| Group | created_by | membership table, not a free-for-all id |
| Membership | user_id + group_id | member or admin |
| Invite | group_id + token | single use, time limited |
Drawn out, the entities and how they relate to each other look like this:
Rendering diagram...
A trust boundary is just the line between "this part of the system will believe what it's told" and "this part has to double-check everything." Keep those lines boring and visible. The browser renders things and collects input, and it should never be trusted, because anyone can open the console and send whatever request they want, no matter what your interface lets them click. The API server is what authenticates the user, decides what they're allowed to do, and validates every input, because it's the one part of the system nobody outside your team can tamper with directly. Postgres holds the one truth about your data. Email and other background jobs get the smallest set of permissions you can hand them and nothing more. Agents love collapsing all four of those into one file, because one file is faster to generate. Your job is to stop that when it actually matters.
Rendering diagram...
Two words get used interchangeably by people who haven't been burned yet: authentication and authorization. They answer different questions, and mixing them up is how apps leak data. Authentication answers "who are you," which is what happens when you type a password or click a magic link. Authorization answers a completely separate question: now that the system knows who you are, what is it going to let you do. Logging in successfully does not mean you're allowed to edit every habit in the table. Every request that touches someone else's data, like GET /api/habits/:id, has to re-check ownership or membership on that specific request, every time, not just once at login. The most common version of this bug even has its own name: IDOR, short for insecure direct object reference. It just means the app trusted an id you sent it instead of checking whether you actually own the thing behind that id. The OWASP Authorization Cheat Sheet (opens in a new tab) (OWASP is the Open Worldwide Application Security Project, a nonprofit that publishes practical, vendor-neutral security guidance) is blunt about the fix: check permissions on every request, deny by default, and hand out the least access that still lets someone do their job.
Rendering diagram...
And if you can't run the app yourself, read a stack trace, and tell a failing test from a bad assertion, the agent is driving and you're just a passenger. That's fine for a toy. It's a bad deal for anything with real user accounts.
Principles the agent will ignore unless you enforce them
Agents don't believe in software engineering as a discipline. They're optimizing for looking complete in one reply, and that bias fights against decades-old engineering principles unless you name those principles in your rules files and actually reject output that violates them.
Two principles do most of the work early, and both go by acronyms worth spelling out once so they stick. YAGNI stands for "you aren't gonna need it": don't build push notifications, an AI coach, or a "flexible plugin system" because it might be useful someday. KISS stands for "keep it simple," which sounds obvious until you've seen the alternative: prefer one authentication path, one habits table, and plain route handlers over a mini-framework invented to handle streak counting.
DRY, "don't repeat yourself," shows up the first time the agent copy-pastes the same authorization check into four route handlers and only three of them get updated the next time the rule changes. Extract a shared requireUser and assertHabitOwner before that happens, not after you find the bug in production.
Separation of concerns, usually abbreviated SoC, and giving each module a single responsibility keep the UI from deciding who has permission to do what, and keep streak math out of the invite email template where it has no business being. Encapsulation means the rest of the app calls something like habits.createForUser(...) instead of reaching into raw SQL from a React component. High cohesion and low coupling means the habit code lives together in one place and talks to email through a small interface, instead of twelve files each importing the provider's SDK directly. Failing fast means validating input at the server's edge and returning a clear 400, 401, or 403, instead of half-writing a row and dying somewhere in the middle. The principle of least astonishment, shortened to POLA, means the same forbidden request fails the exact same way every time, instead of sometimes 403ing and sometimes 500ing depending on which code path it happened to hit. Composition over inheritance means a service that simply takes a Clock and a Mailer as arguments beats a deep BaseTracker class hierarchy the model invented because it looked more official.
| Principle | What I reject in review |
|---|---|
| YAGNI | Speculative features and "for later" tables |
| KISS | Clever abstractions with one caller |
| DRY | Cloned authorization checks that will drift |
| SoC | Permissions decided in the browser |
| Fail fast | Writes that happen before validation |
| Composition | Inheritance trees nobody asked for |
I wouldn't worship the full SOLID checklist on day one either. SOLID is a set of five older object-oriented design principles, and the last four (open/closed, Liskov substitution, interface segregation, and dependency inversion) matter more once a system has actually grown large enough to need them. For Habit Board's first version, YAGNI, KISS, separation of concerns, and failing fast save more pain than reaching for the whole poster on day one. Put a short version of this in AGENTS.md or CLAUDE.md so every session hears it, instead of retyping the same lecture every week:
## Design principles (Habit Board)
- YAGNI: only build what the current brief lists
- KISS: prefer boring, readable code over clever abstractions
- DRY: extract shared authorization helpers, do not clone route logic
- SoC: UI vs server vs DB stay separate. Authorization on the server
- Fail fast: validate at the boundary, reject bad input before writes
- Prefer composition over deep inheritancePrinciples vs practices vs patterns vs architecture
People mash these four words together in conversation, and that fuzziness is exactly what lets an agent answer a principle question with a random architecture change. It's worth pulling them apart once. Principles are broad guides for making decisions, like DRY telling you duplication eventually hurts. Best practices are the specific, day-to-day techniques that put a principle into action, like extracting assertHabitOwner instead of copy-pasting the same check.
| Principle | Practice |
|---|---|
| DRY | Shared helpers instead of cloned route logic |
| KISS | Readable functions, no abstraction with one caller |
| Single source of truth | Roles in the DB, not also in the client |
| Secure by default | Secrets in env, validate inputs, hash passwords if you store them |
| Ship small | Small PRs, frequent deploys, one slice at a time |
I think about four layers when steering AI on a real app. Architecture is the overall shape of the system: for Habit Board's first version, that's one Next.js and Postgres application, sometimes called a modular monolith, not five microservices because someone on YouTube prefers them. Design patterns are smaller, local tools that live inside that shape, like a mailer adapter, a service layer for habits, or dependency injection so tests don't need a real database. Principles are what you use to judge whether a tradeoff is acceptable. Best practices are what CI and your own review habits enforce every day whether you're thinking about them or not.
Rendering diagram...
| Layer | Question | Habit Board example |
|---|---|---|
| Architecture | How is the system shaped? | Modular monolith |
| Design patterns | How do we solve a recurring design mess? | Mailer adapter around the ESP |
| Principles | Is this tradeoff acceptable? | YAGNI: no push in v1 |
| Best practices | What do we do every day? | Small PRs, CI, review AI diffs |
If you want to build real software with AI without slowly drowning in debt, start by learning principles. Practices shift every time a framework changes. Patterns come in and out of fashion as a system grows. Principles are the part that travels with you across every stack you'll ever touch. Put principles in your always-on rules files, practices in skills and CI, and architecture choices in ADRs, which we'll get to shortly, so the agent has to read the reasoning behind a decision before it "helpfully" splits one app into five services.
On patterns specifically, ask for the one that removes a real mess: repository-style data access, a service layer, an adapter around a third-party SDK, MVC-style UI separation. Skip the Gang of Four scavenger hunt. Reach for CQRS, which just means splitting how you read data from how you write it, only once reads and writes have genuinely diverged enough to justify two models, not because the acronym sounds serious.
How vibe coding actually changes the job
Vibe coding, if the term is new, means describing what you want in plain language and letting the model write the actual code instead of typing every line yourself. The hard part of the job stops being typing. It becomes directing the model while still ending up with something you can maintain six months from now. A lot of "full stack advice" turns this into another numbered manifesto to memorize. I'm not doing that here. What follows is how those old habits show up when you're building something like Habit Board.
Build in vertical slices instead of horizontal layers. Finish create-habit end to end, meaning schema, API, validation, authorization, UI, and tests, before you paint five more screens that don't talk to anything real yet.
Rendering diagram...
Ship something small, get feedback, then improve it. Asking the agent for the entire product in one marathon session is how you get half-finished authentication and a merge conflict museum nobody wants to untangle. Keep every change reversible: use branches, understand your own migrations instead of trusting they're fine, put feature flags on risky UI, and only deploy in ways you can roll back.
AI loves a clever one-liner. Push back and ask for names and file sizes a tired human could read at 1am, because that tired human is usually you, and next week's agent session will not magically understand last week's cleverness either. Follow the framework's grain instead of fighting it for originality. Fighting Next.js just to be different usually adds glue code the model will misread the next time it opens the file. Design the API's shape before implementation, so the UI, the tests, and the agent all agree on what POST /api/groups/:id/invites is supposed to return.
Keep roles and permission rules in one place, almost always the database. Duplicating them into localStorage "for speed" is exactly how permissions drift out of sync. Validate at every boundary you can name: forms, API bodies, webhooks, queue payloads, and any AI-generated text before you persist it. Client checks are there for a smoother experience. Server checks are the ones that actually keep anyone honest.
Assume the traffic hitting your app is hostile, because eventually it will be. Leaving authentication and authorization until right before launch is how demos quietly turn into incidents. And don't optimize for speed based on a gut feeling. Profile first, then fix what's actually slow. A speculative caching layer built "just in case" is YAGNI wearing a performance costume.
Automate whatever you find yourself repeating by hand: tests, formatters, linters, type checks, CI. Prefer tests that describe real behavior, like "user B can't read user A's habit," over tests that just assert a function was called with the right arguments, which prove nothing about whether the feature works. Log errors and authentication events, but never log magic-link URLs or session tokens, since a log file is a second database of secrets waiting to leak. Keep business rules like streak logic out of the UI, so a second client someday doesn't have to reinvent it, badly.
Retries happen whether you plan for them or not. Accepting the same group invite twice should never create two memberships. Design for the moment email delivery times out or Postgres has a bad minute, because it will happen. Read the actual logs and DevTools output before you ask the agent to "fix it," or you'll both be guessing. Treat the model like a fast junior developer paired with you: strong at boilerplate and first drafts, weak at architecture, security judgment calls, and the ugly edge cases nobody thought to write down. Review everything before you merge it.
Write ADRs when you plan, then pressure-test the design
All of that still leaves the biggest decisions unrecorded. Plan mode is not done just because the agent produced a list of files it intends to touch. You still need an actual decision you can defend next month, when a different chat session proposes doing the exact opposite. That's what Architecture Decision Records, ADRs for short, are for. Not a twenty-page design document nobody will read. A short note, in a file like docs/adr/0001-magic-link-auth.md, stating the context, the decision, the options you rejected, and the consequences of choosing this over those alternatives.
For Habit Board, early ADRs might cover magic links instead of passwords, Postgres as the only source of truth, and a modular monolith instead of microservices. Keep the template boring:
# ADR 0001: Magic link authentication for v1
## Status
Accepted
## Context
Habit Board needs accounts. We want low friction and fewer password dumps.
## Decision
Email magic links only in v1. No password table.
## Alternatives considered
- Email + password with Argon2id
- Hosted authentication provider (Clerk, Auth.js + OAuth)
## Consequences
- Need reliable email delivery and rate limits
- Account recovery is "send another link"
- Adding passwords later means a new ADR, not a silent rewritePoint AGENTS.md or CLAUDE.md at the ADR folder so the implementer agent doesn't casually invent a second authentication system. When a decision changes, supersede the ADR. Don't pretend the old choice never happened.
Before locking an ADR in, pressure-test it. Read current docs through a docs MCP or the official site, not last year's blog spam. Skim how similar apps handle the same problem. Ask a real person when the stakes are real: a coworker, a friend who has shipped authentication before, someone in a community who will tell you your invite token design is weak. And yes, ask an AI too, but use it as a critic, not as the author of the decision. Use a fresh chat, or a different model if you can:
Paste the ADR under that prompt. Open every link it gives you. If the model can't point at OWASP, framework docs, or a vendor guide you can verify, treat the advice as a rumor.
Rendering diagram...
The point of that loop isn't ceremony. It's stopping the agent from becoming the only reviewer of its own architecture. Research and humans catch things training data smooths over. An AI critique is useful when you force it to disagree with you. An ADR is useful when next week's agent tries to undo the decision in silence.
Day-to-day practices that actually matter on Habit Board look like this: Git from day one, meaningful commits, vertical slices, validation on both client and server, handling loading, empty, and error states in the UI, tests on critical paths, review of AI output before merging, dependencies kept current and few, formatting and linting in CI, basic logging and monitoring, ADRs for non-obvious choices, small PRs, secrets only in env variables, and backups taken before any nasty migration. None of that needs a ceremony of its own. It needs to actually show up in the repo.
How Cursor, Claude Code, and Codex expect to be guided
The file names differ. The idea doesn't. Durable project instructions beat retyping yourself every chat.
Before the comparison, four terms in that table are worth defining once. Always-on guidance is a rules file the agent reads at the start of every session, whether you mention it or not. A skill is a packaged, repeatable workflow you invoke by name instead of re-explaining it. MCP, short for Model Context Protocol, is the standard way these tools connect to outside data and services, like a live docs site or a browser, instead of guessing from training data. A subagent is a separate worker session that does noisy or exploratory work and hands the parent session back a short summary.
| Concern | Cursor | Claude Code | Codex |
|---|---|---|---|
| Always-on guidance | .cursor/rules, AGENTS.md | CLAUDE.md, .claude/rules | AGENTS.md |
| Plan before code | Plan mode (Shift+Tab) | Plan mode | /plan or Shift+Tab |
| Reusable workflows | Skills | Skills | Skills / plugins |
| External tools | MCP | MCP | MCP in config.toml |
| Isolated workers | Subagents | Subagents | Subagents |
Cursor Rules (opens in a new tab) live in .cursor/rules as .mdc files and can always apply, apply when relevant, match file globs, or wait for a manual @ mention. Cursor also reads AGENTS.md, including nested ones, and the CLI even picks up CLAUDE.md. Official advice that holds up in practice: keep rules focused (their docs suggest under 500 lines, splitting large ones apart), use concrete examples, add a rule the moment the agent repeats a mistake, and check rules into git. For Habit Board's authentication work, use Plan mode (opens in a new tab) so it researches and waits for your approval before rewriting half the tree.
Example .cursor/rules slice for API work:
---
description: API route security for Habit Board
globs:
- "app/api/**/*.ts"
- "src/server/**/*.ts"
alwaysApply: false
---
- Authenticate the session before any habit or group mutation.
- Authorize with ownership or membership checks against Postgres.
Never trust body.userId from the client for authorization.
- Validate input on the server with an allowlist schema.
- Return 401 for unauthenticated and 403 for authenticated-but-forbidden.
- Every new API handler ships with integration tests for allow and deny cases.Anthropic's Claude Code best practices (opens in a new tab) are blunt about context filling up and quality dropping as a result. Keep CLAUDE.md short: build commands, etiquette, architectural decisions, gotchas. Their own memory docs aim for roughly under 200 lines. Explore and plan before you implement, then hand it a check it can actually run:
Without a check like that, "looks done" is the only stop condition the model has.
OpenAI's AGENTS.md guide (opens in a new tab) and customization (opens in a new tab) docs treat that file as durable guidance with a real size budget, where files closer to the code you're editing override earlier, broader ones. Keep it small, codify recurring corrections as they happen, pair it with linters and hooks, and use plan mode when the task is fuzzy. A root slice for Habit Board might look like:
# Habit Board
## Commands
- pnpm lint
- pnpm test
- pnpm test:integration
- pnpm typecheck
## Architecture
- Postgres is source of truth.
- Route handlers own authentication and authorization. UI never decides permissions.
- Prefer existing patterns in `src/server/habits.ts`.
## Hard rules
- Do not add push notifications without an explicit request.
- Do not weaken authorization to make a test pass.
- Do not commit secrets. Use env vars documented in `.env.example` with empty values.
- YAGNI and KISS: no speculative features or clever frameworks for v1.
- SoC: never put authorization decisions in the browser.
- Privacy by default: no PII fields that the brief did not request.
- Signup slice must include a working account-deletion path.
- Read `docs/adr/` before changing authentication, data ownership, or deploy shape. Supersede ADRs instead of silently contradicting them.
## Done
A task is done when lint, typecheck, and the relevant tests pass, and the PR description lists authentication and authorization assumptions.If you bounce between tools, one shared source of truth beats three drifting novels that each claim to be the plan.
MCP, skills, and subagents
Rules tell the agent how the repo thinks. That's not enough on its own. You also want real tools, packaged workflows, and workers that keep noisy output out of the main conversation.
Rendering diagram...
MCP (opens in a new tab) connects the agent to docs, a browser, a disposable database, trackers, whatever you approve. Without it, the model guesses from training data or asks you to paste things in manually. With it, you can be specific:
Treat every MCP tool as production-capable until proven otherwise. Prefer read-only credentials for exploration. Don't wire production customer data into a chat window for convenience.
Skills package the how so you stop retyping it every session. Cursor documents Agent Skills (opens in a new tab) under paths like .cursor/skills/. Claude Code's extension overview (opens in a new tab) draws a clean line: CLAUDE.md is always-on, skills are on-demand or triggered with a / command. Codex ships the same idea through skills and plugins. Write a /habit-slice skill once (branch, schema, handler, deny tests, run the suite, summarize authentication and authorization assumptions) and an /authorization-review checklist skill. Concrete commands beat "follow best practices" inside a skill file.
Subagents (opens in a new tab) get their own context window, do the messy work, and return a summary instead of dumping raw output into the main thread. Cursor ships explore, bash, and browser subagents for noisy jobs and lets you define custom ones. Claude Code and Codex offer the same family of feature. Use a subagent for wide search, test log spam, or a second opinion. Use a skill for a one-shot checklist. Don't invent a "changelog subagent" when a skill would do the same job for less overhead.
On Habit Board I'd want an explorer agent before groups land on an existing authentication tree, a test-runner that doesn't paste every log line into the planning chat, a security-reviewer on API diffs, and a verifier that grades the done list. Prompt the parent agent on purpose:
Rendering diagram...
Anthropic's own docs already say the implementer shouldn't be the only grader of its own work. A verifier subagent is that advice, built into one product session. Start with one docs MCP, one slice skill, and one verifier. Add more only once the same pain shows up twice. Marketplace plugins are packaging, not a substitute for deciding what Habit Board is allowed to do.
| Need | Prefer |
|---|---|
| Current docs or browser smoke test | MCP |
| Repeatable slice or authorization checklist | Skill |
| Wide search or second opinion | Subagent |
| "Never trust body.userId" every session | Rules / CLAUDE.md / AGENTS.md |
Security is not a footer
Broken access control sat at the top of the OWASP Top 10 for a reason. Agents recreate those bugs cheerfully, because the demos they trained on never included an attacker.
Here's the shape that should hold for something like Habit Board, drawn out plainly:
Rendering diagram...
No client-side admin SDK with a service role key sitting in the browser bundle. No shared password "for now" that half the team knows. No public bucket for user data exports in v1. Each of those looks harmless in a demo and turns into an incident report later.
Ground Habit Board in the OWASP Cheat Sheet Series instead of vibes. The Input Validation Cheat Sheet (opens in a new tab) wants early server-side validation, allowlists over denylists, both syntax and meaning checks, and a clear split: client checks are for user experience, server checks are for security. Spell that into the prompt:
The Authentication (opens in a new tab) and Session Management (opens in a new tab) sheets want TLS on login and every authenticated page, session ids treated as untrusted input because they're unpredictable by design, cookies marked Secure and HttpOnly with a sensible SameSite, a strict model that only accepts server-created session ids, and regeneration on login. Magic link tokens should be long, random, single use, and time limited. If a diff stores JWTs in localStorage "for simplicity," reject it unless you have a deliberate reason and XSS hardening to match.
Authorization again: deny by default, least privilege, check every request, and don't assume framework defaults cover you. A classic failure looks like this:
const habit = await db.habits.findById(params.id)
return habitThe shape it needs instead:
const habit = await db.habits.findByIdForUser(params.id, session.userId)
if (!habit) return notFoundOrForbidden()
return habitSame story for group boards: membership belongs in the query itself. Secrets stay on the server. .env.example gets empty placeholders, never real values. "Allow *" CORS paired with cookie sessions is a gift to attackers.
When reviewing, I look for client-sent userId or role used for authorization, missing deny tests, string-concatenated SQL, disabled TLS or cookie flags, broad credentials kept around "so local works," and public dump endpoints left in for "admin debugging." Ask for a threat note before any code gets written:
You're not writing a formal threat model for a side project. You're forcing the model to name failure modes before it quietly papers over them.
Compliance: cheaper if you build it in
What applies depends on what Habit Board does and who uses it. Retrofitting privacy after the agent has already sprawled personal fields across five tables is miserable. Building it into the first schema is cheaper.
This is not legal advice. It's engineering hygiene that makes later legal work possible. If you handle health data, payment cards, children's data, or sell into buyers who expect SOC 2, talk to a lawyer before launch, not after a scary email.
Privacy by default means only collecting what you need. If magic-link login only needs an email address, don't also harvest phone numbers, date of birth, precise location, or contacts "for later." Less data means less damage if there's ever a breach. YAGNI applies to personal data fields too, not just features. Consent has to be clear for personal data, marketing mail, tracking cookies, and device permissions. No pre-checked marketing boxes. No vague "improve the experience" blank checks. Users should be able to learn what you collect, why, who you share it with, how long you keep it, and how deletion works, usually through a privacy policy that actually matches the product.
Protect data with HTTPS in production, modern password hashing like Argon2id or bcrypt if you store passwords at all, never plaintext passwords, encryption at rest when the risk warrants it, and secrets kept out of git entirely. Habit Board's magic links avoid storing passwords in the first place, which is a feature, not a shortcut. Many privacy regimes expect access, correction, deletion, and sometimes export. Design tables so "delete my account" can walk user, then habits, then memberships, then invites, without leaving orphaned rows behind. Ask for that path in the same slice as signup, not as a separate ticket six months later.
Log for debugging without turning logs into a second database of secrets. Use mature authentication libraries and sessions instead of rolling your own, put rate limits on login and invites, and consider MFA later if the risk goes up. Don't let the agent reinvent JWT authentication from scratch without a strong reason.
Know which regimes might apply without assuming every app needs all of them: GDPR for people in the EU or EEA, CCPA and CPRA for many California residents, COPPA if the product is directed at children under 13 in the US, HIPAA for protected health information in the US, PCI DSS if you touch card data directly (better to use a payment provider so cards never hit your servers at all), and SOC 2 as a buyer expectation rather than a legal requirement. Third parties like your email provider, analytics, error trackers, and any AI APIs all receive some of your user data, so review what you're actually sending them. Agents love installing analytics that phones home everything by default. Treat adding one as a design and compliance decision, not a drive-by dependency. Keep a basic incident plan on hand too: detect, revoke credentials, restore from backup, notify people when required.
Before a private beta, I want HTTPS everywhere, privacy and terms pages where needed, correct authentication and authorization, secrets outside git, a backup you've actually restored once, rate limits on public authentication routes, basic monitoring, a working account-deletion path, data export if law or customers require it, dependency scanning, and a quick look at the licenses on your open-source dependencies.
Rendering diagram...
Make the agent prove it with tests
Anthropic says give Claude a check it can run. Codex wants "done" defined with build and test expectations right inside AGENTS.md. Cursor's plan-then-build flow only helps if you still demand evidence after implementation, not just a confident summary.
Rendering diagram...
| Layer | Habit Board focus | Cost |
|---|---|---|
| Unit | Streak math, token expiry helpers | Cheap |
| Integration | Ownership and membership deny cases | Medium, high value |
| E2E | Magic link signup, create habit, invite accept | Expensive, keep few |
Don't just say "add unit tests." Name the cases:
Integration tests are where authorization lives or dies. User A creates a habit, and user B can't read it. A valid invite works, an expired or reused one fails, unauthenticated requests fail, and non-members can't list a group's habits. Spell the deny cases out the same way you named the unit cases:
If the agent wrote the handler and the test together, flip a user id on purpose and confirm you get a deny. Tests that only mirror the implementation are a mirror, not a net. They'll pass even when the logic behind them is wrong.
Name behavior cases before the code exists on any path that touches security. Require the suite to actually run, with the output pasted back to you. Forbid weakening production checks just to make a test go green. Put CI on it, so "works in my agent session" stops being the release gate. Split implementing from verifying on authentication and authorization work specifically. A verifier subagent, or even a fresh chat that only grades tests for tautologies, is worth more than another feature.
A workflow that doesn't melt the repo
Define acceptance criteria, plan a simple architecture, generate one vertical slice, review it for correctness and security and readability, run the tests and poke at the behavior yourself, commit, then repeat. Speed of generation was never the skill that mattered here. Consistency and proof are.
Rendering diagram...
Write the brief yourself: must, must not, and later. Name your entities, the authentication method, what's out of scope, the threats that matter, the commands to run, and acceptance criteria like "user B gets 403 on user A's habit." Install always-on rules that are actually falsifiable, not vague aspirations. Wire up one docs MCP, a slice skill, and a verifier before you ask for any features. Plan until you'd actually defend the plan out loud, then pressure-test it with current docs, a human when the choice is expensive, and an AI critique told explicitly to attack the design. Capture the non-obvious choices as ADRs. Implement authentication and users first, then personal habits, then groups, each as a full slice built against those ADRs. Review like you distrust a talented junior: authorization and secrets first, cosmetics later. Commit when green. Ship boring infrastructure. Decline exotic suggestions that contradict an accepted ADR, unless you deliberately supersede that ADR first.
The skill map the model doesn't replace
Coding is only one slice of shipping production software with AI. You still need judgment across programming fundamentals, software engineering, architecture, databases, frontend, backend, DevOps, security, compliance, performance, observability, Git, product thinking, and AI collaboration itself, meaning prompting, review, verification, and managing context.
People skip system design until the agent proposes a message queue for forty users. They skip API design until five different shapes exist for the same invite endpoint. They skip database design until "mysterious bugs" turn out to be a missing unique constraint on membership. They skip security study and then trust the same model that wrote the hole to go find the hole. Skip debugging skills and you can't tell when a "fix" is theater. Skip DevOps and you can't actually ship what you built. Skip observability and "works on my machine" becomes your entire operations strategy. Skip product thinking and you build a notification platform when users asked for a simple habit list. Skip documentation and the next session invents a different architecture, because nothing recorded the last decision anyone made.
A sane learning order looks like this: language, Git, and SQL first, since you can't review a diff without them. Then a frontend, a backend, and APIs, enough to actually ship a slice of something like Habit Board. Then authentication, tests, Docker, and cloud basics, which is what turns a demo into something deployable. Then enough system design, security, and compliance to reject catastrophic defaults on sight. Then performance and observability once you have real measurements to act on. AI workflows, meaning rules, skills, MCP, subagents, and plan mode, come last, as an amplifier on top of that judgment rather than a replacement for it.
Rendering diagram...
| Stage | Focus | Why here |
|---|---|---|
| 1-3 | Language, Git, SQL | You can't review diffs without this |
| 4-6 | Frontend, backend, APIs | Enough to ship Habit Board slices |
| 7-10 | Authentication, tests, Docker, cloud | Turns demos into deployable apps |
| 11-13 | Design, security, compliance | Stops catastrophic defaults |
| 14-16 | Perf, observability, AI workflows | Operate and steer agents well |
If you're new to all this, walk that map far enough to review Habit Board diffs with real judgment. Spend more time writing the brief than polishing prompt wording. Use plan mode until your changes are small enough to reason about. Put OWASP bullets in your rules the moment you touch authentication or authorization. Require tests per slice. Add one docs MCP plus one verifier before you install ten marketplace plugins. If you already ship software for a living, your failure mode is speed addiction: you know better and still accept unread diffs anyway. Invest in hooks, CI, skills, observability, and typed validation. Split implementing from verifying on authentication and authorization paths specifically. Don't outsource database or security review to the same model that wrote the code in question.
Closing
The people who get burned didn't fail some mystical AI ritual. They pasted a wishlist like the habit-tracker prompt at the top of this post and outsourced judgment along with it. The model will write routes, components, and tests. It won't care that Habit Board's group board leaked because findById forgot to check membership.
AI can write the code. You still draw the box: scope, architecture, security, compliance, principles like YAGNI and separation of concerns, vertical slices, ADRs, and proof. Know which layer you're operating on, so the agent doesn't answer a principle question with a random microservice. Keep enough skill in databases, privacy, ops, and product that you can actually reject bad output when you see it. Rules files, CLAUDE.md, and AGENTS.md make the box survive across sessions instead of resetting every chat. MCP, skills, and subagents give the agent tools, playbooks, and a second pair of eyes without melting your main context window. Plan mode refuses to build the wrong shape before it starts. Research, humans, and a hostile AI critique keep you from rubber-stamping your own design. Tests and OWASP-minded review confirm the shape actually holds once it's built.
The people who win with AI aren't the ones with the flashiest prompts. They're the ones who still recognize sound design, catch flawed implementations, and hold the long-term shape of the codebase in their head. AI speeds up implementation. You remain responsible for design, verification, compliance posture, and what the repo looks like in six months.
Feature lists build demos. Guidance builds apps.