Skip to content

Rastreamento GPS em Tempo Real

Arquitetura completa do sistema de rastreamento de motoristas e atualizações em tempo real.


Visão Geral

Driver PWA                    Backend (FastAPI)           Supabase
    │                               │                         │
    │── GPS update (10s) ──────────>│── UPSERT ──────────────>│
    │   PUT /api/v1/locations/me    │   driver_locations       │
    │                               │                         │
    │                               │           Realtime broadcast
    │                               │                         │
    │                               │                         ▼
Admin App ◄──────────────────────────────────────────── WebSocket
    │    subscription: driver_locations                       │
    │    channel                                              │
    │                                                         │
    └── atualiza markers no mapa MapLibre ◄───────────────────┘

Componentes do Sistema

1. Envio de Localização (Driver)

O PWA do driver envia a localização a cada 10 segundos quando está online.

Endpoint:

PUT /api/v1/locations/me
Authorization: Bearer <driver_jwt>

Payload:

{
  "lat": -23.550520,
  "lng": -46.633308,
  "heading": 180.0,
  "speed": 35.5,
  "accuracy": 12.3,
  "delivery_id": "uuid-da-entrega-atual",
  "is_online": true
}

Implementação (backend):

@router.put("/me", status_code=status.HTTP_204_NO_CONTENT)
async def update_my_location(body: LocationUpdate, driver: RequireDriver) -> None:
    supabase = get_supabase()
    supabase.table("driver_locations").upsert(
        {
            "driver_id": driver["sub"],
            "lat": str(body.lat),
            "lng": str(body.lng),
            "heading": str(body.heading) if body.heading else None,
            "speed": str(body.speed) if body.speed else None,
            "accuracy": str(body.accuracy) if body.accuracy else None,
            "delivery_id": body.delivery_id,
            "is_online": body.is_online,
        },
        on_conflict="driver_id",  # UPSERT por driver_id (1 linha por driver)
    ).execute()

Retorno: 204 No Content (sem corpo de resposta para economizar banda).


2. Supabase Realtime (WebSocket)

O Supabase propaga automaticamente as mudanças em driver_locations via WebSocket para todos os clientes subscritos.

Configuração necessária no Supabase Dashboard:

Database > Replication > Source > Selecionar tabelas:
  ✓ driver_locations
  ✓ deliveries
  ✓ wallets


3. Subscription no Admin (Frontend)

O mapa do admin usa Supabase Realtime para receber atualizações GPS sem polling:

// Exemplo de hook de subscription (frontend/hooks/)
import { createClient } from '@/lib/supabase/client'
import { useEffect, useRef } from 'react'
import type { DriverLocation } from '@/types'

export function useDriverLocations(
  onUpdate: (location: DriverLocation) => void
) {
  const supabase = createClient()

  useEffect(() => {
    const channel = supabase
      .channel('driver-locations')
      .on(
        'postgres_changes',
        {
          event: 'UPDATE',  // UPSERT dispara UPDATE no Realtime
          schema: 'public',
          table: 'driver_locations',
          filter: 'is_online=eq.true',
        },
        (payload) => {
          onUpdate(payload.new as DriverLocation)
        }
      )
      .subscribe((status) => {
        if (status === 'SUBSCRIBED') {
          console.log('Realtime connected: driver_locations')
        }
      })

    // Cleanup obrigatório
    return () => {
      supabase.removeChannel(channel)
    }
  }, [onUpdate])
}

4. Atualizações de Entregas (Driver)

Drivers recebem notificações de novas entregas disponíveis em tempo real:

// Hook para entregas disponíveis
export function useAvailableDeliveries(
  onNewDelivery: (delivery: Delivery) => void
) {
  const supabase = createClient()

  useEffect(() => {
    const channel = supabase
      .channel('available-deliveries')
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'deliveries',
          filter: 'status=eq.pending',
        },
        (payload) => {
          onNewDelivery(payload.new as Delivery)
        }
      )
      .subscribe()

    return () => supabase.removeChannel(channel)
  }, [onNewDelivery])
}

5. Saldo em Tempo Real (Driver)

O saldo do driver é atualizado via Realtime após completar uma entrega:

export function useWalletBalance(driverId: string) {
  const [balance, setBalance] = useState<number>(0)
  const supabase = createClient()

  useEffect(() => {
    const channel = supabase
      .channel('wallet-balance')
      .on(
        'postgres_changes',
        {
          event: 'UPDATE',
          schema: 'public',
          table: 'wallets',
          filter: `driver_id=eq.${driverId}`,
        },
        (payload) => {
          setBalance(payload.new.balance)
        }
      )
      .subscribe()

    return () => supabase.removeChannel(channel)
  }, [driverId])

  return balance
}

MapLibre GL JS

O mapa do admin usa MapLibre GL JS para renderizar motoristas em tempo real.

Inicialização

import maplibregl from 'maplibre-gl'

const map = new maplibregl.Map({
  container: 'map',
  style: `https://tiles.stadiamaps.com/styles/alidade_smooth.json?api_key=${process.env.NEXT_PUBLIC_STADIA_API_KEY}`,
  center: [-46.633308, -23.550520],  // São Paulo
  zoom: 12,
})

Atualizando Markers (sem re-render)

const markersRef = useRef<Map<string, maplibregl.Marker>>(new Map())

function updateDriverMarker(location: DriverLocation) {
  const existing = markersRef.current.get(location.driver_id)

  if (existing) {
    // Atualiza posição do marker existente
    existing.setLngLat([location.lng, location.lat])
  } else {
    // Cria novo marker
    const el = document.createElement('div')
    el.className = 'driver-marker'

    const marker = new maplibregl.Marker({ element: el })
      .setLngLat([location.lng, location.lat])
      .setPopup(new maplibregl.Popup().setText(`Driver: ${location.driver_id.slice(0, 8)}`))
      .addTo(map)

    markersRef.current.set(location.driver_id, marker)
  }
}

Rate Limiting

Para evitar sobrecarga no banco de dados e no ASAAS, o envio de localização é limitado:

  • Frequência máxima: 1 update por 10 segundos por driver
  • Implementação recomendada no frontend:
const lastUpdateRef = useRef<number>(0)

function sendLocationUpdate(position: GeolocationPosition) {
  const now = Date.now()
  if (now - lastUpdateRef.current < 10_000) return  // 10s cooldown

  lastUpdateRef.current = now
  fetch('/api/v1/locations/me', {
    method: 'PUT',
    headers: { 'Authorization': `Bearer ${token}` },
    body: JSON.stringify({
      lat: position.coords.latitude,
      lng: position.coords.longitude,
      heading: position.coords.heading,
      speed: position.coords.speed ? position.coords.speed * 3.6 : null,  // m/s → km/h
      accuracy: position.coords.accuracy,
    }),
  })
}

// Iniciar watchPosition da Geolocation API
navigator.geolocation.watchPosition(sendLocationUpdate, null, {
  enableHighAccuracy: true,
  timeout: 15_000,
  maximumAge: 0,
})

Arquitetura de Canais Realtime

Canal Tabela Evento Consumidor Filtro
driver-locations driver_locations UPDATE Admin map is_online=eq.true
available-deliveries deliveries INSERT Driver list status=eq.pending
wallet-balance wallets UPDATE Driver wallet driver_id=eq.<id>
my-delivery deliveries UPDATE Driver (entrega ativa) driver_id=eq.<id>

Presença (Presence)

Para funcionalidades de "quem está online", o Supabase Presence pode ser usado como alternativa ao is_online no banco:

// Driver entra online
const channel = supabase.channel('driver-presence')

channel.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    await channel.track({
      driver_id: user.id,
      online_at: new Date().toISOString(),
    })
  }
})

// Admin monitora presença
channel.on('presence', { event: 'sync' }, () => {
  const state = channel.presenceState()
  // state: { [key]: [{ driver_id, online_at }] }
})

Presence vs banco de dados

Presence é efêmero (perdido ao desconectar). O campo is_online no banco é persistente e pode ser consultado via REST. Use ambos em conjunto para melhor confiabilidade.


Troubleshooting

Conexão Realtime não estabelecida

Verifique se as tabelas estão habilitadas em: Dashboard > Database > Replication > Source > Tables

Updates não chegando no cliente

// Debug: logar status da subscription
channel.subscribe((status, err) => {
  console.log('Realtime status:', status)
  if (err) console.error('Realtime error:', err)
})

Memory leak — markers não removidos

// Cleanup ao desmontar componente
useEffect(() => {
  return () => {
    markersRef.current.forEach(marker => marker.remove())
    markersRef.current.clear()
  }
}, [])