Skip to main content
RapidDev - Software Development Agency

How to Build a Ride Hailing Platform with Replit

Build a simplified ride-hailing platform in Replit in 2-4 hours. Use Replit Agent to generate an Express + PostgreSQL app with driver-rider matching, Haversine distance queries, GPS coordinate tracking via polling, fare estimation using a routing API, Stripe Connect driver payouts, and mutual post-ride ratings. Deploy on Reserved VM for always-on GPS polling.

What you'll build

  • Rider request flow with pickup/dropoff address inputs, fare estimate, and nearest-driver matching using Haversine distance
  • Driver mode with online/offline toggle, GPS location broadcast, and list of available nearby ride requests
  • Ride status lifecycle: requested → matched → driver_en_route → in_progress → completed
  • GPS coordinate tracking table with 5-second polling so riders can watch their driver approach on a map
  • Fare estimation using the OpenRouteService free API for distance and duration between two coordinates
  • Stripe Connect Express driver onboarding and post-ride payout transfers
  • Mutual post-ride ratings with average score maintained on the driver profile
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced16 min read2-4 hoursReplit Core or higherApril 2026RapidDev Engineering Team
TL;DR

Build a simplified ride-hailing platform in Replit in 2-4 hours. Use Replit Agent to generate an Express + PostgreSQL app with driver-rider matching, Haversine distance queries, GPS coordinate tracking via polling, fare estimation using a routing API, Stripe Connect driver payouts, and mutual post-ride ratings. Deploy on Reserved VM for always-on GPS polling.

What you're building

A ride-hailing platform is technically one of the most demanding apps to build — it requires real-time location tracking, dynamic pricing, two-sided user roles, and split financial transfers. This guide builds a simplified but fully functional version: riders request rides with pickup and dropoff coordinates, the system finds the nearest available driver using the Haversine formula, and the driver accepts and navigates to the pickup.

Replit Agent generates the full Express backend from a single prompt. The GPS tracking system uses polling instead of WebSockets: the driver posts their coordinates every 5 seconds, and the rider polls the latest position every 5 seconds. This is intentionally simpler than a WebSocket approach — it works reliably on Replit and avoids the complexity of persistent socket connections. Fare estimation calls the free OpenRouteService API to calculate distance and duration between the two points, then applies a base fare plus per-km and per-minute rate.

Stripe Connect handles driver payouts. This is a manual Stripe integration — the Replit /stripe command does not support Connect flows. Drivers onboard via Stripe Express, and after ride completion the platform initiates a transfer to the driver's connected account. Deploy on Reserved VM: the constant GPS polling and the webhook endpoint for Stripe payout events both require an always-running server.

Final result

A fully functional ride-hailing API with driver-rider matching, GPS coordinate tracking, Haversine-based driver discovery, fare estimation, Stripe Connect driver payouts, and mutual ratings — deployed on Replit Reserved VM.

Tech stack

ReplitIDE & Hosting
ExpressBackend Framework
PostgreSQLDatabase
Drizzle ORMDatabase ORM
Stripe ConnectDriver Payouts
Replit AuthAuth
OpenRouteService APIFare Estimation

Prerequisites

  • A Replit Core account or higher (Reserved VM deployment required for always-on GPS polling)
  • A Stripe account with Connect enabled (Settings → Connect → Enable Stripe Connect)
  • STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET stored in Replit Secrets (open Secrets via lock icon)
  • An OpenRouteService API key (free at openrouteservice.org) stored in Replit Secrets as ORS_API_KEY
  • Basic understanding of latitude/longitude coordinates and what GPS tracking means

Build steps

1

Scaffold the project with Replit Agent

Create a new Repl and use the Agent prompt below to generate the full ride-hailing schema and routes. Install the Stripe SDK manually — do NOT use the /stripe command, which does not support Connect flows.

prompt.txt
1// Type this into Replit Agent:
2// Build a ride-hailing platform with Express and PostgreSQL using Drizzle ORM.
3// Install stripe npm package manually (DO NOT use /stripe command).
4// Tables:
5// - users: id serial pk, user_id text not null unique (Replit Auth ID),
6// type text not null (enum rider/driver), name text not null,
7// phone text, email text, created_at timestamp default now()
8// - drivers: id serial, user_id integer FK users not null unique,
9// vehicle_make text, vehicle_model text, vehicle_year integer,
10// license_plate text, is_online boolean default false,
11// current_lat numeric, current_lng numeric,
12// rating_avg numeric default 5.0, total_rides integer default 0,
13// stripe_account_id text, payout_status text default 'pending'
14// - rides: id serial, rider_id integer FK users not null,
15// driver_id integer FK users, pickup_lat numeric not null,
16// pickup_lng numeric not null, pickup_address text,
17// dropoff_lat numeric not null, dropoff_lng numeric not null,
18// dropoff_address text,
19// status text default 'requested' (enum requested/matched/driver_en_route/in_progress/completed/cancelled),
20// fare_estimate integer not null (cents), fare_actual integer,
21// distance_km numeric, duration_minutes integer,
22// started_at timestamp, completed_at timestamp,
23// created_at timestamp default now()
24// - ride_tracking: id serial, ride_id integer FK rides not null,
25// lat numeric not null, lng numeric not null,
26// recorded_at timestamp default now()
27// - ratings: id serial, ride_id integer FK rides not null unique,
28// rider_rating integer, driver_rating integer,
29// rider_comment text, driver_comment text,
30// created_at timestamp default now()
31// Routes:
32// POST /api/rides/request — fare estimate + create ride
33// POST /api/rides/:id/accept — driver accepts ride
34// PATCH /api/rides/:id/status — advance status
35// POST /api/rides/:id/cancel — cancel
36// POST /api/rides/:id/track — driver posts GPS coordinates
37// GET /api/rides/:id/track — rider polls latest driver position
38// POST /api/rides/:id/rate — mutual post-ride rating
39// PATCH /api/drivers/online — toggle driver availability + update GPS
40// GET /api/drivers/nearby — find online drivers within radius
41// GET /api/rides/active — current active ride
42// GET /api/rides/history — past rides
43// POST /api/drivers/onboard — Stripe Connect Express onboarding
44// POST /api/webhooks/stripe — raw body, Stripe payouts webhook
45// Register webhook route BEFORE express.json(). Use Replit Auth. Bind server to 0.0.0.0.

Pro tip: Install the Stripe SDK in the Replit packages panel or Shell tab: npm install stripe. Also install node-fetch for the OpenRouteService API calls if Agent does not add it automatically.

Expected result: A running Express app with all five tables created. The packages panel shows stripe installed. The console shows 'Ride-hailing server running on port 5000'.

2

Build the fare estimation and ride request route

When a rider requests a ride, the route calls OpenRouteService to get the driving distance and duration, calculates a fare, and finds the nearest online driver using the Haversine formula.

server/routes/rides.js
1const fetch = require('node-fetch');
2const { rides, drivers, users } = require('../../shared/schema');
3const { eq, and, sql } = require('drizzle-orm');
4
5const BASE_FARE_CENTS = 200; // $2.00 base
6const PER_KM_CENTS = 120; // $1.20 per km
7const PER_MIN_CENTS = 25; // $0.25 per minute
8
9async function estimateFare(pickupLat, pickupLng, dropoffLat, dropoffLng) {
10 const url = `https://api.openrouteservice.org/v2/directions/driving-car?api_key=${process.env.ORS_API_KEY}&start=${pickupLng},${pickupLat}&end=${dropoffLng},${dropoffLat}`;
11 const resp = await fetch(url);
12 const data = await resp.json();
13 const segment = data.features?.[0]?.properties?.segments?.[0];
14 const distanceKm = segment ? segment.distance / 1000 : 5;
15 const durationMin = segment ? segment.duration / 60 : 10;
16 const fare = BASE_FARE_CENTS + Math.round(distanceKm * PER_KM_CENTS) + Math.round(durationMin * PER_MIN_CENTS);
17 return { fare, distanceKm: Math.round(distanceKm * 10) / 10, durationMin: Math.round(durationMin) };
18}
19
20// Haversine distance in km between two lat/lng points
21function haversine(lat1, lng1, lat2, lng2) {
22 const R = 6371;
23 const dLat = (lat2 - lat1) * Math.PI / 180;
24 const dLng = (lng2 - lng1) * Math.PI / 180;
25 const a = Math.sin(dLat / 2) ** 2 +
26 Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2;
27 return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
28}
29
30// POST /api/rides/request
31router.post('/rides/request', async (req, res) => {
32 const userId = req.user?.id;
33 if (!userId) return res.status(401).json({ error: 'Login required' });
34
35 const { pickupLat, pickupLng, pickupAddress, dropoffLat, dropoffLng, dropoffAddress } = req.body;
36
37 const { fare, distanceKm, durationMin } = await estimateFare(pickupLat, pickupLng, dropoffLat, dropoffLng);
38
39 const [rider] = await db.select().from(users).where(eq(users.userId, userId));
40 if (!rider) return res.status(404).json({ error: 'User profile not found' });
41
42 const [ride] = await db.insert(rides).values({
43 riderId: rider.id,
44 pickupLat, pickupLng, pickupAddress,
45 dropoffLat, dropoffLng, dropoffAddress,
46 fareEstimate: fare,
47 distanceKm,
48 durationMinutes: durationMin,
49 status: 'requested',
50 }).returning();
51
52 res.status(201).json({ ride, fareEstimate: fare, distanceKm, durationMin });
53});

Pro tip: Store ORS_API_KEY in Replit Secrets (lock icon in sidebar). The free OpenRouteService plan allows 2,000 requests per day — sufficient for testing. For production, consider caching fare estimates by coordinate pair.

Expected result: POST /api/rides/request returns a ride object with fareEstimate in cents and the calculated distance and duration. A ride record is created with status 'requested'.

3

Build GPS tracking and nearby driver discovery

The driver posts their GPS coordinates every 5 seconds via PATCH /api/drivers/online. The rider polls GET /api/rides/:id/track to see the latest driver position. The nearby driver query uses Haversine in SQL to find drivers within 5km of the pickup.

server/routes/tracking.js
1// PATCH /api/drivers/online — driver toggles availability and updates GPS
2router.patch('/drivers/online', async (req, res) => {
3 const userId = req.user?.id;
4 if (!userId) return res.status(401).json({ error: 'Login required' });
5
6 const { isOnline, lat, lng } = req.body;
7
8 const [user] = await db.select().from(users).where(eq(users.userId, userId));
9 const [driver] = await db.select().from(drivers).where(eq(drivers.userId, user.id));
10 if (!driver) return res.status(404).json({ error: 'Driver profile not found' });
11
12 const [updated] = await db.update(drivers)
13 .set({ isOnline, currentLat: lat, currentLng: lng })
14 .where(eq(drivers.id, driver.id))
15 .returning();
16
17 res.json(updated);
18});
19
20// POST /api/rides/:id/track — driver posts current GPS position
21router.post('/rides/:id/track', async (req, res) => {
22 const { lat, lng } = req.body;
23 await db.insert(rideTracking).values({
24 rideId: parseInt(req.params.id),
25 lat, lng,
26 });
27 // Also update driver's current_lat/lng for nearby queries
28 const userId = req.user?.id;
29 const [user] = await db.select().from(users).where(eq(users.userId, userId));
30 const [driver] = await db.select().from(drivers).where(eq(drivers.userId, user.id));
31 if (driver) {
32 await db.update(drivers).set({ currentLat: lat, currentLng: lng }).where(eq(drivers.id, driver.id));
33 }
34 res.json({ recorded: true });
35});
36
37// GET /api/rides/:id/track — rider polls latest driver GPS position
38router.get('/rides/:id/track', async (req, res) => {
39 const [latest] = await db.select().from(rideTracking)
40 .where(eq(rideTracking.rideId, parseInt(req.params.id)))
41 .orderBy(desc(rideTracking.recordedAt))
42 .limit(1);
43 res.json(latest || null);
44});
45
46// GET /api/drivers/nearby — find online drivers within 5km of pickup
47router.get('/drivers/nearby', async (req, res) => {
48 const { lat, lng, radius = 5 } = req.query;
49 const nearbyDrivers = await db.execute(sql`
50 SELECT d.*, u.name,
51 acos(
52 sin(radians(${parseFloat(lat)})) * sin(radians(d.current_lat)) +
53 cos(radians(${parseFloat(lat)})) * cos(radians(d.current_lat)) *
54 cos(radians(d.current_lng) - radians(${parseFloat(lng)}))
55 ) * 6371 AS distance_km
56 FROM drivers d
57 JOIN users u ON u.id = d.user_id
58 WHERE d.is_online = true
59 AND d.current_lat IS NOT NULL
60 HAVING acos(
61 sin(radians(${parseFloat(lat)})) * sin(radians(d.current_lat)) +
62 cos(radians(${parseFloat(lat)})) * cos(radians(d.current_lat)) *
63 cos(radians(d.current_lng) - radians(${parseFloat(lng)}))
64 ) * 6371 <= ${parseFloat(radius)}
65 ORDER BY distance_km ASC
66 LIMIT 10
67 `);
68 res.json(nearbyDrivers.rows);
69});

Expected result: GET /api/drivers/nearby returns a list of online drivers sorted by distance. POST /api/rides/1/track inserts a coordinate row. GET /api/rides/1/track returns the most recent driver position.

4

Build Stripe Connect driver onboarding and payout webhook

Drivers onboard via Stripe Connect Express. After a ride completes, the platform initiates a transfer to the driver's Stripe account. The webhook handler uses constructEvent (sync) to verify the signature.

server/routes/payouts.js
1const Stripe = require('stripe');
2const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
3const express = require('express');
4const router = express.Router();
5
6// POST /api/drivers/onboard — initiate Stripe Connect Express onboarding
7router.post('/drivers/onboard', async (req, res) => {
8 const userId = req.user?.id;
9 const [user] = await db.select().from(users).where(eq(users.userId, userId));
10 const [driver] = await db.select().from(drivers).where(eq(drivers.userId, user.id));
11 if (!driver) return res.status(404).json({ error: 'Driver profile not found' });
12
13 let accountId = driver.stripeAccountId;
14 if (!accountId) {
15 const account = await stripe.accounts.create({
16 type: 'express',
17 capabilities: { transfers: { requested: true } },
18 });
19 accountId = account.id;
20 await db.update(drivers).set({ stripeAccountId: accountId }).where(eq(drivers.id, driver.id));
21 }
22
23 const link = await stripe.accountLinks.create({
24 account: accountId,
25 refresh_url: `${process.env.REPLIT_DEPLOYMENT_URL}/driver/onboard`,
26 return_url: `${process.env.REPLIT_DEPLOYMENT_URL}/driver/dashboard`,
27 type: 'account_onboarding',
28 });
29
30 res.json({ url: link.url });
31});
32
33// POST /api/webhooks/stripe — raw body required, register BEFORE express.json()
34router.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
35 const sig = req.headers['stripe-signature'];
36 let event;
37 try {
38 // constructEvent is SYNC — not async
39 event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
40 } catch (err) {
41 return res.status(400).json({ error: `Webhook verification failed: ${err.message}` });
42 }
43
44 if (event.type === 'account.updated') {
45 const account = event.data.object;
46 if (account.charges_enabled && account.payouts_enabled) {
47 await db.update(drivers)
48 .set({ payoutStatus: 'active' })
49 .where(eq(drivers.stripeAccountId, account.id));
50 }
51 }
52
53 res.json({ received: true });
54});
55
56module.exports = router;

Pro tip: Store REPLIT_DEPLOYMENT_URL in Replit Secrets before deploying. This URL is used in the Stripe Account Link's refresh_url and return_url. Using a dev URL here will break the onboarding redirect flow.

Expected result: POST /api/drivers/onboard returns a Stripe-hosted onboarding URL. After the driver completes identity verification, the account.updated webhook fires and sets driver.payout_status to 'active'.

5

Deploy on Reserved VM and register the webhook endpoint

Deploy on Reserved VM — the constant GPS polling and the Stripe webhook endpoint both require a server that is always running. After deploying, register your webhook URL in the Stripe Dashboard.

server/index.js
1// server/index.js — critical middleware order
2const express = require('express');
3const path = require('path');
4
5const payoutsRouter = require('./routes/payouts'); // contains webhook route
6const ridesRouter = require('./routes/rides');
7const trackingRouter = require('./routes/tracking');
8
9const app = express();
10
11// IMPORTANT: Register webhook route BEFORE express.json()
12// Stripe needs the raw request body for signature verification
13app.use('/api', payoutsRouter);
14
15// Then add JSON parsing for all other routes
16app.use(express.json());
17app.use('/api', ridesRouter);
18app.use('/api', trackingRouter);
19
20// Serve static frontend
21app.use(express.static(path.join(__dirname, '../client/dist')));
22app.get('*', (req, res) => {
23 res.sendFile(path.join(__dirname, '../client/dist/index.html'));
24});
25
26// Bind to 0.0.0.0 — required for Replit
27app.listen(5000, '0.0.0.0', () => console.log('Ride-hailing server running on port 5000'));
28
29// After deploying to Reserved VM:
30// 1. Stripe Dashboard → Developers → Webhooks → Add endpoint
31// 2. URL: https://your-repl.replit.app/api/webhooks/stripe
32// 3. Select events: account.updated, transfer.created
33// 4. Copy Signing Secret → Replit Secrets → STRIPE_WEBHOOK_SECRET

Expected result: The app runs on Reserved VM with a persistent public URL. GPS polling works without cold start delays. The Stripe webhook is registered and payout_status updates after driver onboarding completes.

Complete code

server/routes/rides.js
1const express = require('express');
2const { rides, drivers, users, rideTracking, ratings } = require('../../shared/schema');
3const { eq, and, desc } = require('drizzle-orm');
4const { db } = require('../db');
5
6const router = express.Router();
7
8// POST /api/rides/:id/accept — driver accepts a requested ride
9router.post('/rides/:id/accept', async (req, res) => {
10 const userId = req.user?.id;
11 if (!userId) return res.status(401).json({ error: 'Login required' });
12
13 const [user] = await db.select().from(users).where(eq(users.userId, userId));
14 const [driver] = await db.select().from(drivers).where(eq(drivers.userId, user.id));
15 if (!driver) return res.status(403).json({ error: 'Driver profile required' });
16
17 const [ride] = await db.select().from(rides)
18 .where(and(eq(rides.id, parseInt(req.params.id)), eq(rides.status, 'requested')));
19 if (!ride) return res.status(404).json({ error: 'Ride not found or already accepted' });
20
21 const [updated] = await db.update(rides)
22 .set({ driverId: user.id, status: 'matched' })
23 .where(eq(rides.id, ride.id))
24 .returning();
25
26 res.json(updated);
27});
28
29// PATCH /api/rides/:id/status — advance through ride lifecycle
30router.patch('/rides/:id/status', async (req, res) => {
31 const { status } = req.body;
32 const allowed = ['driver_en_route', 'in_progress', 'completed', 'cancelled'];
33 if (!allowed.includes(status)) return res.status(400).json({ error: 'Invalid status' });
34
35 const updates = { status };
36 if (status === 'in_progress') updates.startedAt = new Date();
37 if (status === 'completed') updates.completedAt = new Date();
38
39 const [updated] = await db.update(rides).set(updates)
40 .where(eq(rides.id, parseInt(req.params.id))).returning();
41 res.json(updated);
42});
43
44// POST /api/rides/:id/rate — mutual post-ride ratings
45router.post('/rides/:id/rate', async (req, res) => {
46 const userId = req.user?.id;
47 if (!userId) return res.status(401).json({ error: 'Login required' });
48 const { driverRating, riderRating, driverComment, riderComment } = req.body;
49 const [rating] = await db.insert(ratings).values({
50 rideId: parseInt(req.params.id),
51 driverRating: driverRating || null,
52 riderRating: riderRating || null,
53 driverComment: driverComment || null,
54 riderComment: riderComment || null,
55 }).returning();
56 res.status(201).json(rating);
57});
58
59module.exports = router;

Customization ideas

Surge pricing multiplier

Add a demand calculation that counts active ride requests within a 5km radius. If the request-to-driver ratio exceeds 2:1, apply a surge multiplier (e.g. 1.5x) to the fare estimate. Display the surge multiplier prominently to the rider before they confirm.

In-app chat between driver and rider

Add a ride_messages table with ride_id, sender_id, message, and created_at. Add POST /api/rides/:id/messages and GET /api/rides/:id/messages routes. The rider polls for new messages every 5 seconds alongside the GPS tracking poll.

Driver earnings dashboard

Add a GET /api/drivers/earnings route returning weekly earnings grouped by day: SUM of fare_actual for completed rides where driver_id matches, joined with the ratings table for average rating per week. Display as a bar chart in the driver dashboard.

Scheduled rides

Add a scheduled_at timestamp to the rides table. Add a background job (using setInterval in server.js on Reserved VM) that runs every minute and matches unmatched scheduled rides to the nearest online driver 10 minutes before the pickup time.

Common pitfalls

Pitfall: Using the /stripe Replit command instead of installing the SDK manually

How to avoid: Install stripe via npm install stripe in the Shell tab. Store STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in Replit Secrets (lock icon) and import with const stripe = new Stripe(process.env.STRIPE_SECRET_KEY).

Pitfall: Deploying on Autoscale instead of Reserved VM

How to avoid: Deploy on Reserved VM ($10-20/month on Replit Core). This keeps the server running 24/7 and eliminates cold start delays for GPS polling and webhook reception.

Pitfall: Registering the Stripe webhook route after express.json() middleware

How to avoid: Register app.use('/api', payoutsRouter) before app.use(express.json()). Use express.raw({ type: 'application/json' }) only on the webhook route itself.

Pitfall: Hardcoding API keys instead of using Replit Secrets

How to avoid: Open the Secrets panel (lock icon in Replit sidebar). Add ORS_API_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and REPLIT_DEPLOYMENT_URL as individual secrets. Access them in code with process.env.KEY_NAME.

Best practices

  • Store ORS_API_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and REPLIT_DEPLOYMENT_URL in Replit Secrets — never hardcode API keys in source files.
  • Register the Stripe webhook route BEFORE express.json() middleware — constructEvent() requires the raw request body Buffer, not a parsed JSON object.
  • Deploy on Reserved VM for ride-hailing apps — GPS polling every 5 seconds and Stripe webhook reception both require a server that is always running without cold start delays.
  • Use the Haversine formula in SQL for nearby driver queries — running it in JavaScript would require fetching all online drivers to the application layer and filtering there, which is slow and wasteful.
  • Validate ride status transitions in the PATCH /api/rides/:id/status route — only allow progression forward through the lifecycle to prevent riders or drivers from jumping to invalid states.
  • Limit ride_tracking rows per ride with a cleanup job or a partial index — storing GPS coordinates every 5 seconds creates thousands of rows per ride. Delete rows older than 1 hour for completed rides.
  • Use Drizzle Studio (built into Replit) to inspect the rides and drivers tables during testing — it makes it easy to manually toggle driver online status and verify status transitions work correctly.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a ride-hailing platform with Express, PostgreSQL, and Drizzle ORM on Replit. I need to implement: (1) a Haversine distance query in PostgreSQL that returns online drivers within a given radius of a pickup coordinate, ordered by distance; (2) a fare estimate function that calls the OpenRouteService Directions API to get driving distance and duration between two lat/lng coordinates and returns a fare in cents using base_fare + per_km_rate + per_minute_rate. Show me the SQL for the Haversine query and the Node.js function for the ORS API call.

Build Prompt

Add a cancellation fee system to the ride-hailing platform. If a rider cancels more than 2 minutes after a driver has been matched and is en route, charge a $2.00 cancellation fee using Stripe PaymentIntents. Store the rider's saved payment method ID in the users table from a setup intent flow. When cancellation is requested, check the time elapsed since status changed to matched, and if greater than 120 seconds, create a PaymentIntent for 200 cents against the rider's saved method and transfer $1.50 to the driver's connected Stripe account as compensation.

Frequently asked questions

How does the real-time GPS tracking work without WebSockets?

The driver's mobile browser posts their coordinates to POST /api/rides/:id/track every 5 seconds using a setInterval call. The rider's frontend polls GET /api/rides/:id/track every 5 seconds to retrieve the latest coordinate row. This polling approach is simpler to build and debug than WebSockets, and works reliably on Replit's Reserved VM.

Why use the Haversine formula instead of a mapping API for driver discovery?

Finding nearby drivers is a fast SQL query — no external API calls needed. The Haversine formula calculates straight-line distance between two lat/lng coordinates directly in PostgreSQL. This runs in milliseconds and does not consume OpenRouteService API quota. The routing API is only called once when estimating a fare.

How do driver payouts work with Stripe Connect?

Drivers onboard via Stripe Express, which guides them through identity verification and bank account setup on a Stripe-hosted page. After a ride completes, the platform initiates a transfer from the platform Stripe account to the driver's connected Stripe account using stripe.transfers.create(). The transfer amount is fare_actual minus the platform commission.

What Replit plan do I need?

Replit Core or higher. This build requires Reserved VM deployment for two reasons: GPS polling creates constant inbound requests that would suffer from cold start delays on Autoscale, and the Stripe webhook endpoint must be running 24/7 to receive payout event confirmations.

Should I deploy on Autoscale or Reserved VM?

Reserved VM. GPS tracking polls every 5 seconds — Autoscale's 2-10 second cold start would create visible gaps in driver position updates. Stripe webhook events also require a server that is always listening. Reserved VM costs $10-20/month on Replit Core and eliminates both problems.

Does GPS tracking work on mobile browsers?

Yes, but only over HTTPS. Replit's deployment URLs (*.replit.app) are always HTTPS, so this works out of the box. The Geolocation API that the driver frontend uses to get the device's current position requires a secure context — it will not work over plain HTTP.

Can RapidDev help build a custom ride-hailing platform?

Yes. RapidDev has built 600+ apps including two-sided marketplace and mobility platforms with custom fare algorithms, surge pricing, and driver settlement workflows. Book a free consultation at rapidevelopers.com.

How do I prevent a driver from accepting multiple rides simultaneously?

Add a check in the POST /api/rides/:id/accept route that queries the rides table for any existing ride where driver_id = the current driver's id and status is in ('matched', 'driver_en_route', 'in_progress'). If one exists, return a 409 error with 'You already have an active ride'. Only allow acceptance if no active ride is found.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.