Own Courier Delivery Implementation
A proprietary courier service is a logical step for stores with high delivery volumes in a limited geography. When reaching a threshold of around 50–100 deliveries per day, in-house courier costs become lower than external service commissions. A courier management system is a separate product within the platform with a mobile app for couriers, a dispatcher panel, and real-time tracking.
System Architecture
The system consists of three interconnected components:
Backend: REST API + WebSocket server for real-time coordinates Dispatcher Panel: web interface for courier and route management Courier Mobile App: React Native or PWA for task receipt and GPS coordinate transmission
Data Model
couriers (
id, user_id, name, phone, vehicle_type,
status: offline | available | busy | on_break,
current_lat, current_lng, last_seen_at,
zone_ids (jsonb) -- delivery zones the courier is responsible for
)
delivery_tasks (
id, order_id, courier_id (nullable),
status: pending | assigned | picked_up | in_transit | delivered | failed,
pickup_address, delivery_address,
scheduled_at, picked_up_at, delivered_at,
proof_photo_url, -- photo at delivery
recipient_signature_url, -- recipient signature
failure_reason
)
courier_routes (
id, courier_id, date,
task_ids (jsonb, ordered), -- delivery order
total_distance_km, estimated_duration_min
)
Courier Assignment
Manual assignment — dispatcher drags the order to a courier on the map. Suitable for small volumes.
Automatic routing — Vehicle Routing Problem (VRP) algorithm. For small volumes, a greedy algorithm works (nearest available courier to the pickup point). For serious optimization, libraries like Google OR-Tools or services like Route4Me API and OptimoRoute.
# Simple heuristic: assign the nearest available courier
def assign_courier(task):
available_couriers = Courier.objects.filter(status='available')
nearest = min(available_couriers,
key=lambda c: haversine(c.location, task.pickup_address))
task.assign(nearest)
Courier Mobile App
Main screens:
- Task List — current route, next point
-
Navigation — integration with Yandex.Navigator via deep link:
yandexnavi://map_search?text={address} - Delivery Confirmation — delivery photo, signature, or confirmation code from SMS to customer
- Failed Delivery — reason selection: door not opened, rescheduled, refused
GPS coordinates are sent to the server every 30 seconds via WebSocket or MQTT. If internet is lost, they are buffered and sent upon reconnection.
// Sending coordinates via WebSocket
const ws = new WebSocket(`wss://api.example.com/couriers/${courierId}/location`);
navigator.geolocation.watchPosition((position) => {
ws.send(JSON.stringify({
lat: position.coords.latitude,
lng: position.coords.longitude,
accuracy: position.coords.accuracy,
timestamp: Date.now()
}));
}, null, {enableHighAccuracy: true, maximumAge: 30000});
Customer Tracking
The customer receives an SMS/push with a link to the tracking page. The page shows a map with the courier's real-time location and expected arrival time. ETA is recalculated every 2 minutes based on the courier's current coordinates and distance to the delivery point.
Dispatcher Panel
- Real-time map with couriers (Yandex.Maps or Leaflet)
- List of unassigned orders
- Drag-and-drop courier assignment
- Task queue for each courier with reordering capability
- Monitoring: courier hasn't moved in 20 minutes → alert to dispatcher
- Route history and daily efficiency
Delivery Zones
Geographical zones restrict the area where a courier operates. Zones are drawn as polygons on the map and stored in PostgreSQL with PostGIS extension:
delivery_zones (
id, name, courier_ids (jsonb),
polygon geometry(Polygon, 4326),
min_order_amount, delivery_price, free_delivery_from
)
Query to determine which zone an address is in:
SELECT * FROM delivery_zones
WHERE ST_Contains(polygon, ST_SetSRID(ST_MakePoint(lng, lat), 4326));
Handling Failed Deliveries
If the courier couldn't find the recipient, the status is set to failed with a reason. A retry delivery is automatically scheduled for the next day, or the customer receives a notification with the option to choose a new time.
Development timeframe: 8–12 weeks for a complete system: API, dispatcher panel, and courier mobile app.







