Back to the journal

The scope document that says everything except what happens when the client changes their mind

Most scope documents we review from other studios cover features, timelines, and deliverables in careful detail. Almost none of them describe the change request process. Then week four arrives, the client wants to swap the dashboard chart for a filterable table, and suddenly two adults are arguing over whether that counts as 'included' or 'extra'. The project doesn't blow up because the scope was wrong. It blows up because the scope never described its own rules for changing.

A useful scope document needs a short section — even three sentences — that answers: how does the client request a change, how quickly do we respond with an impact assessment, and what happens to the timeline and budget when a change is approved? We write this as a numbered process, not legal prose. Something like: you email the change, we reply within two business days with a time and cost delta, and nothing moves until both sides confirm in writing. It sounds bureaucratic until you realise it is the thing that actually protects the relationship.

The temptation on small projects is to stay loose and handle changes conversationally. That works until it doesn't, and when it stops working, there is no document to point at — only a Slack thread full of ambiguity and goodwill that has quietly curdled. If you are quoting a fixed-price project this week, spend fifteen minutes writing the change request clause before you finesse the feature list. The feature list will change. The process for changing it is what keeps everyone professional.

Your inventory lives in a Google Sheet with three tabs that silently disagree with each other

We see this constantly with small wholesalers and e-commerce operators around Cape Town: one tab tracks stock received, another tracks orders fulfilled, and a third attempts a running total. Nobody remembers who last reconciled them. The formulas reference each other in ways that made sense to the person who built them eighteen months ago, and a single mistyped row throws the count off by dozens of units. The business owner knows the numbers feel wrong but checks by walking into the warehouse, which works until it doesn't.

The fix isn't necessarily a full ERP rollout. Often what's needed is a single-source inventory table — even still a spreadsheet initially — with inbound and outbound logged as transactions rather than edited-in-place totals. That one structural change eliminates the drift between tabs because the current count is always a calculated result, never a manually maintained number. From there, connecting it to your Shopify or WooCommerce store via a lightweight integration means online stock levels update from the same source your packing team uses.

The migration path matters more than the destination platform. Export your current sheet, identify which tab is closest to truth, and treat that as your seed data. Build the transaction log structure, then run both systems in parallel for two weeks before cutting over. If you eventually move to something like Cin7 or Dear Inventory, you'll already have clean data to import. The lesson: your spreadsheet doesn't need replacing because it's a spreadsheet — it needs replacing because three tabs pretending to be one database is a reconciliation problem waiting to bite you on a busy Friday.

GPT-4o rewrites your error messages beautifully and still returns the wrong HTTP status code

We recently tested using GPT-4o to generate user-facing error messages for a client's API. The copywriting was genuinely impressive — clear, friendly, actionable sentences that would have taken a junior dev twenty minutes each to draft. But when we asked it to also suggest the appropriate HTTP status code alongside each message, it got roughly one in four wrong. A rate-limited request came back as a 400 instead of 429. A permissions failure was labelled 404. The words were perfect; the machine-readable contract was silently broken.

This is a pattern worth naming: LLMs are far better at the human-readable layer than the protocol layer. They produce text that sounds correct to a person scanning a Slack thread, but the structured metadata — status codes, enum values, header directives — drifts in ways that won't surface until an integration test catches it or, worse, a mobile client parses it in production. If you're using AI to scaffold API responses, the danger isn't hallucinated prose. It's hallucinated plumbing.

The practical fix is boring but effective: let the model draft copy, but pin every structured value to a lookup your code controls. We ended up with a simple mapping — error type to status code — maintained by hand, while the message string beside it was AI-generated and human-reviewed. The LLM never decides the status code; it only decorates the one you've already chosen. If you're integrating generative AI into anything with a contract — API, schema, config — treat its output as a draft of the soft parts only, and keep the hard parts deterministic.

Your staging environment drifted from production three months ago and nobody noticed until the deploy failed

Small teams often start with a staging server that mirrors production closely enough. Then someone upgrades the database engine on staging but not prod, or adds an environment variable to production during a hotfix and forgets to backport it. Over weeks the two environments quietly diverge. The drift is invisible until a deploy breaks in production despite passing every check in staging, and suddenly your Friday afternoon is gone.

Configuration drift is not a large-team problem that small studios can ignore. It is arguably worse for small teams because there is no dedicated platform engineer watching for it. The fix is not adopting a full Infrastructure as Code stack overnight — that is its own rabbit hole. The fix is a single, version-controlled document (or a short script) that enumerates every meaningful difference between your environments: runtime versions, environment variables, installed extensions, cron jobs, and firewall rules. When someone changes production, the document gets updated in the same pull request. If it does not, the change is incomplete.

Run a manual comparison once a month. It takes thirty minutes, and it will surface something every time — a PHP extension enabled on one side, a timezone mismatch, an orphaned Redis instance. The goal is not perfect parity; staging will always differ in scale and secrets. The goal is knowing exactly where it differs and choosing those differences deliberately. If your last production incident traced back to an environment mismatch, you already have the motivation. If it has not happened yet, it will.

Your payment integration works perfectly until a customer in Limpopo pays with a bank that settles on its own schedule

Most South African payment integrations get tested against one or two major banks with fast settlement times, and the demo works beautifully. Then you go live and discover that not every bank confirms transactions at the same speed, not every network connection stays alive long enough for a synchronous callback, and a meaningful percentage of your customers are paying via methods — like instant EFT or bank transfers — where the confirmation arrives minutes or hours later. If your order flow assumes a clean, immediate success response, you have a problem that only shows up in production, and only for the customers you can least afford to lose.

The fix is designing around eventual confirmation from the start. Treat every payment as pending until your system receives an explicit, verified webhook or status-check response. Build a reconciliation step that polls or listens for outstanding transactions and updates order states accordingly. This matters doubly in South Africa because connectivity drops — whether from load-shedding, unstable mobile data, or rural coverage gaps — mean that callbacks from payment providers can arrive late, arrive twice, or silently never arrive at all. Your system needs to handle all three gracefully without duplicating orders or leaving a customer staring at a spinner.

If you are integrating Paystack, Yoco, Peach Payments, or any local gateway, map out the unhappy path before you write a line of code. Document what happens when the webhook fails, when the user closes the browser mid-redirect, and when settlement takes six hours. Your payment integration is not done when the happy path works — it is done when a customer on a 3G connection in a rural town gets the same reliable experience as someone on fibre in Rondebosch.

Replacing your hero banner testimonial with a specific number from a real client doubled one landing page's form submissions

We rebuilt a client's service landing page last month and the single change that moved the needle most wasn't layout, colour, or button copy. It was swapping a generic testimonial — "Great service, highly recommend!" — for a concrete outcome line: "Reduced our invoice processing from 6 hours a week to 40 minutes." Same customer, same sentiment, but now with a verifiable detail. Form submissions from that page went from roughly 3 per week to between 6 and 8. Everything else stayed the same.

The reason is trust mechanics. A vague endorsement reads like filler because visitors have seen thousands of identical ones. A specific metric forces the reader to imagine the claim being false — and when it's plausible and precise, it passes that test quickly. The number doesn't need to be dramatic. "Saved us 2 hours on month-end reconciliation" outperforms "transformed our workflow" because it's falsifiable. It sounds like something a person actually said, because it is.

If you're collecting testimonials, stop asking clients "Would you recommend us?" and start asking "What specifically changed after we delivered?" Push for a number: hours saved, percentage improvement, days reduced. Then put that answer — attributed, with a name and company if they'll allow it — above the fold on your landing page, not buried in a carousel nobody scrolls. One honest, specific proof point near your call to action outperforms a wall of five-star praise every time.

Your pull request template asks seven questions and reviewers answer none of them

Most teams eventually add a pull request template to their repo. It starts well-intentioned: checkboxes for testing, a description field, a prompt asking what changed and why. Within a month, developers are pasting the same boilerplate, ticking every box reflexively, and reviewers scroll past the template entirely to look at the diff. The template becomes furniture — present but ignored. The review itself devolves into nitpicking variable names while the actual design decision slips through unquestioned.

The problem is that seven-question templates optimise for the wrong moment. They try to front-load context at submission time, when the developer is already mentally done with the work. A better approach is fewer, harder prompts. We trimmed ours to two: "What trade-off did you make?" and "What would break if this is wrong?" The first forces the author to surface the judgment call they actually made — not describe what the code does, which the diff already shows. The second gives the reviewer a specific place to push back. It turns the review from a line-by-line scan into a conversation about risk.

This tiny change had a noticeable effect on our review throughput. Reviewers started leaving fewer cosmetic comments and more architectural ones. Authors wrote shorter descriptions but with more signal. If your PR template has been copy-pasted unchanged for six months, delete everything except the one question that would make a reviewer pause and think. That single question does more than a full checklist nobody reads.

FROM IDEAS TO SOMETHING REAL

Your next chapter.
Let’s build it.

A website, an app, a better way to work. Tell us what you have in mind.

Start a conversation