dfw_errand is a two-sided marketplace: customers post errands, drivers pick them up. The app supports both roles in a single binary, switching mode based on a profile flag. Repo: /Users/imzee/projects/appmint_go/dfw_errand/.
It's the cleanest example of using the logistics module end to end, plus dual-role auth in the same app.
What it uses
| Module | Endpoints |
|---|---|
| Profile | /profile/customer/signin |
| Logistics | /client/logistics/init, /jobs, /jobs/:id, /jobs/:id/messages, /quote, /payouts/* |
| Storefront | /storefront/stripe/intent (customer payment), Stripe SDK |
| Repository | /repository/find/job (client-side job history) |
The driver side hits /client/logistics/jobs/available, /jobs/:id/accept, location updates, and the payout endpoints. The customer side hits the regular job-creation, tracking, and payment endpoints.
Project layout
dfw_errand/lib/
├── services/
│ ├── api_service.dart # Generic AppEngine wrapper
│ ├── http_client.dart # Singleton with `useUserToken` flag
│ ├── delivery_service.dart # Logistics endpoints — dual customer/driver
│ ├── finance_service.dart # Payout / earnings (driver)
│ ├── location_service.dart # GPS for driver location updates
│ ├── repository_service.dart # Generic /repository/* CRUD
│ ├── storage_service.dart # JWT + role
│ └── stripe_service.dart # Customer payment
├── providers/
└── screens/
delivery_service.dart is the meatiest file — every customer and driver endpoint wrapped in a typed method. Worth reading top to bottom.
Files to read first
lib/services/delivery_service.dart— the full set of/client/logistics/*calls. Includes asetMode('customer'|'driver')switch that adjusts which endpoints the service uses.lib/services/location_service.dart— usesgeolocatorto push driver coordinates to/client/logistics/locationevery N seconds while online.lib/screens/customer/track_job_screen.dart— polling pattern for status updates (5s interval, stop on terminal state). Mirrors the tracking recipe.lib/providers/app_mode_provider.dart— the customer/driver toggle state, read by service classes.
Patterns worth lifting
Dual-role single binary
The same APK serves both customers and drivers. A single profile field (isDriver) determines which navigation tree the app shows after sign-in. Both sides share auth, the HTTP client, and most plumbing.
// pseudocode
if (user.isDriver) {
Navigator.pushReplacementNamed(context, '/driver-home');
} else {
Navigator.pushReplacementNamed(context, '/customer-home');
}
This avoids two app store listings and two codebases. The trade-off: the app bundle includes screens neither user will see (drivers don't need the customer order placement screen and vice versa). For most marketplaces that's an acceptable cost.
Background location updates
The driver side runs a foreground service while online. geolocator + flutter_background_service keeps location updates flowing even when the app is backgrounded.
Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 25, // meters
),
).listen((position) {
appmintHttp.put('/client/logistics/location', body: {
'lat': position.latitude,
'lng': position.longitude,
'heading': position.heading,
'speed': position.speed,
});
});
A 25-meter filter prevents the device from spamming updates while stopped at lights.
Quote-before-book
Customers can preview a price before booking:
final quote = await appmintHttp.post('/client/logistics/quote', body: {
'pickup': pickupAddr,
'dropoff': dropoffAddr,
'weight': estimatedWeight,
});
// quote.estimatedPrice
This pattern is reusable for any pricing-via-distance service. The endpoint runs the same calculation the actual booking does, so a quote followed by a book at the same prices is a stable contract.
What it doesn't do
- No biometrics — drivers don't want a face scan when they need to accept a job fast.
- No offline cache for jobs — being offline-but-still-displaying-stale-jobs is misleading. Better to surface "you are offline."
- No push for new jobs (yet) — drivers refresh the available-jobs list manually. A future version will add FCM push for instant notification when a matching job appears.
Where to fork from
If you're building a delivery/errand/services marketplace:
- Copy
lib/services/{http_client,api_service,delivery_service,location_service}.dart. - Adapt
delivery_serviceto your domain (replacejobswithtasks,tickets,requests). - Keep the
app_mode_providerpattern — most marketplaces are dual-role.
Reference docs to pair with this example
- Auth — customer-side sign-in.
- Delivery tracking — the polling tracking screen.
- Checkout with Stripe — customer payment.