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:
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:
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)
})