← Use cases

Order and delivery tracking

A status page that updates itself and a courier who moves on the map, sent only while the customer is watching, with a push at the steps that matter.

"Where is my order?" is the question delivery, ride-hailing, food ordering and field-service apps hear most. A status page that updates itself, and a map where the courier actually moves, answers it before anyone writes to support. The same pattern works for repair tickets, bookings and anything else with steps a customer waits for.

What your users get

  • The order status changing from "preparing" to "on the way" to "delivered" on its own.
  • The courier's position moving on the map every few seconds.
  • A push notification at the steps that matter when the app is closed.

Built with

  • Private channels (private-order-<id>), so only the customer and your staff can follow an order.
  • Cache channels (private-cache-), so a customer who opens the page gets the latest status and position at once.
  • channel_vacated webhooks, so you stop sending positions when nobody is watching.
  • Push notifications (Beams-compatible) for the steps a customer must not miss.

Step 1: authorize the order channel

app.post('/pusher/auth', requireLogin, express.urlencoded({ extended: false }), async (req, res) => {
  const { socket_id, channel_name } = req.body;
  const id = channel_name.match(/^private-cache-order-(\d+)$/)?.[1];
  const order = id && (await db.orders.find(id));
  if (!order || (order.customerId !== req.user.id && !req.user.isStaff)) return res.sendStatus(403);
  res.json(pusher.authorizeChannel(socket_id, channel_name));
});

Step 2: publish status changes and positions

Status changes come from your order system. Positions come from the courier app, which reports them to your server, and your server forwards them while someone is watching:

async function setStatus(order, status) {
  await db.orders.update(order.id, { status });
  await pusher.trigger(`private-cache-order-${order.id}`, 'status', { status, at: Date.now() });
}

app.post('/courier/location', requireCourier, async (req, res) => {
  const order = await db.orders.activeFor(req.user.id);
  if (order && watching.has(order.id)) {
    await pusher.trigger(`private-cache-order-${order.id}`, 'location', { lat: req.body.lat, lng: req.body.lng });
  }
  res.sendStatus(204);
});

Step 3: show it in the app

const order = pusher.subscribe(`private-cache-order-${orderId}`);
order.bind('status', ({ status }) => timeline.set(status));
order.bind('location', ({ lat, lng }) => map.moveCourier(lat, lng));

A cache channel keeps only its last event, whatever its name. So that a customer who arrives between two updates gets both the status and the position, include the current status in each location event too.

Step 4: only send while someone watches

Subscribe a webhook endpoint to channel_occupied and channel_vacated. When the customer closes the page, the channel is vacated and your server stops forwarding positions:

for (const e of webhook.getEvents()) {
  const id = e.channel.match(/^private-cache-order-(\d+)$/)?.[1];
  if (!id) continue;
  if (e.name === 'channel_occupied') watching.add(id);
  if (e.name === 'channel_vacated') watching.delete(id);
}

Keep watching somewhere all your servers share, such as Redis or your database.

Step 5: push the steps that matter

await beams.publishToUsers([String(order.customerId)], {
  fcm: { notification: { title: 'Your order is on the way', body: 'Arriving in about 15 minutes.' } },
  apns: { aps: { alert: { title: 'Your order is on the way', body: 'Arriving in about 15 minutes.' } } },
  web: { notification: { title: 'Your order is on the way', deep_link: `https://your-site.example/orders/${order.id}` } },
});

Set up the devices with the push notifications guide.

Tips

  • A position every 3 to 5 seconds looks smooth when the map animates between points, and costs a fraction of one per second.
  • Round coordinates to five decimal places (about one meter); more precision only adds bytes.
  • Stop sending positions as soon as an order is delivered, and let the cache keep the final status for latecomers.