Uber-style real-time matching for emergency and same-day home services. Poster requests, contractors accept, work gets done immediately.
OnDemandRequestScreen — Category chips use flexWrap, location auto-detected via expo-location
useQuery(api.users.posters.me) • useMutation(api.tracking.ondemand.requestAppointment)
MatchingScreen — Convex real-time subscription pushes contractor matches as they come in
useQuery(api.tracking.ondemandQueries.getNearbyContractors) • live subscription via Convex
RequestDetailScreen — Real-time ETA countdown, map route placeholder, call/message CTAs
useQuery(api.tracking.ondemandQueries.getAppointmentRequest) • useMutation(api.tracking.ondemand.cancelAppointmentRequest)
AppointmentRequestCard — 2-minute countdown timer, payout estimate based on hourly rate
Convex real-time push • useMutation(api.tracking.ondemand.acceptAppointment)
ActiveOnDemandScreen — GPS location tracking, arrival triggers clock-in prompt
useMutation(api.tracking.ondemand.arriveAtJob) • expo-location watchPositionAsync
ContractorOnDemandScreen — OnlineStatusToggle, radius filter, real-time earnings feed
useQuery(api.tracking.ondemandQueries.getMyOnlineStatus) • useMutation(api.tracking.ondemand.goOnline)
JobCompleteScreen — Tip selector (poster), star rating, review textarea for both roles
useMutation(api.tracking.ondemand.completeAppointment) • useMutation(api.jobs.reviews.create)
OnDemandHistoryScreen — Segmented filter, reorder button for poster, "Similar Jobs" for contractor
useQuery(api.tracking.ondemandQueries.getAppointmentHistory) • Pagination via cursor
appointmentRequests
posterId: v.id("posterUsers")
contractorId: v.id("contractorUsers")
categoryId: v.string()
status: "pending"|"accepted"|"declined"|"expired"|"cancelled"|"completed"
urgency: "emergency"|"same_day"|"flexible"
latitude/longitude: v.number()
address, city, state, zipCode
description: v.optional(v.string())
estimatedTravelMinutes: v.number()
expiresAt: v.number()
contractorOnlineStatus
contractorId: v.id("contractorUsers")
isOnline: v.boolean()
latitude/longitude: v.number()
geohash: v.string()
serviceCategories: v.array(v.string())
autoAcceptEnabled: v.boolean()
maxTravelMinutes: v.number()
tracking/ondemand
goOnline(latitude, longitude, serviceCategories)
goOffline()
updateOnlineLocation(latitude, longitude)
requestAppointment(contractorId, categoryId, address...)
acceptAppointment(requestId)
declineAppointment(requestId)
cancelAppointmentRequest(requestId)
completeAppointment(requestId)
tracking/ondemandQueries
getNearbyContractors(lat, lng, radiusMiles)
getMyOnlineStatus()
getActiveServiceCategories(lat, lng)
getContractorAppointmentRequests(status)
getPendingRequestCount()
getAppointmentRequest(requestId)
getAppointmentHistory(limit, cursor)
components/tracking/ondemand/
OnlineStatusToggle
AppointmentRequestCard
ContractorMapPin
NearbyContractorCard
CategoryFilter
RadiusFilter
PendingRequestsBadge
ContractorDiscoveryPopup
Real-time via Convex
useQuery() auto-subscribes to live data
Contractor location: 30s interval push
Match results: instant subscription
expo-location
watchPositionAsync for GPS tracking
Accuracy.High, 50m distance interval
Foreground permission required
Request expires in 5 minutes
contractor must respond or auto-decline
Auto-accept
if contractor.autoAcceptEnabled = true
Platform fee based on subscription tier
deducted from contractor payout
Geohash matching
prefix-based proximity search
One active request per poster
prevents duplicate flooding
Escrow holds payment
released after completion + rating