Diminished Returns and Launch Surprises
The last ten days have been productive. Real productive - shit got done! The kind where you look back and realise you actually shipped things rather than just worked on them. AudiBar got its first sale. Chronode launched and immediately revealed bugs I'd never seen in testing. BillKit went from "I should deal with those invoices" to a functioning native app with AI processing.
But productive doesn't mean smooth. Expensive problems arose, the kind that don't show up in tutorials or documentation. The kind you only learn by actually putting code in front of real users and real scenarios.
This is what the last ten days taught me.
Diminished Returns
The first AudiBar sale notification came through whilst I was working on something else. A small dopamine hit. Someone had actually paid money for something I'd built. I opened App Store Connect to check the details.
Then came that 'WTF' moment.
Where was the rest of the money? The app was priced at £2.99. I expected Apple's 30% cut. I knew about that. What I didn't expect was to see 20% VAT removed first, then another 30% taken from what remained. After both deductions, I was left with roughly 50% of the original price.
For all that work. All those hours. All that learning. Half the money gone before it even reached me. I stared at the screen. Did a bit of swearing. Checked the numbers again. They didn't change.
Economics of App Store Sales
I'm a sole trader in the UK. I don't collect VAT yet. My revenue isn't high enough to require registration. What I didn't realise was that Apple collects tax on all payments for all products, regardless of your tax status or where you're based. Whether you're VAT registered or not, Apple collects it on your behalf. Then they take their 30% commission on top.
This isn't a complaint about Apple's policies. It's just the reality of selling through the App Store. I just hadn't done the maths properly before shipping.
The calculation looks like this:
- Customer pays: £2.99
- VAT removed (20%): £0.60
- Remaining: £2.39
- Apple's cut (30%): £0.72
- What I receive: £1.67
That's 56% of the sale price. For a £2.99 one-time purchase, losing nearly half feels significant.
Apple Small Business Program
I discovered Apple has a Small Business Program that reduces their commission to 15% if you earn less than $1 million per year. I applied immediately. At the time of writing, I haven't heard back. If I'm not accepted, I'll need to reconsider pricing. Not because I want to charge more, but because the economics need to make sense for continued development and support.
This was another lesson about the difference between building and shipping. Building teaches you Swift, SwiftUI, and Core Data. Shipping teaches you business reality, pricing strategy, and tax implications. You can't learn the second one without doing the first one.
Production Is Different
Chronode launched on my website as a self-published app, and I immediately started using it for actual work. Not testing. Not simulated usage. Real development sessions tracking real projects.
Within hours, I'd found multiple issues that hadn't appeared during any amount of testing:
The add app view was completely squashed, making apps unselectable. The "requires accessibility" alert appeared on every launch instead of just once. Icon loading occasionally showed the Chronode icon instead of the actual application icon. Sublime Text 3 and 4 project detection wasn't working correctly.
None of these surfaced during development. They only appeared when I used the app naturally, in my actual workflow, without thinking about testing scenarios.
Rapid Response
Version 1.0.1 shipped within hours of 1.0. Not because I was panicking or rushing. Just because I'd found bugs that needed fixing, and there was no reason to wait. I'm glad I caught them before anyone else potentially downloaded a broken version.
This happens with every app. AudiBar 's first update fixed iCloud Drive file loading issues, scrolling animations for long track names, and playlist loading protection. These weren't theoretical problems I anticipated. They were real issues I encountered whilst using the app.
Production usage reveals things testing never will. You click differently. You navigate differently. You encounter edge cases that test plans don't cover. Using your own software is the most effective quality assurance process available.
Zero-Payment Edge Cases
Before launching Chronode , I wanted to verify the complete purchase flow in production. I'd tested everything in Stripe's test mode, but I wanted absolute certainty that the live environment worked correctly.
I created a 100% discount code. One-time use. I'd "buy" my own app for free, confirm the license generation worked, and be confident in the system. The checkout completed. No payment taken, obviously. Then the webhook fired to process the purchase and generate a license key. The job failed immediately.
When Assumptions Break
Stripe doesn't create payment intents for free purchases. It also doesn't ask for your name during checkout when there's no payment. Both of these were assumptions my code made. The webhook expected a payment intent ID and a customer name. Neither existed.
It was an "are you serious?" moment. Thinking back, it makes complete sense. Why would Stripe create a payment intent when no payment is being processed? Fair enough.
I added code to handle free purchases: generate a synthetic payment intent ID, set a default name if none was captured, proceed with license generation. Tried again. Everything worked perfectly.
This is another truth that shipping reveals. Testing catches logical errors. Production catches assumptions. I'd tested the payment flow extensively, but I'd never tested what happens when someone pays nothing. Why would I? Except now I needed to support discount codes, including 100% discounts, and my assumptions broke.
You learn these lessons by shipping. Not by building more carefully or testing more thoroughly. By putting real code in front of real scenarios, even if the real scenario is your own attempt to test the system.
123 Invoices and a Decision
Earlier in the year, I started building BillKit , a Laravel web application to be offered as a SaaS product with invoice processing and AI extraction. It was about halfway finished when other priorities took over. I was still working full time during its development, and I just couldn't get back to it.
Then I started learning Swift in June. Kanodo came first, then Chronode and AudiBar . All three apps taught me different aspects of macOS development: audio file handling, application tracking, Core Data, and SwiftUI patterns. By late October, I'd built three apps from scratch and learnt a significant amount about native development.
Folder Forced Decisions
Late October arrived. Four days left in the month. I looked at my expenses folder. 83 invoices for 2025. 40 more for 2024. All sitting there, unprocessed. I had no idea how much I'd spent in the last year. I had no idea what I'd spent it on. I had the invoices and receipts, but no organised record of the data they contained.
I could have waited until tax time to calculate everything manually. That was the plan. But I wanted to know sooner. I wanted to understand my spending by category. I wanted to see if I needed to adjust budgets. The information was there in those files. I just needed to extract it.
Rethinking the Approach
BillKit was originally intended as a web app following the usual model. That made sense when I was thinking about it as a product to sell to others. But now, looking at my own 123 invoices and needing a solution immediately, the existing options all required monthly subscriptions.
QuickBooks, Xero, FreshBooks and the like all charge ongoing fees for feaatures I didn't need. I didn't want to pay someone else monthly just to manage my own data whilst I had a half-finished solution sitting there. With four months of Swift experience, I realised I could build what I needed as a native app instead - local data, offline capability, and complete control.
With four months of Swift experience and two shipped apps behind me, I realised I could build what I needed as a native macOS app. Not eventually. Not after finishing the Laravel version. Now.
I had four days left in October before November arrived. Those were days I could have spent getting back to Kanodo , but I decided to see what I could do with BillKit instead. Most of the core development happened in that initial four-day sprint, but I'm writing this on November 4th. That's four more days into the next month. Eight days total so far, and it's not completely finished yet. But I'm okay with that. I don't mind spending another week on it to get it right.
The point wasn't to bang out an app in four days. The point was that I had a real problem, I had the skills to solve it, and I could build something useful instead of paying someone else monthly fees for a service I didn't fully need.
Scratching your own itch works because you're the user. You know the pain. You know what matters and what doesn't. You're not guessing about features or designing for hypothetical users. You're building exactly what you need, and if others find it useful too, that's a bonus.
Pivot or Persist, Again
My previous post was about persistence versus pivoting. About knowing when to push through obstacles and when to change direction. That lesson applied again during BillKit 's development, just in a different context.
I wanted table columns to be reorderable. Like Finder. Drag a column header to a new position, and the table rearranges. I also wanted proper column alignment. Monetary values right-aligned. Status indicators centred. Text left-aligned. These felt like basic table functionality.
Implementing column reordering broke sorting. Click a column header to sort by that column. Should be straightforward. But SwiftUI's Table view has a known issue where custom column ordering interferes with built-in sorting. You can have one or the other. Not both.
Adding alignment modifiers to TableRow broke sorting as well. Another known issue. The moment you specify .alignment() on a column, the sorting functionality stops working.
I had to choose: column reordering and custom alignment, or column sorting.
Choosing What Matters
Sorting won. Users need to sort expenses by date, amount, and vendor - that's fundamental functionality. Reordering columns is nice, and right-aligned monetary values look professional, but neither is essential. Sorting is.
This is what shipping forces you to do. Building in isolation, you might spend days trying to work around the SwiftUI bugs. Finding hacky solutions. Implementing custom sorting. Shipping forces prioritisation. What do users actually need versus what would be nice to have?
The decision was easy once framed correctly. Keep the functionality that matters. Pivot away from the polish that doesn't.
3.5 Apps in 4 Months
It's actually four apps since August, but Kanodo isn't quite finished yet, so I'll call it 3.5. Kanodo in September, AudiBar and Chronode in October, and BillKit 's core complete in early November. Kanodo is nearly done and planned for launch by end of year.
Sometimes, in the evenings, when I'm trying to relax, my mind wanders back to what I was working on during the day. Doubts creep in. Did I rush it? Am I just trying to bang out as many apps as possible? Quantity versus quality?
Reality Checks
Then I look at what's actually been built. Every app has comprehensive unit tests, proper error handling, polished interfaces, documentation, and marketing websites. They all solve specific problems properly.
AudiBar handles audio file playback from the menu bar. That required learning audio APIs, file handling, and background processing. Chronode tracks time automatically by monitoring other applications. That required learning accessibility APIs, application observation, and background daemon architecture. BillKit processes invoices with AI extraction. That required learning vision language model integration, structured data parsing, and multi-provider architecture.
Every app taught something different and had its own technical challenges that required learning new APIs and patterns. Yes, there are similarities. Services, models, views, extensions follow similar patterns across all of them. But there are many nuances that didn't port from one to another.
This isn't rushing. This is building real products and shipping actual software that people use. The confidence comes from shipping, not just from building. Every launch teaches lessons that the next launch benefits from.
Quality Over Speed
I don't think "right, build this app in three days then publish to the App Store." That would be rushing. That would be pushing crap to the App Store. I take the time to add unit tests, add polish, make sure everything works properly. Plus the websites to market and document them. Plus this blog and projects section. I'm way beyond just rushing things out the door.
The quality is there. The difference is that I ship when something is ready, rather than pushing crap to the App store because it's "Good enough" and I want to move on to the next project.
Same Lesson, Different Examples
Last time I wrote about how technical challenges are rarely the hardest part. How discovering Chronode couldn't be sandboxed after building it, navigating EU compliance, and creating payment systems from scratch weren't coding problems. They were business problems I didn't know existed until I hit them.
This round proved the pattern holds. Different problems, same reality.
The 50% App Store economics lesson. The zero-payment edge cases in Stripe. The bugs that only appear during real usage. The TableColumn sorting pivot. These weren't things I could have learnt by reading documentation or watching tutorials. They only surfaced by actually launching products and dealing with real scenarios.
Building Gets You to the Starting Line
Building taught me Swift, SwiftUI, Core Data, and macOS APIs. That foundation is essential. But the App Store fee structure, production edge cases, architectural pivots under pressure, and pricing strategy adjustments? Those only came from putting real code in front of real situations.
The technical work gets you to the starting line. Everything after that? You learn by doing, by launching, by dealing with what actually happens rather than what you planned for.
It's been less than two weeks since that last post. Two launches later (almost three), the lesson hasn't changed. It's just accumulated more expensive examples.
Onwards and Upwards
The technical work continues, though. BillKit needs search functionality, data export, dashboard reports, and an expense details sheet before it's ready for the App Store. Kanodo needs its StoreKit migration, final testing, and all the marketing materials that come with a proper launch. There's plenty left to build.
But the expensive lessons are done. At least until the next launch reveals new ones, because there's always something you didn't see coming.
Until next time, I'm back to the code. Ciao!