Skip to content

SaiKrishnaMulukutla/uber

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

197 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RideGo — Ride-Hailing Backend

Live demo: https://api-gateway-ov0j.onrender.com (Singapore · Render free tier — first request may take ~30 s to cold-start)

Ride-hailing backend in Go. Seven microservices behind an NGINX gateway, event-driven via Aiven Kafka, geospatial driver matching with Upstash Redis GEO, Neon Postgres for persistence. Runs locally with Docker Compose; deployed on Render.


Table of Contents


Features

Auth

  • Registration is two steps: POST /register sends a 6-digit OTP, account only created after POST /verify-register confirms it. Login is password-only — no OTP on every signin.
  • Forgot password goes through OTP too: request a code, verify it, set new password.
  • HS256 JWTs: 15-min access tokens, 7-day refresh tokens, separate rider/driver roles.

Matching

  • POST /trips/request publishes a Kafka event. matching-service picks it up, finds the nearest available driver via Redis GEORADIUS, and sends an offer. Driver has 60 seconds to accept via POST /drivers/trips/{id}/respond.
  • Three vehicle tiers: go (hatchback), x (sedan), xl (SUV). Fare is computed server-side with surge capped at 5×.

Trip lifecycle

  • Once driver accepts, trip-service generates a 4-digit ride OTP stored in Redis. Driver can only start the trip after the rider reads them the code — cuts down on ghost rides.
  • Live GPS tracking over WebSocket while the trip is in progress.

Payments

  • Cash, UPI (QR scan or VPA collect), or Razorpay card. Browser-based checkout page with a WebSocket push on completion.
  • Razorpay webhooks are idempotent — duplicate events do nothing.
  • Driver earnings at GET /payments/earnings?period=week|month|all.

Operations

  • All inter-service events go through 5 Kafka topics. Low-traffic events (trip.cancelled, rating.submitted, payment.completed) share the ridego.events topic via an EventEnvelope{type, payload} wrapper.
  • No cross-service DB queries — each service owns its own schema.

Architecture

flowchart TD
    Client(["Client"])

    subgraph Gateway["API Gateway · :8000"]
        Nginx["nginx · reverse proxy + WS upgrade"]
    end

    subgraph UserSvc["User Service · :8081"]
        U["/users"]
    end
    subgraph DriverSvc["Driver Service · :8082"]
        D["/drivers"]
    end
    subgraph TripSvc["Trip Service · :8083"]
        T["/trips"]
        WS["/ws/trips/:id"]
    end
    subgraph MatchSvc["Matching Service"]
        M["Kafka consumer · no HTTP"]
    end
    subgraph NotifSvc["Notification Service · :8084"]
        N["/notifications"]
    end
    subgraph PaySvc["Payment Service · :8085"]
        P["/payments"]
    end

    subgraph Data["Data Layer"]
        PG[("PostgreSQL · 5 DBs")]
        Redis[("Redis · GEO + locks")]
        Kafka[["Aiven Kafka · 5 topics"]]
    end

    Client -->|HTTP / WS| Nginx
    Nginx --> U & D & T & WS & N & P

    U & N & P --> PG
    D & T --> PG & Redis
    U & D --> Redis
    T & P -->|publish| Kafka
    Kafka -->|consume| M & D & U & N & P & T
    M --> Redis
    M -->|publish| Kafka
Loading

Event Flow

POST /trips/request
    └─► ride.requested ──► matching-service (Redis GEO nearest driver)
                               └─► ride.offered ──► notification-service (push to driver)
                                       │
                               driver: POST /drivers/trips/{id}/respond {"accept": true}
                                       │
                               driver-service publishes driver.assigned
                                       ├─► trip-service  → assigns driver + generates 4-digit ride OTP (stored in Redis)
                                       ├─► driver-service → marks driver busy
                                       └─► notification-service
                                               ├─ notifies driver: "New Trip Assigned"
                                               └─ notifies rider:  "Driver On the Way! Check app for OTP"

Rider: GET /trips/{id}  (while status = DRIVER_ASSIGNED)
    └─► response includes ride_otp (only visible to the rider)
        rider tells the OTP to the driver verbally

Driver: PATCH /trips/{id}/start  { "otp": "1234" }
    └─► OTP validated against Redis → consumed on success → trip transitions to STARTED

PATCH /trips/:id/end
    └─► trip.completed ──► payment-service, driver-service, notification-service (+ email to rider)

POST /trips/:id/rate
    └─► ridego.events (rating.submitted) ──► driver-service, user-service, notification-service

payment completed
    └─► ridego.events (payment.completed) ──► notification-service (+ email to rider)

Services

Service Port Database
api-gateway 8000
user-service 8081 users_db
driver-service 8082 drivers_db
trip-service 8083 trips_db
matching-service — (no HTTP)
notification-service 8084 notifications_db
payment-service 8085 payments_db
PostgreSQL 5433
Redis 6380
Kafka (KRaft) 9093

Kafka Topics

5 topics total (Aiven free tier limit). Low-traffic events share ridego.events through an EventEnvelope{type, payload} wrapper; consumers switch on type.

Topic Publisher Consumers
ride.requested trip-service matching-service
ride.offered matching-service notification-service
driver.assigned driver-service (accept) trip-service, driver-service, notification-service
trip.completed trip-service payment-service, driver-service, notification-service
ridego.events trip-service (trip.cancelled, rating.submitted), payment-service (payment.completed) matching-service, driver-service, user-service, notification-service

Quick Start

Requires Docker and Docker Compose. Go 1.22+ only needed if building outside Docker.

git clone https://github.com/SaiKrishnaMulukutla/uber && cd uber
cp infra/.env.example infra/.env
# Fill in DATABASE_URL, REDIS_ADDR, KAFKA_*, JWT_SECRET, EMAIL_* — see table below
make up

Services start in dependency order. Each service runs its own migrations on boot and connects to the cloud infra (Neon, Upstash, Aiven) — no local Postgres or Redis needed.

make logs          # tail all service logs
make logs-trip     # tail a specific service
make down          # stop all containers
make clean         # stop + wipe volumes

Environment Variables

Variable Required Description
DATABASE_URL yes Neon PostgreSQL connection string (postgresql://...?sslmode=require)
REDIS_ADDR yes Upstash Redis URL (rediss://:<token>@<host>:6379)
KAFKA_BROKERS yes Aiven broker (<host>:25004)
KAFKA_USERNAME yes Aiven SASL username
KAFKA_PASSWORD yes Aiven SASL password
KAFKA_CA_CERT yes Base64-encoded Aiven project CA PEM (needed for TLS)
JWT_SECRET yes HS256 signing key — generate with openssl rand -base64 32
BREVO_API_KEY yes (prod) Brevo transactional email API key (used on Render — no SMTP port needed)
EMAIL_USER yes Sender email address (also activates SMTP fallback in local dev)
EMAIL_PASS local only Gmail app password (SMTP fallback; Render blocks port 465/587)
EMAIL_HOST no SMTP host (default: smtp.gmail.com)
EMAIL_PORT no SMTP port (default: 465)
PAYMENT_PROVIDER no cash (default) or razorpay
RAZORPAY_KEY_ID if razorpay Razorpay API key ID
RAZORPAY_KEY_SECRET if razorpay Razorpay API key secret
RAZORPAY_WEBHOOK_SECRET if razorpay Webhook signing secret from Razorpay Dashboard
INTERNAL_SECRET yes Shared secret for matching-service → trip-service calls
ALLOWED_ORIGINS no Comma-separated WebSocket origin allowlist (empty = allow all)

Render blocks ports 465/587, so Gmail SMTP only works locally. On Render, set BREVO_API_KEY — Brevo sends over HTTP and has no port issues.


API Reference

All requests go through http://localhost:8000. Authenticated endpoints need Authorization: Bearer <access_token>.

Users — /users

Method Path Auth Description
POST /users/register Step 1: validate input, send registration OTP
POST /users/verify-register Step 2: verify OTP, create account, issue JWT
POST /users/login Validate password, issue JWT directly
POST /users/forgot-password Send OTP to email for password reset
POST /users/reset-password Verify OTP and set new password
POST /users/refresh Rotate access token using refresh token
GET /users/{id} Bearer (rider) Get own profile (IDOR protected)

Drivers — /drivers

Method Path Auth Description
POST /drivers/register Step 1: validate input, send registration OTP
POST /drivers/verify-register Step 2: verify OTP, create account, issue JWT
POST /drivers/login Validate password, issue JWT directly
POST /drivers/forgot-password Send OTP to email for password reset
POST /drivers/reset-password Verify OTP and set new password
POST /drivers/refresh Rotate access token
GET /drivers/{id} Bearer (driver) Get own profile
PATCH /drivers/{id}/location Bearer (driver) Update GPS location in Redis GEO
PATCH /drivers/{id}/status Bearer (driver) Set available / offline
GET /drivers/nearby Bearer Find available drivers within radius
POST /drivers/trips/{tripId}/respond Bearer (driver) Accept or reject a pending ride offer (60s window)

Trips — /trips

Method Path Auth Description
POST /trips/estimate Bearer (rider) Fare + duration estimate; accepts optional vehicle_type
POST /trips/request Bearer (rider) Request a ride; accepts optional vehicle_type (go/x/xl)
GET /trips/history Bearer Paginated trip history
GET /trips/{id} Bearer Trip details (participants only); includes ride_otp for rider when status is DRIVER_ASSIGNED
PATCH /trips/{id}/assign X-Internal-Secret Internal — matching-service only
PATCH /trips/{id}/start Bearer (driver) Start trip; requires { "otp": "<4-digit code>" } body — OTP shown to rider on GET /trips/{id}
PATCH /trips/{id}/end Bearer (driver) End trip; computes fare by vehicle category
PATCH /trips/{id}/cancel Bearer Cancel trip; restores driver to GEO pool
POST /trips/{id}/rate Bearer Rate counterpart (1–5 stars)
POST /trips/{id}/location Bearer (driver) Push live GPS coordinate
WS /ws/trips/{id}?token=<jwt> JWT query param Real-time driver location stream
GET /trips/surge Bearer (admin) Get current surge multiplier
PATCH /trips/surge Bearer (admin) Set surge multiplier (1.0 – 5.0)

Notifications — /notifications

Method Path Auth Description
GET /notifications/ Bearer Paginated notification list
PATCH /notifications/{id}/read Bearer Mark notification as read

Payments — /payments

Method Path Auth Description
GET /payments/history Bearer Paginated payment history
GET /payments/{tripId} Bearer Payment by trip (participants only)
GET /payments/checkout/{id} HTML checkout page — auto-selects cash/UPI/card tab
WS /payments/ws/{id} WebSocket — pushes completion signal to checkout page
POST /payments/{id}/confirm-cash Bearer (driver) Driver confirms cash received → completes payment
POST /payments/checkout/{id}/upi Checkout token Send Razorpay UPI collect request to rider's VPA
POST /payments/orders Bearer (rider) Create Razorpay order → returns checkout_url, provider_order_id, key_id
POST /payments/verify Checkout token Submit Razorpay signature → completes payment
POST /payments/webhook HMAC Razorpay async backup confirmation
POST /payments/simulate-success Bearer Complete payment instantly (dev / test only)
GET /payments/earnings Bearer (driver) Earnings summary — ?period=week|month|all with daily breakdown

End-to-End Flow

BASE=http://localhost:8000

# 1. Register rider (2 steps — OTP verifies email before account is created)
# Step 1 — sends OTP to email
curl -s -X POST $BASE/users/register \
  -H "Content-Type: application/json" \
  -d '{"name":"Test Rider","email":"rider@test.com","phone":"+911111111111","password":"Pass123!"}'
# → { "message": "OTP sent to rider@test.com" }

# Step 2 — verify OTP (check inbox for 6-digit code)
curl -s -X POST $BASE/users/verify-register \
  -H "Content-Type: application/json" \
  -d '{"email":"rider@test.com","otp":"123456"}'
# → { "access_token": "...", "refresh_token": "...", "user": { ... } }
RIDER_TOKEN=<access_token from above>

# Register driver (same 2-step flow)
curl -s -X POST $BASE/drivers/register \
  -H "Content-Type: application/json" \
  -d '{"name":"Test Driver","email":"driver@test.com","phone":"+912222222222","password":"Pass123!","vehicle_type":"x","license_plate":"KA-01-AB-1234"}'
# → { "message": "OTP sent to driver@test.com" }
curl -s -X POST $BASE/drivers/verify-register \
  -H "Content-Type: application/json" \
  -d '{"email":"driver@test.com","otp":"123456"}'
DRIVER_TOKEN=<access_token from above>
DRIVER_ID=<driver.id from above>

# 2. Login (password-only — no OTP required)
curl -s -X POST $BASE/users/login \
  -H "Content-Type: application/json" \
  -d '{"email":"rider@test.com","password":"Pass123!"}'
# → { "access_token": "...", "refresh_token": "...", "user": { ... } }
RIDER_TOKEN=<access_token from above>

# 3. Driver goes online and sets location
curl -s -X PATCH $BASE/drivers/$DRIVER_ID/status \
  -H "Authorization: Bearer $DRIVER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"available"}'

curl -s -X PATCH $BASE/drivers/$DRIVER_ID/location \
  -H "Authorization: Bearer $DRIVER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"lat":12.9716,"lng":77.5946}'

# 4. Rider requests a trip (Kafka matching triggers automatically)
TRIP=$(curl -s -X POST $BASE/trips/request \
  -H "Authorization: Bearer $RIDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pickup_lat":12.9716,"pickup_lng":77.5946,"drop_lat":12.9352,"drop_lng":77.6245,"payment_method":"upi"}')
TRIP_ID=$(echo $TRIP | jq -r '.trip_id')

# 5. Driver accepts the ride offer (within 60s window)
curl -s -X POST $BASE/drivers/trips/$TRIP_ID/respond \
  -H "Authorization: Bearer $DRIVER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"accept":true}' | jq .

TRIP_DETAILS=$(curl -s $BASE/trips/$TRIP_ID \
  -H "Authorization: Bearer $RIDER_TOKEN")
echo $TRIP_DETAILS | jq '{status,driver_id,ride_otp}'
# → status should be DRIVER_ASSIGNED; ride_otp is the 4-digit code the rider tells the driver
RIDE_OTP=$(echo $TRIP_DETAILS | jq -r '.ride_otp')

# 6. Driver starts and ends the trip (OTP required to start)
curl -s -X PATCH $BASE/trips/$TRIP_ID/start \
  -H "Authorization: Bearer $DRIVER_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"otp\":\"$RIDE_OTP\"}" | jq .status

curl -s -X PATCH $BASE/trips/$TRIP_ID/end \
  -H "Authorization: Bearer $DRIVER_TOKEN" \
  -H "Content-Type: application/json" -d '{}' | jq '{status,fare}'

# 7. Open checkout page in browser
PAYMENT=$(curl -s $BASE/payments/$TRIP_ID -H "Authorization: Bearer $RIDER_TOKEN")
PAYMENT_ID=$(echo $PAYMENT | jq -r '.id')
CHECKOUT_URL=$(curl -s -X POST $BASE/payments/orders \
  -H "Authorization: Bearer $RIDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"payment_id\":\"$PAYMENT_ID\"}" | jq -r '.checkout_url')
echo "Open in browser: $CHECKOUT_URL"
# UPI tab: enter  success@razorpay  as the VPA
# Card tab: use   4100 2800 0000 1007  (any expiry, any CVV)

# For cash trips — driver confirms after collecting cash:
# curl -s -X POST $BASE/payments/$PAYMENT_ID/confirm-cash \
#   -H "Authorization: Bearer $DRIVER_TOKEN"

# Dev shortcut — skip Razorpay entirely:
# curl -s -X POST $BASE/payments/simulate-success \
#   -H "Authorization: Bearer $RIDER_TOKEN" \
#   -H "Content-Type: application/json" \
#   -d "{\"payment_id\":\"$PAYMENT_ID\"}"

# 8. Rate, check notifications and payment
curl -s -X POST $BASE/trips/$TRIP_ID/rate \
  -H "Authorization: Bearer $RIDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"score":5,"comment":"Smooth ride"}' | jq .

sleep 2
curl -s $BASE/notifications/ \
  -H "Authorization: Bearer $RIDER_TOKEN" | jq '[.notifications[].message]'

curl -s $BASE/payments/$TRIP_ID \
  -H "Authorization: Bearer $RIDER_TOKEN" | jq '{status,amount}'

Payment Flow

Cash

PATCH /trips/{id}/end
  └─► trip.completed (Kafka)
        └─► payment-service: INSERT status=PENDING → AWAITING_CASH_CONFIRM

Rider opens checkout page → sees waiting state (no action needed)

Driver calls POST /payments/{id}/confirm-cash
  └─► status=COMPLETED
        └─► ridego.events {type:payment.completed} ──► notification-service
        └─► WebSocket hub broadcast ──► checkout page shows success

UPI

PATCH /trips/{id}/end
  └─► trip.completed (Kafka)
        └─► payment-service: INSERT status=PENDING

POST /payments/orders  { payment_id }
  ← { checkout_url, provider_order_id, upi_qr_url, key_id }
  payment status → PROCESSING
  (Razorpay Order + QR Code created; QR image hosted by Razorpay)

Rider opens checkout_url → UPI tab auto-selected

  ── Option A: QR Scan ────────────────────────────────────────────
  Rider scans upi_qr_url QR with any UPI app and approves payment
  └─► Razorpay fires qr_code.credited webhook
        └─► POST /payments/webhook (HMAC verified)
              └─► FindByProviderQRID → status=COMPLETED
                    └─► ridego.events {type:payment.completed} + WebSocket push → page shows success

  ── Option B: VPA Collect ────────────────────────────────────────
  Rider enters UPI ID (e.g. name@paytm) → POST /payments/checkout/{id}/upi { vpa }
  └─► Razorpay UPI collect push sent to rider's UPI app
  Rider approves in app
  └─► Razorpay fires payment.captured webhook
        └─► POST /payments/webhook (HMAC verified)
              └─► FindByProviderOrderID → status=COMPLETED
                    └─► ridego.events {type:payment.completed} + WebSocket push → page shows success

Card (Razorpay)

[1]  PATCH /trips/{id}/end
       └─► trip.completed (Kafka) → payment created  status=PENDING

[2]  GET /payments/{tripId}
       ← { id: payment_id, status: "PENDING", amount }

[3]  POST /payments/orders  { payment_id }
       ← { provider_order_id, amount, currency: "INR", key_id }
       payment status → PROCESSING

[4]  Frontend — Razorpay Checkout
       new Razorpay({
         key:      key_id,
         order_id: provider_order_id,
         amount:   amount * 100,   // paise
         currency: "INR",
         handler(response) {
           POST /payments/verify ← razorpay_payment_id + razorpay_order_id + razorpay_signature + payment_id
         }
       }).open()

[5]  POST /payments/verify
       ← HMAC-SHA256(order_id|payment_id) verified against key_secret
       ← status=COMPLETED → ridego.events {type:payment.completed} → email + in-app notification

[6]  Backup — Razorpay webhook (async, if browser closes before step 5)
       POST /payments/webhook  X-Razorpay-Signature: <hmac>
       ← idempotent: no-op if already COMPLETED

Simulate (dev / test — no Razorpay keys needed)

PAYMENT_ID=$(curl -s $BASE/payments/$TRIP_ID \
  -H "Authorization: Bearer $RIDER_TOKEN" | jq -r '.id')

curl -s -X POST $BASE/payments/simulate-success \
  -H "Authorization: Bearer $RIDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"payment_id\":\"$PAYMENT_ID\"}" | jq '{status,amount}'

Enabling Razorpay

# infra/.env
PAYMENT_PROVIDER=razorpay
RAZORPAY_KEY_ID=rzp_test_...
RAZORPAY_KEY_SECRET=...
RAZORPAY_WEBHOOK_SECRET=...   # from Razorpay Dashboard → Webhooks

The webhook URL registers automatically on startup. To set it manually: Razorpay Dashboard → Webhooks → https://your-domain.com/payments/webhook, events payment.captured and qr_code.credited.

For local testing, ngrok http 8000 gives you a public HTTPS URL Razorpay can reach.


State Machines

Trip

REQUESTED ──► DRIVER_ASSIGNED ──────────────────────────────► STARTED ──► COMPLETED
    │               │            driver submits ride OTP           │           └─ payment created, rating unlocked
    │               │            (4-digit, shown to rider in       └── /end
    │               │             GET /trips/{id} response)
    │               └── /cancel ──► CANCELLED
    │                              └─ driver restored to GEO pool; ridego.events {trip.cancelled}
    └── auto-cancel after 10 min (no driver found) ──► CANCELLED
                    └─ trip-service poller publishes ridego.events {trip.cancelled}

Payment

cash:  PENDING ──► AWAITING_CASH_CONFIRM ──► COMPLETED  (driver confirms)
upi:   PENDING ──► PROCESSING ──► COMPLETED  (Razorpay QR scan or VPA collect webhook)
card:  PENDING ──► PROCESSING ──► COMPLETED  (Razorpay signature verified or webhook)
                       └────────► FAILED

Every path ends with ridego.events {type:payment.completed} → email + in-app notification to the rider.

Fare Formula

fare = base + per_km × distance_km × surge_multiplier   (surge capped at 5×)
Category Base Per km Typical vehicle
go ₹30 ₹8/km hatchback / compact
x ₹50 ₹12/km sedan (default)
xl ₹80 ₹16/km SUV / MUV

Driver-supplied distance is trusted only if it's within 1.5× the haversine and under 200 km. Outside that, haversine wins.


Testing

# unit tests (no infra needed)
go test uber/shared/... uber/user-service/... uber/driver-service/... \
  uber/trip-service/... uber/notification-service/... uber/payment-service/...

make build          # build all services
bash test/test_all.sh  # e2e (needs the stack running)

test/postman_collection.json has the full flow with auto-captured tokens and IDs — import it and run the collection top to bottom.


Created with ❤️ by Mulukutla Sai Krishna

Releases

No releases published

Packages

 
 
 

Contributors