Per-seat pricing feels like the obvious default for a B2B SaaS product. It scales with the customer, it's easy to explain, and every second competitor does it. But if you're building for small South African businesses — a logistics company with eight drivers, a clinic with rotating staff — per-seat pricing creates a specific, predictable problem: your customers will share a single login to avoid paying more. You'll see one account with sessions from four different devices in three cities, and your usage metrics will tell you a story that has nothing to do with reality.
This matters beyond lost revenue. Shared logins wreck your audit trail, break any feature that relies on knowing who did what, and make onboarding data meaningless. You can't tell which user abandoned a workflow and which one never started. You end up designing around a ghost user who behaves like four different people, and your product decisions get quietly worse because of it.
If your target customer is a team of under fifteen where individuals don't each have a company email address, consider pricing on something they can't share: active projects, transactions processed, locations managed, or a flat tier based on usage volume. You still capture expansion revenue as the business grows, but you remove the incentive to cram everyone behind one password. Before you lock in your pricing model, watch how your pilot users actually log in. If three people are already sharing credentials during your beta, per-seat pricing isn't a revenue strategy — it's a fiction you're choosing to believe.
That layout shift when your web font loads is costing you more than it looks
Cumulative Layout Shift (CLS) is one of those Core Web Vitals metrics that feels abstract until you watch a real user try to tap a button right as a custom font swaps in and the entire page jumps. The text reflows, the button moves twelve pixels down, and the user taps whatever just slid into its place. On content-heavy pages with multiple font weights, we've seen CLS scores balloon past 0.25 — well above Google's 0.1 threshold — purely from font loading. It's not images. It's not ads. It's your lovely brand typeface arriving late.
The fix isn't to abandon custom fonts. It's to control the swap. Start by setting `font-display: optional` instead of `swap` on your @font-face declarations. With `optional`, the browser gives the font a very short window to load; if it misses that window, the fallback stays for the entire page visit. No swap, no shift. The trade-off is that first-time visitors might see your fallback font, but the layout stays rock solid. Pair this with `<link rel="preload" as="font" type="font/woff2" crossorigin>` in your document head so the font file starts downloading before the CSS parser even encounters it. Together, these two changes usually eliminate font-related layout shift entirely.
If brand consistency matters on every single page load, there's a middle path: use the CSS `size-adjust`, `ascent-override`, and `descent-override` descriptors on your fallback font stack to match its dimensions closely to your custom font. This way, even when a swap does happen, the text occupies nearly the same space and nothing jumps. Audit your CLS in Chrome DevTools under the Performance panel — filter for layout shift clusters and check whether fonts are the culprit before optimising images you don't need to touch.
Flutter's platform channel is where your beautiful cross-platform app quietly becomes two native apps
Flutter sells you one codebase for iOS and Android, and for screens, navigation, and network calls that promise mostly holds. Then you need to access the fingerprint sensor, or read NFC, or interact with a Bluetooth receipt printer — and suddenly you're writing Swift on one side and Kotlin on the other, bridging them through platform channels with serialised messages. The cross-platform abstraction doesn't disappear, but it develops a seam right where your app touches hardware.
We've built several Flutter apps that started as pure Dart and gradually accumulated platform channel code for things like background location tracking and local biometric auth. Each channel is its own small maintenance surface: you test it separately, you handle errors differently on each platform, and OS upgrades can break one side without warning. A plugin from pub.dev might paper over this, but popular plugins get abandoned or lag behind OS releases. When that happens, you inherit someone else's platform channel code and maintain it anyway.
If you're scoping a Flutter project, audit which device capabilities you actually need before locking in timelines. List every feature that requires a platform channel and decide early whether you'll use a maintained plugin or write your own bridge. Budget time for testing those channels on physical devices — the iOS simulator and Android emulator both lie about hardware behaviour. The Dart UI being cross-platform doesn't mean the integration work beneath it is free.
The project that took three weeks started with a scope document that took three hours
Most small studio projects that blow their budget don't fail during development. They fail in the first conversation, when everyone nods along to a vague description of what "the app" should do and assumes they're picturing the same thing. We've started treating scope documents less like admin and more like the actual product of our first paid engagement. Three hours of writing down exactly what's in, what's out, and what "done" looks like for each feature has saved us from weeks of rework more times than we can count.
The trick isn't length — it's specificity at the boundary. Nobody argues about the obvious stuff. The fights happen at the edges: does "user management" include role-based permissions or just login? Does "reporting" mean a dashboard or a CSV export? We write those boundaries as explicit yes/no lines. If a client reads the scope and says "wait, I assumed that was included," that's a win. That conversation costs you fifteen minutes now instead of a painful change request in week four.
We charge for scoping as a standalone deliverable, even if the client ultimately builds with someone else. It reframes the relationship from "pitch me for free" to "we're already solving your problem." If you're quoting fixed-price work without a written scope that names every boundary, you're not quoting — you're guessing. Spend the three hours. Make the boundaries boring and obvious. Your future self will be grateful.
The inventory spreadsheet nobody dares touch because only Karen knows the formulae
Every second small business we talk to runs a critical process through a spreadsheet that one person built, one person understands, and everyone else treats like a sacred text. It is almost always inventory or quoting. The formulae reference hidden columns, the VLOOKUPs broke when someone inserted a row in 2021, and there is a tab called 'DO NOT DELETE' that apparently keeps the whole thing from collapsing. Karen is on leave and nobody will touch it.
The instinct is to jump straight to a full inventory system — something off the shelf with barcode scanning and warehouse zones. But most of these businesses are not warehouse operations. They are a team of eight who need to know what is in stock, what has been promised to a client, and what to reorder on Tuesday. The gap between the spreadsheet and a proper system is not features; it is that the logic Karen built was never written down as business rules. Before you shop for software, sit with Karen for two hours and document every rule her spreadsheet encodes. Which supplier gets used first. What the minimum reorder quantity actually is. Why column Q multiplies by 1.15 but only for items in category D.
Once those rules exist outside the spreadsheet, you can evaluate whether a simple Airtable base, a lightweight stock tool with a Xero integration, or even a small custom app is the right fit. The tool choice matters far less than extracting the logic first. If Karen resigns before you do this, you are rebuilding from memory and receipts.