My Fruitopia
A morning’s picking, ordered before the cut-off and followed to the door.
iOS · Android

Today’s list 
Live tracking 
Product detail 
My orders
Selling fruit and vegetables by the day is not retail. What exists changes every morning, it is sold by the kilo or by the piece, and it has to reach an office desk or a front door while it is still worth buying. A catalogue is the wrong shape for that. A list for one day is the right one.
Product definition, interface design, application architecture, the Flutter app for both the customer and the seller, the Supabase backend and data model, the checkout, stock and delivery rules, the live tracking, notifications — and the release work in progress.
- App Store
- Google Play
Both listings are being published under the app’s current name. The app itself is built and running.
- iOS
- Android
- Application
- Flutter
- Dart
- Cubit
- go_router
- get_it
- Clean Architecture
- Backend
- Supabase
- PostgreSQL
- Row Level Security
- Postgres RPC
- Supabase Realtime
- Infrastructure
- Firebase Cloud Messaging
- Supabase Edge Functions
- Supabase Storage
- Product strategy
- UI/UX design
- Design system
- Technical architecture
- Mobile development
- Backend development
- Database design
- Integrations
- Testing & QA
- Deployment
- Maintenance
What it does
Each part was built for a real job on the day, not for a feature list. The product’s own site walks through them in full.
The day’s list
The seller publishes what actually came in that morning, with its delivery date, its cut-off time, the regions it reaches and the delivery fee. Nothing on the list is out of stock, because the list is the stock.
Priced in its own unit
By the kilo, the half-kilo, in 100g steps, or by the piece, box, pack, dozen or bundle. Each item is priced and ordered in the unit it is really sold in rather than forced into one.
Ordering that closes
The cut-off and the delivery date are on screen from the first tap. Once the deadline passes the list stops being offered, so nobody fills a basket for a van that has already left.
One order per delivery day
Ordering again for the same day tops up the order already open instead of starting a second one — one delivery for the customer, one box for the seller, and one line per item on the packing list.
Home or office
A delivery region, and an office building where that is the destination. Chosen once and remembered, and the most recent checkout is the one that decides where a merged order goes.
Live delivery tracking
Once the van is out: its position on a map, the stops still ahead, and an estimate that moves as it moves — all without a refresh.
Order history
Active and delivered orders with their items, totals and dates, so ordering the usual again takes seconds.
Push notifications
The day’s list going up, an order confirmed, a delivery starting — told rather than checked for. The seller chooses which alerts they want.
Broadcasts
A message to every customer in a chosen region, written and sent from the seller’s phone.
Inventory & cost prices
Products, photographs, categories and cost prices, kept behind the list the customer sees.
Route planning
The day’s orders gathered into a route by region and building, then worked through stop by stop with progress saved as it goes.
Revenue & margin
Takings, profit measured against the cost price captured at checkout, and what actually sold.
One basket, traced
A customer puts a mango in their basket and taps through checkout. This is everything that happens between that tap and the van reaching their door.
Read from supabase_checkout_data_source.dart, place_customer_order in supabase/schema.sql, and order_tracking_cubit.dart. Every name below appears in the source exactly as written.
- 01
The list is the stock
Each delivery day the seller publishes one list: what was picked, what it costs, how much is left, which regions it reaches, the delivery fee and the hour ordering closes. The customer’s home screen is that row and the items hanging off it — not a catalogue filtered down to whatever happens to be available.
- daily_lists
- daily_list_items
- order_deadline
- delivery_fee
- 02
Ordering closes on the clock
The deadline and the delivery date are on screen from the first tap. A list whose deadline has passed is skipped when the storefront is fetched, so it stops being offered rather than failing at the end. Nobody fills a basket they will not be allowed to submit.
- order_deadline
- delivery_date
- 03
Checkout is one call, not an insert
The app never writes to the orders table. It hands the whole basket to a single security-definer function and lets the database decide what happens to it — which is what makes the order, its lines and the stock either all done or all not done.
- place_customer_order
- security definer
What it says when it refuses
“Not authenticated”. The function reads the customer from auth.uid() and refuses outright if there is nobody there, whatever the app sent with the request.
- 04
Two taps cannot split one order
Before anything is read, the transaction takes an advisory lock keyed on the customer and the delivery date. Two checkouts racing for the same day would otherwise both see no open order and insert one each — the very split the function exists to prevent. The second now waits, then finds the first.
- pg_advisory_xact_lock
- 05
An open order is topped up, never duplicated
A pending, unpaid order for the same seller and the same day is the one to add to; anything delivered or already settled is closed business and starts a fresh one. The same product ordered twice tops up its existing line rather than repeating it, so the packing list stays one entry per item — and the address follows the latest checkout, because one merged order is one drop-off.
- orders
- order_items
- fulfillment
- payment
- 06
Stock comes off the list as it goes
Every line decrements the day’s list, floored at zero so a race cannot drive it negative. The “only 8 left” the next customer reads is the same number this order has just moved.
- decrement_daily_list_stock
- daily_list_items
- stock
- 07
The totals are recomputed, never patched
Once the lines are in, the subtotal is summed back off the order’s own rows and the delivery fee added once. A merged order can never disagree with what it actually contains, and delivery is never charged twice.
- subtotal
- delivery_fee
- total
- 08
The seller hears about it
Inserting the order fires one trigger and topping it up fires another, each writing a notification — but only if the seller has that alert switched on. That row’s insert then calls an edge function with nothing but its id: a forged request cannot choose the recipient or the wording, because the function reads both from the table itself.
- notify_seller_new_order
- on_order_topped_up
- notifications
- push-on-notification
- 09
Then the customer watches it come
The tracking screen subscribes to two Postgres change streams filtered to that seller — the van’s position and the route’s progress. The map moves as the van moves, and a completed stop shortens the estimate immediately rather than at its next significant move.
- seller_locations
- delivery_progress
- onPostgresChanges
Engineering detail
Taken from the project source, not from marketing material.
- Architecture
- Feature-first Clean Architecture with Cubit state management, go_router shells and a get_it service locator; the domain layer is pure Dart and returns a Result value rather than throwing across the boundary
- Backend
- Supabase — PostgreSQL with row-level security, security-definer functions for checkout and stock, and an edge function that sends the push
- Data model
- 18 tracked migrations on top of the base schema, covering daily lists, delivery routes and progress, cost prices, delivery fees, cart reminders and payment method
- Realtime
- Postgres change streams on seller_locations and delivery_progress drive the customer’s map; the van’s position and the stops ahead update without a refresh
- Device capabilities
- Map rendering (flutter_map), device location (geolocator), image capture and cropping for product photographs, local notifications alongside push
- Release
- Version 0.0.15, build 16 at the time of writing; one Flutter codebase for iOS and Android, English and French, light and dark
A morning’s picking, ordered before the cut-off and followed to the door.
