At CoralShades, I built CurryDash: a marketplace for Sri Lankan home cooks and small restaurants in Australia. It runs two ordering models side by side. Customers can subscribe to a weekly curry pack, or order on demand. Both draw on the same vendor catalogue and delivery windows. CurryDash is built. It has not launched.
The problem
Home cooks already sell curry packs through social media groups and chat threads. That works while the circle is small. It stops working when demand grows: there is no way to find new cooks, payment is a bank transfer, and nobody can track an order.

The big delivery apps don't fit either. A cook who makes a batch every Friday needs customers to commit in advance. On-demand delivery alone can't plan a batch. So the platform had to run both models at once:
- A subscription engine that creates orders on a weekly, fortnightly or monthly cycle
- Real-time on-demand ordering, with live status for the customer
- Split payments, so the platform commission comes off before the vendor payout
- Menus built the way these cooks sell: a pack price, then choices for protein, spice level and sides
- Separate sign-in and permissions for customers, vendors and admins
How it fits together
Two order paths, one platform
Connections: Flutter apps to Laravel API; Laravel API to Subscription engine (subscription orders); Laravel API to On-demand orders (on-demand orders); Subscription engine to Stripe Connect; On-demand orders to Stripe Connect; Laravel API to MySQL and Redis; Cloud Run to Laravel API (hosts).
Two order models
The two order types share a catalogue, but almost nothing else. Their states, triggers and side effects differ.
- Subscription: draft, confirmed, preparing, packed, delivered
- On demand: pending, accepted, preparing, dispatched, delivered
An early design used one state machine with a type flag. I dropped it, because every transition filled up with conditions. Instead there is a base Order model with the shared fields, and two subtypes, each with its own state machine, listeners and jobs. Notifications, payments and reviews point at either type through Laravel's polymorphic relations. Two code paths to maintain, but each one is short and testable on its own.
Packs, not menu items
Vendors don't sell single dishes. They sell configurable packs, so the catalogue is a three-level tree: package, configuration, option.
Package (Family Curry Pack, $45)
├── Configuration: "Choose your protein" (min 2, max 3)
│ ├── Option: Chicken Curry (+$0)
│ ├── Option: Fish Curry (+$3)
│ └── Option: Prawn Curry (+$5)
├── Configuration: "Select spice level" (min 1, max 1)
│ └── Options: Mild | Medium | Hot
└── Configuration: "Add sides" (min 0, max 4)
└── Options: Dhal | Sambol | Papadum | Roti
The price is the pack price plus the selected options. Building this tree from day one avoided a schema migration later; a generic "item plus add-on" model would not have held it.

What a vendor does to go live
- 1RegisterBusiness profile and food safety documents, uploaded through the self-service portal.
- 2Connect payoutsLink a bank account through Stripe Connect.
- 3Build packsSet up packages, configurations and options, plus delivery days.
- 4Take ordersSubscription volume shows first, so the week's batch can be planned. Live on-demand orders sit below it.
Payments
When an on-demand order is confirmed, Stripe Connect splits the platform commission from the vendor payout in the same transaction.
public function createOrderPaymentIntent(
StripeClient $stripe,
OnDemandOrder $order,
): PaymentIntent {
$vendor = $order->vendor;
$totalCents = (int) ($order->total_amount * 100);
$commissionCents = (int) ($totalCents * $this->platformCommissionRate);
return $stripe->paymentIntents->create([
'amount' => $totalCents,
'currency' => 'aud',
'application_fee_amount' => $commissionCents,
'transfer_data' => [
'destination' => $vendor->stripe_connect_account_id,
],
'metadata' => [
'order_id' => $order->id,
'order_type' => 'on_demand',
],
]);
}What I'd keep
A monolith was the right call for a small team. Laravel's service providers and domain repositories give each area its own boundary without separate deployments, and any one area can be split out later if it needs to scale on its own.
The hardest design work was the vendor dashboard. A home cook thinks in batches; a restaurant thinks in live orders. The dashboard puts the subscription forecast at the top and the live queue below, clearly separated.
Need a marketplace or multi-vendor platform?
Split payments, subscriptions and live orders are a lot to hold in one system. If that's your problem, book a call.
Book a call (opens in a new tab)

