Real-Time Courier/Order Tracking on Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Real-Time Courier/Order Tracking Implementation on Website

A user places an order and waits for the courier. A "My Orders" page with a "status: in transit" field is old school. The modern standard is a map with a live courier marker and a timer "will arrive in N minutes." Technically, this is a combination of three components: a courier's mobile device, a backend application, and a client browser.

Data Stream Architecture

[Courier Device]
    GPS → POST /api/courier/location every 3–5s
        ↓
[Backend]
    Save to Redis (TTL 30s)
    Publish to Redis Pub/Sub channel order:{id}
        ↓
[WebSocket Server (Laravel Reverb / Pusher)]
    Broadcast event LocationUpdated
        ↓
[Client Browser]
    Update marker on map

Geopositions are not stored in PostgreSQL on every update—that's 720 records per hour per courier. We only write to the database when the order status changes and the final position when completed. Current position stays in Redis with TTL.

Orders Table

CREATE TABLE delivery_orders (
    id             BIGSERIAL PRIMARY KEY,
    user_id        BIGINT NOT NULL REFERENCES users(id),
    courier_id     BIGINT REFERENCES couriers(id),
    status         VARCHAR(50) NOT NULL DEFAULT 'pending',
                   -- pending | assigned | picked_up | in_transit | delivered | failed
    address_lat    DECIMAL(10, 8),
    address_lng    DECIMAL(11, 8),
    address_text   VARCHAR(500),
    estimated_at   TIMESTAMP,
    delivered_at   TIMESTAMP,
    created_at     TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE TABLE delivery_status_log (
    id         BIGSERIAL PRIMARY KEY,
    order_id   BIGINT NOT NULL REFERENCES delivery_orders(id),
    status     VARCHAR(50) NOT NULL,
    lat        DECIMAL(10, 8),
    lng        DECIMAL(11, 8),
    note       TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

Courier API: Location Update

The endpoint is called from the courier's device every 3–5 seconds:

class CourierLocationController extends Controller
{
    public function update(Request $request, DeliveryOrder $order): JsonResponse
    {
        $data = $request->validate([
            'lat' => 'required|numeric|between:-90,90',
            'lng' => 'required|numeric|between:-180,180',
        ]);

        // Current position—only in Redis, with 60-second TTL
        $key = "courier_location:{$order->courier_id}";
        Redis::setex($key, 60, json_encode([
            'lat'      => $data['lat'],
            'lng'      => $data['lng'],
            'order_id' => $order->id,
            'ts'       => now()->timestamp,
        ]));

        // Broadcast to order customer
        broadcast(new CourierLocationUpdated(
            orderId: $order->id,
            lat:     $data['lat'],
            lng:     $data['lng'],
            eta:     $this->calculateEta($order, $data['lat'], $data['lng']),
        ));

        return response()->json(['ok' => true]);
    }

    private function calculateEta(DeliveryOrder $order, float $lat, float $lng): ?int
    {
        // Approximate calculation by straight line—30 km/h average city speed
        $distanceKm = $this->haversineKm($lat, $lng, $order->address_lat, $order->address_lng);
        return (int) round($distanceKm / 30 * 60); // minutes
    }
}

Laravel Event

class CourierLocationUpdated implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public readonly int    $orderId,
        public readonly float  $lat,
        public readonly float  $lng,
        public readonly ?int   $eta,
    ) {}

    public function broadcastOn(): Channel
    {
        return new PrivateChannel("order.{$this->orderId}");
    }

    public function broadcastWith(): array
    {
        return [
            'lat' => $this->lat,
            'lng' => $this->lng,
            'eta' => $this->eta,
        ];
    }
}

PrivateChannel—the client must be authenticated to subscribe. This prevents unauthorized users from subscribing to someone else's order channel.

Channel Authorization

// routes/channels.php
Broadcast::channel('order.{orderId}', function (User $user, int $orderId) {
    return $user->id === DeliveryOrder::find($orderId)?->user_id;
});

Client Side: Map

import mapboxgl from 'mapbox-gl';

mapboxgl.accessToken = 'pk.eyJ...';
const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [orderLng, orderLat],
    zoom: 14,
});

// Delivery address marker
new mapboxgl.Marker({ color: '#EF4444' })
    .setLngLat([orderLng, orderLat])
    .addTo(map);

// Courier marker
const courierMarker = new mapboxgl.Marker({ color: '#3B82F6' })
    .setLngLat([initialLng, initialLat])
    .addTo(map);

// WebSocket subscription
Echo.private(`order.${orderId}`)
    .listen('CourierLocationUpdated', ({ lat, lng, eta }) => {
        courierMarker.setLngLat([lng, lat]);
        if (eta !== null) {
            document.getElementById('eta').textContent =
                eta < 2 ? 'Courier is nearby' : `Will arrive in ~${eta} min`;
        }
    });

Alternative to Mapbox—Yandex Maps API or Google Maps Platform. For CIS countries, Yandex is preferable for geocoding quality and coverage.

Smooth Marker Movement

Abrupt marker jumps every 3–5 seconds look rough. Solution—animation via requestAnimationFrame:

function animateMarker(marker, from, to, duration = 500) {
    const start = performance.now();
    function step(now) {
        const t = Math.min((now - start) / duration, 1);
        const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; // easeInOut
        const lng = from[0] + (to[0] - from[0]) * ease;
        const lat = from[1] + (to[1] - from[1]) * ease;
        marker.setLngLat([lng, lat]);
        if (t < 1) requestAnimationFrame(step);
    }
    requestAnimationFrame(step);
}

Push Notifications on Status Change

Beyond live tracking, clients need notifications about transitions: "courier picked up order," "courier 5 minutes away," "order delivered." Implemented via Web Push API or SMS:

// In OrderStatusChanged event handler
if ($order->status === 'in_transit') {
    $order->user->notify(new CourierPickedUpNotification($order));
}

Courier Offline Mode

If the courier's device loses connection—the mobile app buffers coordinates, which are sent in bulk when connection restores. The backend accepts a point array with timestamps and replays the path animation rather than jumping to the final position.

Timeline

  • Basic tracking (Redis + broadcast + map): 4–5 days
  • Private Channel authorization + access logic: 1 day
  • ETA calculation by straight line: 0.5 day
  • ETA via Routing API (OSRM / Google Directions): +1–2 days
  • Marker animation + path smoothing: 1 day
  • Push notifications on status change: 1–2 days
  • Dispatcher admin panel (all couriers on map): 3–4 days