Princess Cosmetics runs a successful Shopify storefront in Qatar. Orders were real; dispatch was not. Staff copied order details into WhatsApp, drivers called back for addresses, and nobody had a single view of what was out for delivery. The client did not need a new ecommerce platform. They needed operations software that respected what Shopify already did well.
We had ten weeks. Rebuilding catalog, checkout, or inventory inside Flutter would have burned half that timeline. The architecture we shipped keeps every product, price, and stock count in Shopify while Supabase holds dispatch rows, user accounts, and row-level access for staff versus drivers.
Splitting commerce from dispatch
- Shopify: catalog, checkout, payments, inventory source of truth
- Signed webhooks: orders/create and orders/updated normalized into dispatch rows
- Supabase Postgres: operational state including assigned driver, pickup time, delivery proof
- Supabase Auth and RLS: staff see all shop orders; drivers see only assigned rows
- Flutter: two role-specific flows from one codebase (staff assign, driver confirm)
- Firebase FCM: push when an order is assigned or status changes
Webhook verification (non-negotiable)
Every Shopify webhook hits a Supabase Edge Function that verifies HMAC before parsing JSON. Unsigned payloads are rejected immediately with no dispatch row and no side effects.
export async function handleShopifyOrder(req: Request) {
const rawBody = await req.text();
const hmac = req.headers.get('x-shopify-hmac-sha256');
if (!verifyShopifyHmac(rawBody, hmac, SHOPIFY_SECRET)) {
return new Response('Unauthorized', { status: 401 });
}
const order = JSON.parse(rawBody);
await supabase.from('dispatch_orders').upsert(normalizeOrder(order));
return new Response('OK', { status: 200 });
}Row-level security instead of app-layer checks
Drivers must never see another driver's route because a Flutter conditional failed. Supabase RLS policies enforce access in Postgres on every select and update. Staff policies scope to shop_id. Driver policies require assigned_driver_id equals auth.uid(). A leaked API key still cannot exfiltrate another shop's orders.
- Staff role: read and update all dispatch_orders where shop_id matches JWT claim
- Driver role: read and update rows where assigned_driver_id equals auth.uid()
- Anon: zero row access even with a session token missing role claims
Realtime without polling the order list
When staff assigns a driver, the driver's phone should alert immediately, not refresh on a timer. Flutter subscribes to Supabase Realtime postgres changes filtered by driver ID.
supabase
.channel('driver-orders')
.onPostgresChanges(
event: PostgresChangeEvent.all,
schema: 'public',
table: 'dispatch_orders',
filter: PostgresChangeFilter(
type: PostgresChangeFilterType.eq,
column: 'assigned_driver_id',
value: currentUser.id,
),
callback: (_) => ref.invalidate(orderListProvider),
)
.subscribe();What went live
- Android app on Google Play with staff and driver flows
- Shopify orders flowing into dispatch within seconds of checkout
- Role-based access enforced at the database, not just hidden buttons
- Push notifications on assignment and delivery confirmation
- Ten-week timeline from kickoff to production riders
Shopify owns commerce. Supabase owns auth, dispatch state, and realtime. Flutter owns the mobile UX. That separation is how we delivered a production delivery stack without reinventing the storefront the client already trusted.



