How to create beautiful user-centric products with AI
The models are the easy part. What keeps a product user-centric is the harness around them, from principles and stories to prototypes, tests and gates.

Most teams we talk to are stuck on the same question. The new coding tools write code faster than anyone can review it properly, so how do you keep the speed without giving up quality and security?
The numbers are worse than most people admit. Stack Overflow's latest developer survey puts the top frustration at output that is almost right, but not quite, named by two thirds of respondents. Sonar's State of Code survey found that 96 percent of developers don't fully trust what these tools produce, yet barely half of them consistently check the output before committing it. Writing code stopped being the bottleneck. Verifying it became one, and most teams' processes never caught up.
We build Growomat this way every day. Claude and Codex write a large share of our code, and we care a great deal about the product staying beautiful, intuitive and reliable. So here is how we actually work: what we tell the models, what we make them produce before any code exists, and what stands between their output and our customers. We're writing it down so other teams can borrow the parts that work.
The short version: the model is the easy part. Everything that matters lives in the harness around it.
Four principles
It's easy to get lost in the technical detail of software development. Models, prompting tricks, testing frameworks. We start somewhere else, with four principles:
- Make the complex simple for our users. Every feature is designed with simplicity in mind, and simplicity is verified rather than assumed. Test cases cover web and mobile in each language, we check the screenshots, and we run persona walkthroughs (more on those below).
- Intuitive design. No one reads manuals. Each feature should guide the user and give them a beautiful interface, so navigation makes sense on first contact.
- Keep documentation and code current. Coding assistants love to add: more code, more docs, another helper with every feature. That means context bloat and more surface area for bugs. Update what exists instead of adding alongside it.
- Strictly gate every feature on end-to-end testing, security, internationalisation and regressions. We work fast. Nothing ships around the gates.
These sit at the very top of our CLAUDE.md, the first file the model reads in every session. Your team's principles will be different. What matters is putting them where your tooling actually picks them up, rather than in a wiki no model will ever open.
Directly below them we keep two more things:
- Structure. The layout of our repositories, the interfaces between them, and where the documentation lives.
- Scripts and tools. How to start, stop and debug the application, and how to run each of the end-to-end scenarios.
Every session then starts the same way. The model knows where everything is, how to run it, how to test it, and what we value.
The story comes first, and the test plan comes before the story is done
We don't open a feature with technical instructions. We describe the problem we're solving for the customer, and the first thing we ask for is questions: every clarifying question the model has, before it forms any opinion about a solution.
That conversation produces a user story on a strict template.
- The story in one sentence. As a user type, I want goal, so that benefit. If we can't write that line, we've learned something important before writing any code.
- Context. The problem, the current state of the product, and for fixes, the root cause.
- Acceptance criteria with IDs, including a No regressions list of existing behaviour that must not break.
- Internationalisation. Every new user-facing string, named up front with its English value.
- Test coverage, written before the acceptance criteria are final. The part most teams skip. If we could only keep one section of the template, we'd keep this one.
That last one deserves an explanation. The template's default answer is "update an existing spec", not "write a new one". Before a story is accepted we search the suites for the surfaces it touches and name the exact spec files that will change. A new spec file needs justification, which usually means a genuinely new surface with no coverage today. "New specs: none" is a good and common answer, and we state it explicitly so a reviewer can see the question was asked.
Two things fall out of this. If nobody can name the tests that will prove a feature, then nobody understands the feature yet, and that is much cheaper to discover before the code exists. The second effect is subtler. The suite stops growing a file per feature, which is the difference between tests that map your product and tests that map your history.
Prototype before code
Anything with an interface gets a clickable prototype alongside the story: a throwaway page with the real layout, the real copy and fake data. The model builds one in minutes.
Design review happens here. Moving a control on a prototype costs nothing, moving it after implementation costs a story. It's also where principle one gets its first real test. If the prototype needs explaining, so will the feature, and no amount of implementation quality will rescue it.
The prototype is disposable by design. It never becomes the implementation. It becomes the specification for one.
Tests move with the code
Code written this fast will rot a test suite that's organised by history, the "tests we added for that feature" pattern. Ours are organised by behaviour instead. Both suites, backend and frontend, use one layout:
e2e/
├── journeys/ FEW: ordered, cross-feature user flows
├── components/<area>/ one directory per subsystem
├── contracts/ parameterised matrices (auth, validation, shapes)
└── lib/ fixtures and helpers, the only sanctioned setup path
That is about four hundred spec files across the two suites, and seven of them are journeys. Keeping the second number small is the whole discipline. A journey earns its place by walking an ordered flow that no single component spec can assert, and each one opens by declaring what breaks it:
/**
* JOURNEY — sign up → onboarding → first campaign live.
*
* TOUCH THIS if you change: the onboarding step order or guards, the plan
* step, the trial claim, the connect step, or the welcome shell. A story
* that renames a step or inserts one WILL break this file — that is the
* point.
*/
Everything else is asserted in exactly one place: a component spec named after the behaviour, or a contract matrix that sweeps authentication, validation and response shapes across every endpoint at once.
Because spec filenames name the behaviour they assert, "what does my change affect?" is one grep rather than an archaeology dig. The movement rules are mechanical. Change a surface, update every spec that asserts it, in the same change. Remove a feature, delete its specs in the same change. Rename a UI identifier and a checker fails the build on any test still pointing at the old one.
One rule has no exceptions. A failing test is never skipped, disabled or deleted to get to green. It is either a real regression or a stale assertion, and the only acceptable next move is finding out which.
Gates, not reminders
Working with coding assistants means accepting that they will forget your rules. So will you. A convention that lives in a document is a hope. A convention that lives in a script that fails the build is a fact.
So every rule we've caught ourselves repeating in review has become a gate:
- Vocabulary gates. We renamed a core entity once, and a script now fails the build on the old word anywhere in user-facing copy. Another enforces one word per state, so a campaign is "paused" everywhere and never "paused" here and "suspended" over there.
- Claims gates. Growomat delivers to Google, Microsoft, Reddit and Meta. Copy that names some of those platforms and quietly drops the others fails the build, so marketing cannot shrink the product by accident.
- Price gates. Prices live in one config file. A price typed anywhere else, in a doc or a landing page or a blog post, is a second source of truth that won't move when the real one does. The gate rejects it.
- Conformance gates. Every ad platform integration declares its capabilities in a manifest, and a test suite verifies that the code satisfies each claim. A manifest that lies is a build failure rather than a code-review hope.
- Dependency gates. Coding assistants occasionally reach for a package
that does not exist, or one that was abandoned years ago. Every pipeline
installs with
npm ci, which refuses to run at all when the lockfile and the manifest disagree, so an invented dependency dies at install time instead of becoming a puzzle for whoever picks up the branch.
None of this is infrastructure. Most of our gates are a grep with an exit code. Here is the renamed-entity one, essentially in full:
HITS=$(grep -rInE '\bsites?\b' src content | grep -v 'vocabulary-gate:allow')
if [ -n "$HITS" ]; then
echo "The entity is 'business', not 'site'. Fix or justify:"
echo "$HITS"; exit 1
fi
The escape hatch matters as much as the rule. A legitimate exception carries an allow marker on the same line as the hit, so every exemption turns up in the diff that introduced it and gets reviewed along with it. That is the difference between a gate and a nag.
This post went through those gates on its way to you, including the one that polices the blog's own vocabulary. Nothing here skips the pipeline.
The point isn't any single gate. Review comments don't scale with this pace of development and gates do. They read every line, every time, and they never get tired or polite.
Let a stranger use it
Everything above verifies what we already thought to assert. None of it catches the confusing label, the dead end, or the button nobody can find.
For that we run persona walkthroughs. A model gets a persona ("a first-time founder who has never bought an ad"), a goal, and cold access to the product. No insider knowledge, no test hooks, no script. It uses the app the way a stranger would, then writes a narrative report of what it did, where it hesitated and why.
The walkthrough is deliberately blind. At each step the model sees only what a user would see: a screenshot of the page, the accessibility outline, and the URL. It decides what to click from the pixels. When it cannot find a feature, that is the finding, and the report cites those screenshots step by step, so "I couldn't find it" arrives with the exact frame where the looking happened.
An operational note, because this is the part teams get wrong when they copy it. Passing screenshots to a vision model is slow and it costs real money, so it never runs on a pull request. Behavioural specs gate merges and keep that feedback loop short. Walkthroughs are run deliberately instead, when we have changed something a user has to understand, and before a release goes out. The screenshots our ordinary test runs produce across web, mobile and each language are reviewed by us rather than by a model, which is how a truncated German label gets caught without a vision model in the loop at all. Put the expensive judgement where it pays, and a Tuesday afternoon hotfix still ships on Tuesday afternoon.
One rule makes the whole thing work: a persona giving up is a successful run. Walkthroughs never gate the build and never produce a pass or a fail. The abandonment story is the deliverable. "I couldn't tell whether saving had worked, so I stopped" is worth more than a hundred green checkmarks.
Where real people come in
Persona walkthroughs have a limit, and it is worth naming out loud. A model is an echo of the data it was trained on. It can be surprised, but mostly in the ways its training makes plausible, and it has never had an ad budget it could not afford to waste or a boss asking why last month underperformed. Lean on simulated users too heavily and you will ship a product that makes perfect sense to a model and feels slightly off to a person.
So the loop closes with real ones.
We run Microsoft Clarity on the product, loaded only after someone gives explicit cookie consent. No consent, no recording, no cookies. What it buys us is session recordings and heatmaps: where real people hesitate, where they click something that was never a button, where they abandon a flow that passed every test we wrote. Our own funnel events are tagged onto those recordings along with a little context, which plan someone is on, which onboarding step they reached, where the visit came from, and sessions are identified by a non-personal id. That turns a pile of video into something you can ask narrow questions of. Show me people on a trial who reached the connect step and left.
The difference from testing is the whole point. A test tells you the connect step works. A recording tells you that four people reached it, scrolled up and down twice, and closed the tab. Only one of those is a product problem, and no assertion we could write would ever report it.
Support requests are the third input and the least filtered of the lot, because they are sentences real customers typed while annoyed.
None of it changes the process, it feeds it. What recordings, tickets and walkthroughs turn up goes back to the top of this post: a story, a test plan named before the acceptance criteria are final, a prototype if it has an interface, and a gate if it turns out to be a rule we keep repeating in review. The models make that loop faster. They do not close it.
What to steal first
Five things, in this order:
- Write your principles where your model reads them first, along with the structure of your repositories and the commands to run and test everything. One file, at the front of every session.
- Demand the test plan before the story is done. Default to updating existing specs. If nobody can name the specs, nobody understands the feature yet.
- Turn your three most-repeated review comments into scripts that fail the build. You will never write those comments again.
- Watch a stranger use your product, and treat "I gave up" as the most valuable report you can get.
- Then watch real users, because a simulated one shares its training data with the thing that helped build your product. Session recordings and support tickets are where that blind spot gets found.
None of this slows the models down. It aims them. The pace these tools give you is real, but pace without a harness just means arriving at the wrong product sooner.
Growomat Dev Team
Run all four platforms from one place.
Build, optimize and report on Google, Microsoft, Reddit and Meta campaigns without an agency retainer.
See what it costsRead next
engineering
Why the Reddit Ads API was harder than it looks
We budgeted a day to integrate Reddit Ads and it took two weeks. Here are the refusals we hit, why our tests never caught them, and how it ends.

Your ad budget is a hope, not a guarantee
Every ads tool alerts you when spending passes your budget. None of them act on it. How the Emergency Brake pauses campaigns, and what it deliberately never does.