Back to the journal

Charge money on day one or you will validate the wrong thing

Most founders we work with want to launch a free beta first and 'add pricing later.' The logic feels safe: remove friction, get users, prove the concept. But what you actually prove is that people will click around something free. That tells you almost nothing about whether your product solves a problem worth paying for. The moment you flip the switch to paid, you discover your real market — and it is often a completely different group of people with completely different expectations.

We built an MVP last year for a client in the logistics space. They wanted a three-month free pilot with a handful of trucking companies. We pushed back and suggested a modest monthly fee from week one, even before the feature set was complete. Two of the five pilot companies dropped out immediately. The three that stayed gave more detailed feedback, responded to calls faster, and flagged bugs same-day. Paying users behave like partners because they have skin in the game. Free users behave like tourists.

This does not mean you need a polished Stripe integration before launch. A manual invoice, an EFT payment, even a simple 'pay us R500 to get early access' Google Form will do. The mechanism matters far less than the act of exchange. If you are building an MVP right now, put a price on it before you put it in anyone's hands. The silence you get back is more honest than any amount of free-tier engagement metrics.

Lazy-loading images below the fold while eagerly fetching the LCP one above it is a five-minute fix that moves your PageSpeed score more than most refactors

Most sites we audit treat every image the same way: either everything gets `loading="lazy"` because a linter suggested it, or nothing does because nobody thought about it. Both approaches leave performance on the table. The image that matters most for your Largest Contentful Paint score — usually a hero banner or product photo visible without scrolling — needs to load as early as possible. Slapping `loading="lazy"` on that element actually delays it, because the browser waits until layout is calculated before deciding to fetch. Your LCP time creeps up and Google notices.

The fix is boringly simple. Identify your above-the-fold hero image, set `loading="eager"` (or just remove the lazy attribute since eager is the default), and add `fetchpriority="high"` so the browser bumps it ahead of stylesheets and third-party scripts in the queue. Then mark every image below the fold with `loading="lazy"`. On a recent client project — a Johannesburg e-commerce catalogue with heavy product photography — this single change dropped LCP from 4.1 seconds to 2.3 seconds on a throttled 4G connection, with zero changes to hosting or image compression.

If you're using a framework like Next.js or Nuxt, check that your image component isn't silently applying lazy loading to the primary visible image. Next's `Image` component defaults to lazy; you override it with `priority`. Audit one page, fix the hero, lazy-load the rest, and measure before and after with Lighthouse on mobile. Five minutes, measurable difference.

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

Flutter sells you one codebase for iOS and Android, and for screens full of widgets that promise holds up brilliantly. The trouble starts when you need something the framework doesn't wrap: biometric authentication with device-specific fallback chains, Bluetooth Low Energy peripherals, or South Africa's favourite trick — deep-linking from a banking app's payment confirmation back into yours. At that point you open a platform channel, write Swift on one side, Kotlin on the other, and suddenly you are maintaining three codebases instead of one.

We hit this on a recent delivery app where the client wanted background location updates accurate enough to show a driver's position while the app was suspended. The Dart side was trivial. The platform channel work was not — iOS and Android handle background execution budgets completely differently, and each OS version tightens the rules. Our 'cross-platform' feature needed two separate implementations, two separate test matrices, and a Dart shim whose only job was choosing which native result to trust. The total effort was roughly 70% of what fully native builds would have cost.

None of this means Flutter is the wrong choice. It means your project estimate needs a line item for every feature that touches hardware or OS-level behaviour. Before you commit to a cross-platform framework, list every integration point — payments, sensors, notifications, background work — and mark which ones require platform channels. If that list is long, budget native effort explicitly rather than discovering it mid-sprint when your timeline is already set.

Writing the scope document before the proposal kills more bad projects than saying no ever will

Most freelancers and small studios treat the scope document as a formality — something you dash off after you've already decided to take the project. Flip that. We now write a lean scope document before we even decide whether to send a proposal. It lists every integration, every user role, every edge case the client has mentioned, and every assumption we're making. The document is typically one page. Writing it takes an hour or two. But the number of times that hour has surfaced a dealbreaker — an undisclosed legacy system, an assumed feature that triples complexity, a compliance requirement nobody mentioned on the call — has saved us weeks of painful renegotiation.

The trick is sharing it with the client before pricing. You send the scope, you ask them to correct it, and you wait. If they add three bullet points that fundamentally change the build, you now know that before your estimate is locked in. If they go quiet for two weeks, you've learned something about how the engagement will feel. The scope document becomes a low-cost stress test of the entire working relationship. Fixed-price or hourly, it doesn't matter — both models break when the boundaries are vague.

The practical takeaway: before your next proposal, spend an hour writing a plain-language scope document that lists features, integrations, user roles, and your assumptions. Send it to the client for sign-off before you quote. You'll either sharpen the project into something you can confidently price, or you'll discover early that the project isn't ready — and that discovery, before contracts are signed, is worth far more than the hour it cost you.

Pipe your Xero invoice webhooks into a WhatsApp message before your client ever opens their email

Most small businesses in South Africa have the same accounts receivable workflow: Xero generates an invoice, emails it, and then someone manually follows up on WhatsApp three days later asking if the client got it. That manual chase is where invoices go to die. The gap between the email leaving Xero and the WhatsApp nudge landing is pure friction, and it is entirely automatable.

Xero's webhook system fires events when an invoice is created or updated. You can catch that event in a lightweight middleware — a simple Node or Python service, or even a Zapier/Make scenario if you want zero code — extract the contact's mobile number from the Xero contact record, and trigger a WhatsApp message through the official Business API or a provider like Twilio. The message does not need to contain the full invoice; a friendly note with the amount, due date, and a link to the Xero online payment page is enough. The key detail most people miss: Xero's webhook payload only sends resource IDs, not full objects. You need to call back into the Xero API to fetch the invoice and contact details before composing the message.

Keep the template transactional and within WhatsApp's approved message guidelines, or your number gets flagged fast. Include an opt-out path. And log every send against the invoice ID so your accounts person can see at a glance which clients were notified without opening another dashboard. The practical takeaway: wire the webhook to WhatsApp once, and you remove the most common delay in your payment cycle — the one where a human had to remember to follow up.

GPT-4o's structured output mode turns flaky API parsing into a boring, solved problem

Half the pain of integrating an LLM into a production workflow isn't the prompt — it's parsing whatever the model decides to give back. You ask for JSON, you get JSON wrapped in markdown fences. You ask for three fields, you get four plus a chatty preamble. So you write regex, you write retry logic, you add a validation layer, and suddenly a third of your integration code exists just to cope with format drift. OpenAI's structured output mode (and similar features from Anthropic and others) changes this: you pass in a JSON schema and the model is constrained to produce only output that conforms to it. Not 'try to match this', but actually constrained at the token-sampling level.

We started using this on a classification pipeline that reads incoming support emails, extracts a category, urgency flag, and suggested next action. Before structured outputs, roughly one in fifteen responses needed a retry because the model invented an extra field or returned urgency as a string instead of a boolean. After switching, zero parsing failures across several thousand calls over two months. The integration code shrank noticeably because we could delete every defensive coercion function we'd built.

The limitation worth knowing: your schema must be fully specified upfront, so it works best for predictable, well-defined outputs rather than open-ended generation. If your schema is deeply nested or has many optional branches, you may hit latency increases. But for the majority of cases where you're asking an LLM to fill a known shape — extraction, labelling, routing — this is the feature that makes LLM integrations feel like calling a normal typed API. If you're still writing regex to clean up model responses, switch to schema-constrained output and reclaim that code.

A single Postgres connection pool saved us more downtime than any monitoring tool we added last year

Small teams running on managed Postgres (whether it's Supabase, Railway, Neon, or a plain RDS instance) hit the same wall eventually: the database has a hard connection limit, and every serverless function, background worker, and admin panel opens its own connection. On a 100-connection tier you can exhaust the pool with surprisingly little traffic, especially during a deploy when old and new instances overlap. The error messages are unhelpful — timeouts, vague 'too many clients' logs — and the instinct is to throw money at a bigger tier.

What actually fixes it is sticking a connection pooler like PgBouncer in front of Postgres in transaction mode. Each application connection borrows a database connection only for the duration of a query, then hands it back. A service that held five connections open while doing almost nothing now shares one or two with everything else. Supabase ships a built-in pooler on a separate port; for other hosts you can run PgBouncer as a tiny sidecar container. The configuration is roughly fifteen lines. The one gotcha worth knowing: transaction-mode pooling breaks features that depend on session state, like prepared statements or LISTEN/NOTIFY, so check whether your ORM leans on those.

If your stack touches managed Postgres and you haven't explicitly configured a pooler, do it this week. Point every application service at the pooler endpoint instead of the direct database port, set a sensible pool size per service (start low, five or ten), and watch your connection count on the database side drop dramatically. It is the kind of infrastructure change that removes a whole category of 3 a.m. alerts.

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