Back to the journal

Your staging environment drifted from production three months ago and nobody noticed

Most small teams start with good intentions: a staging server that mirrors production so you can catch issues before they reach users. Then someone upgrades the database engine on production but not staging, or a new environment variable gets added to the deploy script but never copied across. Within a few months the two environments diverge silently. Tests pass in staging, deployments break in production, and the whole point of having a separate environment evaporates.

The fix is not more documentation or a shared spreadsheet of server configs. It is infrastructure as code, even at a small scale. A single Docker Compose file or a Terraform config that defines both environments from the same source makes drift structurally difficult. When you change a Postgres version or add a Redis instance, the change propagates to every environment because they share a definition. Tools like Docker Compose cost almost nothing to adopt if your app already runs in containers, and even a two-person team benefits immediately.

The subtle danger of a drifted staging environment is not just broken deploys — it is false confidence. Your QA process tells you everything is fine, your client signs off on staging, and then launch day surfaces issues that staging was supposed to catch. Audit your staging setup this week: compare runtime versions, environment variables, and backing services against production. If anything differs without a documented reason, treat it as a bug. A staging environment that lies to you is worse than not having one at all.

Your payment fallback flow probably hasn't been tested since you integrated it

Most South African e-commerce builds integrate a primary gateway — Payfast, Yoco, Peach — and then bolt on a fallback: EFT, manual proof-of-upload, or a second gateway. The fallback gets tested once during development, everyone moves on, and it quietly rots. A gateway goes down at 14:00 on payday Friday, the fallback triggers, and the EFT confirmation page still references a bank account you closed eight months ago. Or the webhook endpoint for your secondary processor was never updated after the API version bump in March. The customer sees a broken page and leaves.

This matters more here than in markets with a single dominant payment rail. South African buyers already juggle card payments, instant EFT, SnapScan, and store credit — and they'll bounce faster if something looks broken because they have alternatives. Your fallback isn't a theoretical safety net; it's a path real revenue travels through every time a gateway hiccups, which in our market happens often enough to measure. If you treat it as a second-class flow, you're leaking conversions exactly when traffic is highest and customers are most ready to pay.

Put your fallback flows into a monthly smoke test. Not a full regression — just hit the trigger conditions manually, confirm the pages render, confirm the webhooks land, confirm the reconciliation still matches your current bank details and API versions. Add it to the same calendar reminder as your SSL renewal check. The fifteen minutes this takes is trivially small compared to a lost payday-Friday afternoon of orders.

Swapping your clever form for a single-field landing page doubled the enquiry rate overnight

We had a client in Cape Town running a services business with a perfectly reasonable landing page: hero section, three benefit blocks, testimonials, and a contact form with six fields including company size and budget range. The page looked professional. It converted at roughly 1.2%. When we stripped the form down to one field — just an email address — and moved everything else to an automated follow-up sequence, the conversion rate jumped to 2.8% within the first week. No redesign, no new copy, no ad spend change.

The instinct to gather qualifying information upfront makes sense from an internal workflow perspective. Sales teams want leads pre-sorted. But every additional field is a micro-decision you're asking a stranger to make before they trust you. Budget range is particularly brutal — people either don't know, don't want to commit, or assume you'll use it against them. The qualifying conversation still happens, just later, when the person has already opted in and feels some reciprocity.

This doesn't mean every form should be one field. E-commerce checkouts need shipping details; booking flows need dates. But for top-of-funnel lead capture pages, the question worth asking about each field is: does this serve the visitor right now, or does it serve our internal sorting? If it's the latter, move it downstream. Collect the email, send a smart follow-up that asks the next question in context, and let people self-qualify at their own pace. You'll get fewer junk leads than you expect and meaningfully more real ones.

Your code review comments disappear into a pull request graveyard nobody ever searches

Every team we work with does code reviews. Most of them are thoughtful — reviewers flag edge cases, suggest better patterns, explain why a particular database query will fall over at scale. But once the pull request is merged, those comments vanish into a timeline nobody revisits. Six months later a different developer hits the same problem, makes the same mistake, and a reviewer types out the same explanation from scratch. The knowledge existed; it just wasn't anywhere findable.

The fix is embarrassingly low-tech. Keep a shared document — a wiki page, a Notion doc, even a markdown file in the repo — where you paste the most instructive review comments with a line or two of context. Not every nitpick about naming conventions; just the ones where someone explained *why* something breaks, or *when* a particular pattern is the wrong choice. We started doing this about a year ago and the file is only around eighty entries long, but new joiners read through it in their first week and immediately stop repeating the classics.

You don't need a fancy tool for this. The hard part is the habit: when you finish a review and realise you've written three paragraphs explaining connection pool exhaustion under load shedding, copy it somewhere durable before you hit 'Approve'. If a comment is worth writing once, it's worth being able to find again. Treat your best review comments like documentation, because that's what they already are — just trapped in the wrong place.

Charging from day one filters out the feedback that wastes your roadmap

Most founders we work with want to launch a free beta first and 'add pricing later.' The logic sounds safe: remove friction, get users in, learn what they need. But free users behave fundamentally differently from paying ones. They'll request features they'd never actually pay for, tolerate bugs they'd rage-quit over if money were involved, and ghost you the moment something shinier appears. The feedback you collect is real, but it maps to a product nobody will buy.

When you charge even a modest amount — R99 a month, a one-off R500 — you immediately learn which problem is painful enough for someone to reach for their wallet. That signal is worth more than a thousand survey responses. A paying user who asks for CSV export is telling you their workflow depends on your tool. A free user asking for the same thing might just be curious. The difference reshapes your entire backlog. We've seen teams burn two or three months building features driven by free-tier noise, only to watch conversion flatline when they finally flip the switch to paid.

You don't need a perfect billing integration on launch day. A simple Yoco or PayFast link, even a manual invoice, is enough to test willingness to pay. The point isn't revenue yet — it's sorting signal from noise before you commit your small team's time to the wrong features. If nobody pays at any price, that's the most valuable data your MVP can produce.

Lazy-loading your images without a width and height attribute causes the page to jank on every scroll

Most developers know to add loading="lazy" to off-screen images. Fewer remember that lazy-loading without explicit width and height attributes — or an equivalent aspect-ratio in CSS — creates a layout shift every time an image pops into view. The browser reserves zero space for the image until it arrives, then shoves everything below it downward. Google's Cumulative Layout Shift (CLS) metric penalises exactly this, and on content-heavy pages with ten or fifteen images, the score stacks up fast.

The fix is almost embarrassingly simple. Set width and height attributes on your img tags that match the image's intrinsic ratio, even if you override the rendered size with CSS. Modern browsers use those attributes to calculate an aspect ratio and reserve the correct space before the image loads. If you're working with responsive images through srcset, the same rule applies: the width and height on the element give the browser enough information to hold the slot. For containers where dimensions vary, a single line of CSS — aspect-ratio: 16 / 9 or whatever fits — does the same job without hardcoded attributes.

This matters beyond a Lighthouse score. On mid-range phones common in South Africa, layout shifts during scroll feel broken. Users lose their reading position, tap the wrong link, and bounce. If you're already lazy-loading images on a production site, run a quick audit: inspect each img tag for missing dimensions and each wrapper for a missing aspect-ratio rule. One pass through your templates can cut your CLS in half and make the page feel noticeably more stable on real devices.

Flutter's platform channel is where your beautiful cross-platform app quietly becomes two native projects again

Flutter sells you on a single codebase, and for UI work it genuinely delivers. But the moment you need to talk to a device-specific API — biometric authentication, NFC, background Bluetooth, a specific payment terminal SDK — you drop into platform channels and suddenly you're writing Swift on one side and Kotlin on the other. That single codebase now has three codebases hiding inside it, and the team maintaining it needs all three skill sets.

We hit this recently integrating a South African point-of-sale peripheral. The Flutter plugin ecosystem had nothing current, so we wrote platform channel bridges for both iOS and Android. Each bridge needed its own error handling, its own lifecycle awareness, and its own set of integration tests. The Dart layer became a thin dispatcher rather than the source of truth. Debugging meant jumping between three languages and three sets of logs, which is exactly the complexity cross-platform was supposed to eliminate.

None of this means Flutter is the wrong choice — for content-driven apps and standard flows it still saves enormous time. But before you commit, audit every native API your app will touch. Count the platform channels you'll need. If that list is longer than two or three, budget for native developers on both platforms from the start, and structure your project so the Dart side owns UI and business logic while native modules are isolated, versioned packages with their own test suites. Pretending the native layer won't grow is how cross-platform projects quietly blow their timelines.

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