Back to Blog
Building a Butchery Ordering App with Flutter & Firebase — What I Learned
After several weeks of late nights and a lot of coffee, I just shipped the Butchery Ordering App, a full cross-platform mobile application built in Flutter that lets customers browse cuts, place orders, schedule deliveries, and pay, all from their phone. It's live, it's in customers' hands, and I learned an enormous amount along the way. This post is an honest account of how I built it, the decisions I made, the things that went wrong, and what I'd do differently if I started over today.
The client — a local butchery — was taking orders over WhatsApp. Literally. Customers would send photos of handwritten lists, the owner would type replies, deliveries were tracked in a notebook, and payments happened via mobile money with no receipt trail.
It worked. Until it didn't. Lost messages, double orders, missed deliveries. The business was growing and the process wasn't scaling. They needed something modern but didn't want complexity. My job was to digitise the order flow without overwhelming a non-technical team.
The Stack
After evaluating the options I landed on:
Flutter + Dart — single codebase for Android and iOS
Firebase Firestore — real-time database for orders and inventory
Firebase Auth — phone number authentication, since most customers don't use email
Node.js + PostgreSQL — backend API for admin operations, reporting, and webhooks
Flutterwave — payment gateway with mobile money support for Zambia
Architecture Decisions
Why Flutter over React Native?
I've shipped React Native before. The JavaScript bridge has improved a lot but I wanted true native rendering without the overhead. Flutter's widget tree gives you pixel-perfect control and the performance on mid-range Android devices — which most of my client's customers use — is noticeably better. The Dart learning curve was real. Coming from Python and PHP it felt restrictive at first. By week two it felt like a superpower.
Why Firebase for the client-facing layer?
Real-time sync was non-negotiable. When the owner marks an order as "Out for delivery", the customer needed to see that immediately without polling. Firestore's onSnapshot listeners made this trivial to implement. The downside is that Firestore's querying is limited — no complex joins or aggregates. This is why I kept PostgreSQL in the picture for the admin reporting layer. Things like "revenue by cut type this month" live there.
BLoC over Provider for state management
I chose the BLoC pattern. It's more boilerplate than Provider or Riverpod, but the explicit separation of events, states, and business logic made the codebase easy to reason about as it grew. When someone else opens the project, they can look at any BLoC file and understand exactly what triggers what. That's worth the extra lines.
Things That Went Wrong
1. I underestimated offline support
Flutter and Firebase give you optimistic UI out of the box, but I didn't think carefully enough about the offline edge cases specific to this app. What happens when a customer places an order on a bad connection and the Firestore write queues? The UI says success. The order processes 20 minutes later when they get signal. By then the item might be out of stock.
My fix was server-side stock validation via a Cloud Function that fires on every new order document. If stock is insufficient, the order status flips to REJECTED and a push notification fires immediately. Not perfect, but it handles the case.
Lesson: design for the network conditions your actual users live in, not your office wifi.
2. Phone auth was trickier than expected
Firebase phone auth sounds simple — send OTP, verify, done. In practice, Zambian phone numbers have formatting variations, some carriers delay SMS delivery, and users on older phones sometimes never receive the OTP at all. I ended up building a retry flow with a countdown timer and a WhatsApp OTP fallback via Twilio for users whose SMS just wasn't arriving.
Lesson: authentication is never as simple as the docs make it look. Budget double the time.
3. The admin panel was an afterthought
I built the customer-facing app first and left the admin panel for later. Big mistake. The data model I chose made sense for the mobile app but created awkward queries on the admin side. If I did this again I would build the admin reporting views first. They reveal the real shape of your data.
What Went Well
Flutter's widget system is genuinely great. Building the product catalogue UI — animated category tabs, a custom bottom sheet for product detail, a smooth cart drawer — took about three days. The same in React Native would have taken a week and felt less polished.
Firebase Cloud Functions saved me. Sending a confirmation SMS when order status changes, generating a PDF receipt on payment success, nightly stock summary emails to the owner — all of these are clean isolated functions triggered by Firestore document changes. Zero infrastructure to manage.
The client uses it every day. This is the one that matters. The owner, who had never used an app like this, was placing test orders herself within 20 minutes of the demo. That's the real metric.
What I'd Do Differently
Start with the data model. Spend a full day on it before writing any UI.
Build the admin panel in parallel, not after. It forces better data design.
Test on real devices from week one. The emulator lies about performance.
Next time I'd look more closely at Stripe where the market supports it — the developer experience is significantly better than most African-market alternatives.
Final Thoughts
This was one of the most satisfying projects I've shipped — not because of the technology, but because it solved a real problem for a real person. The owner's WhatsApp is now just for family. Orders come in on the app.
That's what software is supposed to do.
If you're working on something similar or have questions about the Flutter and Firebase architecture, reach out. I'm always happy to talk through the details.
← More Articles
Last updated: May 02, 2026