Choosing a partner
Your developer says it is done. How do you check?
You check it the way you would check a builder’s work: on site, yourself, against something written down before anyone started. Four things, in practice. Acceptance criteria agreed in advance. A staging URL you open with your own login, not a screen share. A deliberate walk through the paths where things go wrong. And a reload, to confirm the data survived. A demo passes none of these, because a demo is a rehearsed route narrated by the person who built it.
None of it requires reading code. All of it fits inside ninety minutes with a browser.
Why “it works on the demo” is not evidence
A demo is a performance, and that is not an accusation. The developer chooses the account, the data, the order of clicks and the pace. Every input is one they have typed before. Nothing is interrupted.
Three questions it structurally cannot answer. Does it work when a stranger touches it, in an order nobody anticipated? Does it work when something fails, because in production something always is? And did anything actually get saved, or did the screen only say so? Demos remain the right tool for direction. They are the wrong tool for acceptance, which is a different act with money attached.
Acceptance criteria, written before the work started
A criterion is not a description. It is a sentence that can be false. The shape that works: a user can do X, and afterwards Y is observably true. “A customer can add a second delivery address at checkout, and after logging out and back in, both addresses appear on the account page.” You can stand in front of that and say yes or no.
Compare what buyers usually sign instead. “The dashboard is complete.” “Works on mobile.” “Fast.” “Handles errors gracefully.” Each is an adjective pretending to be a test, and each gets defined, on the day it matters, by whoever is most confident in the room. The decisions that make a brief quotable are the ones that make it checkable, which is why writing a spec a developer can quote is the same exercise as writing one you can accept against.
You do not have to draft them alone: you write the behaviours in business language, the developer writes the edge cases you would not think of, and both sign the merged list before work starts. A developer who will not put criteria in writing is not being casual about paperwork. They are preserving the right to argue later about what done meant.
Then agree the clock: a deliverable is deemed accepted if you have not rejected it in writing inside a fixed window, five working days being normal, citing a specific criterion. Without one, a stakeholder on holiday suspends a payment indefinitely. The contract side of this sits in fixed price versus time and materials.
A staging URL you can open yourself
Not a video, not a screen share, not a clickable design file. A web address on the real stack, with credentials for two accounts so you can test what happens when two people are in it at once. Three questions:
- Is this the build that goes to production? Same commit, same pipeline. If it was assembled by hand for the review, what you accept gets rebuilt before you receive it.
- Does it have the same moving parts? Same database engine, same background job runner, same file storage. Staging with the queue switched off is the commonest way a reporting or emailing feature passes review and then does nothing in production.
- What data is in it? Seeded records are tidy in ways your real data is not, and tidy data hides the import bugs.
Mirroring production is not free, and on small work it is the wrong spend. On a marketing site, which we publish at $7,500 to $15,000 for 2 to 4 weeks and up to 8 pages, a per-branch preview deploy is enough and a dedicated staging tier is ceremony you pay for. On an internal tool or dashboard, $18,000 to $34,000 and usually touching data other people depend on, real staging is part of the work. Those bands are estimates in US dollars, not quotations; the quote that follows scoping is fixed in writing before kickoff.
Walk the unhappy paths on purpose
The happy path is the one the developer has run two hundred times. Spend your ninety minutes on the others. Five are worth doing every time.
Wrong password, five times fast
You want a message that does not reveal which half was wrong, and a fifth attempt within ten seconds treated differently from the first. Then run a password reset for an address that has no account: the right behaviour is the same neutral response as a real one, because anything else turns the reset form into a tool for confirming who your customers are.
A card that declines
If money moves, this is not optional. Stripe publishes test cards for it, and three are worth asking for by name: 4242 4242 4242 4242 succeeds, 4000 0000 0000 9995 is declined with an insufficient funds code, and 4000 0000 0000 0341 attaches to a customer successfully and then fails when you try to charge it. The third is the interesting one, because a team that only ever tested with the success number has never seen it: the customer record exists, the subscription looks created, and no payment happened. Ask what the user sees, and what your own system records. The aftermath is Stripe webhooks in production.
No network, mid-submit
Set the browser developer tools to offline and press save, or fill in a long form on a phone and switch on airplane mode before submitting. Does the application tell you it failed and keep what you typed, or spin forever and discard the lot? The second is not rare, and it is why support tickets say the system lost my application.
Double-click the submit button
Then press the browser back button and submit again. Two orders, two invoices, two emails to the customer? A correct implementation gives the attempt an identifier and refuses to perform it twice. Stripe’s own idempotency keys work this way, and its API reference notes keys may be removed once they are at least 24 hours old, which is why the key belongs to the attempt rather than being reused forever. You need to know only that pressing twice produced one of everything.
Two people editing the same record
Open one record in two browsers as two different users, change a field in each, save the first and then the second. If the second silently wins and the first edit is gone with no warning, you have found the bug that gets reported months later as “the system deleted my notes”.
The standard answer is old. RFC 9110, the current HTTP specification, defines the If-Match header in section 13.1.1 and 412 Precondition Failed in section 15.5.13: the client sends back the version it read, and the server refuses the write if the record moved underneath it. Either the second person is told their copy is stale, or the edits are merged deliberately. A silent overwrite is neither.
The reload test, on its own
This is the highest yield check a non-technical buyer can run, and it takes four seconds. Do the thing. Hard refresh. Is it still there? Then log out, log back in on a different browser, and look again.
Interfaces routinely show a result before the server has confirmed it, because waiting feels slow. The technique is legitimate and ubiquitous. It is also the exact mechanism by which “it worked in the demo” can be entirely true and commercially worthless. The screen said saved. The refresh is what asks the database.
Three questions about the day after you accept
Listen to the shape of the answers, not the tools named.
Where is the error tracking, and what is it reporting right now?
A good answer opens a tab in front of you, shows the last seven days of issues with counts and first-seen dates, and says which are known and which are new since the last deploy. A weak answer is “we check the logs if something gets reported”, which means the first person to know your software is broken will be a customer. Ask whether you get access. You should.
Has a rollback ever been rehearsed?
Not “can we roll back”, which everyone answers yes to. Ask whether anyone has done it on this system, when, and how long it took from the decision to the old version being live. An answer with a date in it describes a capability. An answer in the conditional describes an intention.
Has a backup ever been restored, rather than merely taken?
A backup nobody has restored is a claim, and the difference gets discovered on the worst possible morning. Managed databases make the taking easy and the restoring invisible: Amazon RDS lets you set an automated backup retention period anywhere from 0 to 35 days, defaulting to seven days via the console and one day via the API or the CLI, with 0 switching automated backups off altogether. Ask which number is set on your database, then when someone last restored it into a scratch instance and logged in as a real user. The wider version of this is what happens if your agency disappears.
“Not done” versus “done, and the spec was wrong”
This is the hard part. Both feel identical from your chair: you open the thing and it is not what you wanted. The remedy and the invoice differ completely, and the only instrument separating them is the criterion written before the work started.
- A defect. The build fails a criterion as written. Fixed at no charge, and not a schedule extension you should be asked to fund. That is not a favour, it is the thing you bought.
- A change. The build satisfies the criterion, and the criterion was the wrong thing to ask for. New work, and somebody pays, usually you. Discovering that a requirement was wrong is progress, and it is still not free.
- The grey zone. The criterion was silent and a competent professional should have raised it. Nobody wrote down what happens when a user is removed from an organisation that still owns their records, so the answer gets invented after the fact.
Most real disputes are the third, and it has no clean rule. Our position, stated so you can hold us to it: if we should have asked and did not, we absorb it. That is not generosity, it is where the expertise you paid for is supposed to live. A vendor who calls every unasked question a client omission is describing your next twelve months.
What keeps the grey zone small is a log, not a better clause: request, decision, date, approver, visible to both sides. Nearly every late argument on a build is a memory argument.
What a change request from us looks like, and when it is fair
One page, before any work, carrying five things: what you asked for, what the signed criterion says, why the two differ, the cost, and the schedule impact stated separately from it. A change adding a small sum and two days is a different decision from one adding the same sum and three weeks; a blended number hides which you are approving.
Fair to charge for: the criterion was met and you changed your mind; the production data turned out to be shaped differently from how it was described to us; a third party changed its API mid-build; a new stakeholder arrived with a veto and a different opinion.
Not fair, and we absorb it: we misread a criterion; we failed to ask an obvious question at scoping; we chose an approach that made a foreseeable requirement expensive; typos and layout errors against designs we were given; anything the criteria already cover, however inconvenient for us.
The structural point applies to us as much as to anyone selling fixed scope. Once the price is signed, the vendor is motivated to read an ambiguous line narrowly at exactly the moment you need it read generously. That does not go away because people are decent, which is why the criteria get written while everybody is still relaxed, and why we would rather sell a scoping step than a hopeful estimate. Our published scoping sprint is $3,500 for one week, credited in full against a build that starts within thirty days, and its output is that list. That figure and the bands above are estimates in US dollars, not quotations; the quote that follows scoping is fixed in writing before kickoff.
When this is the wrong amount of process
Too heavy: on a small marketing site the apparatus above outweighs the work. The criteria fit on one page and read like an inventory. The pages exist with the approved copy. A test submission lands in the named inbox while you watch. The site answers on the right domain with a valid certificate, on a phone as well as a laptop. Staging parity and rehearsed rollbacks earn nothing at that size.
Too light: if money moves through the software, if it holds health or financial records, or if a regulator can ask you to demonstrate a control, a buyer clicking through a browser is a first pass and nothing more. That case wants an independent review by someone neither building it nor selling the next phase, and we say so when it applies to our own platform work.
And the outcome nobody plans for: sometimes every criterion passes and the thing still does not help the business. That is a discovery failure acceptance finally made visible, and the remedy is upstream. If it keeps happening with the same partner, the pattern to read is switching development agencies.
The compressed version
Agree criteria that can be false, before work starts, with an acceptance clock attached. Get a staging URL, two logins, and confirmation it is the same build with the same moving parts. Spend ninety minutes on the paths where things break, and reload after every success. Ask the three day-after questions and take the dates seriously. When something is wrong, decide in writing whether it failed a criterion or exceeded one, because those carry different invoices.
If you have just been told a build is finished and do not know how to check it, send us the acceptance criteria you were given, or tell us none were written. We will say which of these checks your situation needs, and that answer is frequently cheaper than hiring anyone.