Most small studios send a proposal too early. A prospective client describes what they want, you nod along, go away, and write up a scope document based on that single conversation. Then two weeks into the build you discover the 'simple dashboard' actually needs role-based access, offline sync, and an export to a legacy ERP system nobody mentioned. The scope was accurate to what was said — it just wasn't accurate to what was needed.
The fix is unglamorous: after the initial call, write a short document — half a page, maybe ten bullet points — that restates what you think the project involves, including the things you suspect are lurking but weren't discussed. Send it back and explicitly ask the client to mark anything that's wrong, missing, or surprising. This is not the proposal. It's a mirror held up before you commit to a number. Clients almost always add or correct something material, and that correction happens while the cost of change is still zero.
This step rarely takes more than an hour, but it shifts the dynamic. You look thorough rather than eager, the client feels heard rather than processed, and both sides enter the engagement with shared assumptions written down. If the client won't engage with it, that silence is useful information too — it tells you the project's communication culture before you've signed anything.
The spreadsheet your ops manager emails around every Monday is a database without any of the safeguards
Every small business has one. It's the master stock sheet, or the client tracker, or the job card list — maintained by one person who knows which columns matter, emailed to five people who each save their own copy, and reconciled by hand when the versions inevitably drift. It works until it doesn't, and when it breaks it breaks quietly: a deleted row, a formula overwritten with a hardcoded number, a filter left on that hides half the data. Nobody notices for two weeks.
The fix isn't always a full ERP or a bespoke app. Often it's a simple Airtable, NocoDB, or even a Google Sheet wired into a proper automation layer — something like Make or n8n — that enforces structure. The key shift is separating the single source of truth from the views people consume. Your warehouse team gets a filtered dashboard. Your finance person gets a weekly summary pushed to their inbox or Slack. Nobody edits the same cells at the same time, and every change is logged. That logging alone solves half the arguments.
Before you build anything, sit with the person who maintains the spreadsheet and watch them work for thirty minutes. Map every manual step: the copy-paste between tabs, the VLOOKUP that pulls from another file, the WhatsApp message they send when a value crosses a threshold. Each of those steps is an automation waiting to happen, and naming them gives you a concrete project scope instead of a vague 'we need a system' conversation. Start with the one that causes the most Monday-morning pain.
GPT-4o's context window is large enough to mislead you about what it actually retained
Large context windows — 128k tokens and climbing — sound like a solved problem. Paste in your entire codebase, your full spec document, your three months of Slack logs, and ask a question. The model will answer confidently every time. What it won't do is tell you it quietly ignored the middle third of your input. Research on long-context LLMs consistently shows degraded recall for information placed in the centre of a prompt, even when the window technically fits everything. The model doesn't error out; it just gets vaguer where you can't easily check.
We ran into this on a client project where we were feeding a 40-page compliance document into GPT-4o and asking it to flag contradictions between sections. It caught issues near the top and bottom of the document reliably. The middle sections — where some of the most nuanced clauses lived — got summarised into bland reassurances. We only noticed because a team member manually cross-checked one answer against the source. The output had looked plausible enough that without that check, we'd have shipped a flawed review.
The practical fix isn't to avoid long contexts entirely — it's to chunk your input deliberately. Break documents into sections, run each through separately, then use a final pass to synthesise. Yes, it's slower. Yes, it costs more tokens. But it turns a confident-sounding guess into something you can actually trust. If you're building any workflow that relies on an LLM reading long documents end to end, build a verification step before you build anything else.
A single shared database login means you will never know which service broke your data
Small teams often start with one database user account shared across every service, script, and cron job that touches production. It works fine until a migration corrupts a table or a rogue query deletes rows it shouldn't have, and you open the database logs to find every operation attributed to the same generic 'app_user'. At that point your incident investigation becomes archaeology: guessing which process ran at 02:14 based on deployment timestamps and Slack messages.
Creating a dedicated database credential per service or process takes about ten minutes and costs nothing on any managed Postgres or MySQL host. Each login gets only the permissions it actually needs — your reporting script gets read-only access, your migration runner gets schema-alter rights, your application gets standard CRUD. When something goes wrong, the logs immediately tell you which actor did it. When something gets compromised, you revoke one credential without restarting everything else.
This is the kind of task that never makes it onto a sprint board because it feels like housekeeping, not a feature. But the first time you're triaging a data incident at midnight with a client on the line, the difference between 'reporting_readonly issued a DROP' and 'app_user issued a DROP' is the difference between a five-minute fix and a two-hour guessing game. If your production database has a single shared login today, carve out an hour this week to split it. Future-you, bleary-eyed and under pressure, will be grateful.
Your USSD fallback menu is worth more engineering time than your onboarding animations
Most of the small-business tools we build in Cape Town have a slick web or mobile frontend, and somewhere in the backlog, a single Jira ticket that says 'USSD fallback — phase 2.' Phase 2 never arrives. Meanwhile a meaningful slice of your actual users — field workers, drivers, informal traders — interact with your system from a device or connection that makes your React app irrelevant. They need a three-screen USSD flow that lets them confirm a delivery, check a balance, or log a job. That flow deserves the same product thinking you give your primary interface.
Building a decent USSD integration in South Africa is not glamorous, but it is surprisingly well-supported. Providers like Africa's Talking or local aggregators expose session-based APIs that map neatly onto a simple state machine. The engineering challenge is less about the protocol and more about discipline: keep menu depth under three levels, never require the user to type more than a single digit or a short reference code, and handle session timeouts gracefully because connections drop constantly. If you treat the USSD channel as a compressed version of your main app's most critical happy path, you end up with something people actually use when the Wi-Fi is out or the data bundle is finished.
The practical takeaway: audit your product for the one or two workflows your least-connected user needs most, then prototype a USSD or SMS flow for just those paths. You are not building a second product — you are building a pressure-relief valve that keeps revenue moving when connectivity, devices, or load-shedding make your primary frontend unreachable.
Replacing your privacy policy link with a one-sentence data promise lifted our form completions by a third
Most landing page forms include a tiny link to a privacy policy nobody reads. It sits there doing almost nothing — a legal checkbox that adds zero reassurance at the moment a visitor decides whether to hand over their email or phone number. We ran a test on a lead-gen page for a Cape Town financial services client where we swapped that link for a single plain-language sentence directly beneath the submit button: "We only use this to send your quote. No spam, no sharing." Form completions rose roughly 35% over four weeks with no other changes to the page.
The reason is straightforward: trust signals only work if people actually process them. A privacy policy link requires a click, a new tab, and parsing legal language — nobody does that mid-decision. A short, specific promise spoken in first person ("we") and addressing the visitor's actual fear ("will you spam me?") resolves the objection right where the friction lives. The sentence works because it is falsifiable and narrow. It does not say "we care about your privacy," which is vague corporate filler. It names the specific thing the data will be used for and explicitly rules out the two outcomes people dread.
This does not replace your actual privacy policy — you still need one for POPIA compliance and it should remain accessible in the footer. But the moment of conversion deserves copy that matches the visitor's headspace, not a legal department's. If your form has a submit button with nothing beneath it except a policy link, try writing one honest sentence about what happens to the data. Test it for two weeks and watch the numbers.
The git blame nobody reads is the only documentation that ages well
Most teams treat commit messages as throwaway text — 'fix bug', 'update styles', 'WIP'. Then six months later someone stares at a baffling conditional and wonders why it exists. They check the wiki (outdated), the Jira ticket (closed, vague), the Slack thread (lost to the scroll). The one artefact that is permanently attached to that exact line of code — the commit message — says 'misc fixes'. This is a waste of the most durable documentation surface your project has.
A well-written commit message answers the question git blame actually asks: why was this change made? Not what changed (the diff shows that), but what prompted it. 'Prevent duplicate webhook delivery when upstream retries within 200ms' is a message that saves a future developer thirty minutes of archaeology. It costs the original author maybe forty-five extra seconds. The ratio is absurd. We started enforcing a simple convention at MoJuKo: the first line states the intent, and if a commit touches business logic, the body must reference the triggering conversation, ticket, or observed behaviour. No novel tooling required — just a repo-level commit template and peer review that actually checks the message, not only the diff.
If you run a small team or a studio, this habit pays off fastest where staff turnover or context-switching is common. You do not need a documentation sprint. You need every commit that lands on your main branch to carry a single sentence of honest context. That sentence will outlast your wiki, your project manager's memory, and probably your Slack workspace.