Backend for a simple ride sharing workflow built with Go, gRPC, and MySQL. It supports Google Sign‑In (via ID token) to issue backend JWTs, and provides APIs for users, rides (offers/requests), matching, chat, reviews, and location.
This service is a focused, production-style gRPC backend for coordinating rides between people in the same network (e.g., a campus or company domain). Users authenticate with Google, the backend verifies the ID token with Google, then issues its own JWT for subsequent calls. Once authenticated, users can:
- Create ride offers (drivers) or ride requests (riders)
- Get matched in two ways: riders can request to join an offer, or drivers can accept a rider’s request (which creates a match)
- Chat only when a match is accepted/completed (enforced by business rules)
- Share last-known location using geohash prefix queries for nearby lookups
- Review each other after a ride
The codebase is organized with clear layering: gRPC handlers -> services (business rules) -> repositories (GORM) -> MySQL. Cross-cutting concerns (auth) are handled with a gRPC interceptor, and dependencies are wired with Google Wire for clean construction and testability.
- Login: Client gets a Google ID token and calls
AuthService/Login. The server verifies the token with Google, checks theaudagainstGOOGLE_CLIENT_ID, ensures the email is verified and belongs to anALLOWED_DOMAINSdomain, creates the user if needed, and returns a backend JWT. - Profile & presence: Authenticated users can fetch/update their profile and upsert their current location. Location is stored along with a geohash for prefix-based proximity queries.
- Supply and demand:
- Drivers post ride offers with route, time, seats, and optional fare.
- Riders post ride requests with route, time, and seats.
- Matching:
- Riders can
RequestToJoina driver’s offer (driver laterAcceptRequestorRejectRequest). - Drivers can
AcceptRideRequestdirectly on a rider’s request, which creates an offer+match and marks the request as matched.
- Riders can
- Communication: After a match is accepted/completed, participants can use
ChatService/SendMessageon that ride ID. The service enforces that only the matched rider/driver can send. - Wrap-up: Driver (or system) marks the match
completed, and users can submit reviews. Listing RPCs exist to retrieve a user’s data, nearby offers/requests, messages, matches, and reviews.
- gRPC server with reflection enabled. Middleware intercepts all non-public RPCs and enforces JWT auth. The interceptor injects
user_idandemailinto the request context for handlers. - Handlers translate protobufs and call services. Services enforce business rules like match eligibility, message permissions, and status transitions. Repositories perform GORM queries on MySQL. Auto-migrations run on startup.
- Dependency Injection via Google Wire assembles handlers, services, and repositories from a single provider set for a clean, testable composition.
- Go (gRPC, Protobuf)
- GORM (MySQL)
- Google Wire (DI)
- JWT (HS256)
- godotenv
main.go: gRPC server bootstrap, auth interceptor, service registrationapi/: gRPC handlers (one per service)service/: business logicrepository/: data access with GORMdb/: GORM models and hooksconfig/: environment config and DB initializationdi/: dependency injection via Wire (wire.go, generatedwire_gen.go)proto/v1/: protobuf definitions and generated code
- Go 1.24+
- MySQL instance accessible from the app
The server loads environment variables from .env (via godotenv).
# gRPC
GRPC_PORT=8080
# DB
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=yourpassword
DB_NAME=hope
# Auth
JWT_SECRET=your-long-random-secret
GOOGLE_CLIENT_ID=your-google-oauth-client-id
ALLOWED_DOMAINS=example.com,another.comNotes:
- Database DSN used:
user:password@tcp(host:port)/db?parseTime=True&loc=Local. - Auto-migrations run on startup for all models in
db/.
go run ./...The server listens on :${GRPC_PORT} (default :8080). gRPC reflection is enabled.
Only proto.v1.AuthService/Login is public. All other RPCs require a Bearer token in the metadata header:
authorization: Bearer <JWT>
Login flow:
- Client obtains a Google ID token.
Loginverifies token via Google, checksaudagainstGOOGLE_CLIENT_ID, ensuresemail_verified, enforcesALLOWED_DOMAINS.- A user is created if not present; then a backend JWT (HS256, 24h) is returned.
Example (login):
grpcurl -plaintext \
-d '{"id_token":"<google-id-token>"}' \
localhost:8080 proto.v1.AuthService/LoginExample (authenticated call):
grpcurl -plaintext \
-H "authorization: Bearer <jwt>" \
-d '{}' \
localhost:8080 proto.v1.UserService/GetMeAll services are under the proto.v1 package.
-
AuthService
Login(LoginRequest) -> LoginResponse(public)
-
UserService
GetMe(GetMeRequest) -> GetMeResponse(auth)GetUser(GetUserRequest) -> GetUserResponse(auth)UpdateMe(UpdateMeRequest) -> UpdateMeResponse(auth)ListUsers(ListUsersRequest) -> ListUsersResponse(auth)
-
RideService
- Offers
CreateOffer(CreateOfferRequest) -> CreateOfferResponse(auth)GetOffer(GetOfferRequest) -> GetOfferResponse(auth)UpdateOffer(UpdateOfferRequest) -> UpdateOfferResponse(auth)DeleteOffer(DeleteOfferRequest) -> DeleteOfferResponse(auth)ListNearbyOffers(ListNearbyOffersRequest) -> ListNearbyOffersResponse(auth)ListMyOffers(ListMyOffersRequest) -> ListMyOffersResponse(auth)
- Requests
CreateRequest(CreateRequestRequest) -> CreateRequestResponse(auth)GetRequest(GetRequestRequest) -> GetRequestResponse(auth)UpdateRequestStatus(UpdateRequestStatusRequest) -> UpdateRequestStatusResponse(auth)DeleteRequest(DeleteRequestRequest) -> DeleteRequestResponse(auth)ListNearbyRequests(ListNearbyRequestsRequest) -> ListNearbyRequestsResponse(auth)ListMyRequests(ListMyRequestsRequest) -> ListMyRequestsResponse(auth)
- Offers
-
MatchService
RequestToJoin(RequestToJoinRequest) -> RequestToJoinResponse(auth)AcceptRideRequest(AcceptRideRequestRequest) -> AcceptRideRequestResponse(auth)AcceptRequest(AcceptRequestRequest) -> AcceptRequestResponse(auth)RejectRequest(RejectRequestRequest) -> RejectRequestResponse(auth)CompleteMatch(CompleteMatchRequest) -> CompleteMatchResponse(auth)GetMatch(GetMatchRequest) -> GetMatchResponse(auth)ListMatchesByRide(ListMatchesByRideRequest) -> ListMatchesByRideResponse(auth)ListMatchesByRider(ListMatchesByRiderRequest) -> ListMatchesByRiderResponse(auth)ListMyMatches(ListMyMatchesRequest) -> ListMyMatchesResponse(auth)
-
ChatService
SendMessage(SendMessageRequest) -> SendMessageResponse(auth)ListMessagesByRide(ListMessagesByRideRequest) -> ListMessagesByRideResponse(auth)ListMessagesBySender(ListMessagesBySenderRequest) -> ListMessagesBySenderResponse(auth)ListChatsForUser(ListChatsForUserRequest) -> ListChatsForUserResponse(auth)
-
LocationService
UpsertLocation(UpsertLocationRequest) -> UpsertLocationResponse(auth)GetLocationByUser(GetLocationByUserRequest) -> GetLocationByUserResponse(auth)ListNearby(ListNearbyRequest) -> ListNearbyResponse(auth)DeleteMyLocation(DeleteMyLocationRequest) -> DeleteMyLocationResponse(auth)
Below is how I designed and implemented each RPC end‑to‑end. I describe the handler (gRPC edge), service (business rules), and repository (DB), and why I made those choices.
- Login
- What: Exchange a Google ID token for my backend JWT and create a user record if needed.
- How:
api/auth_handlers.govalidates payload and callsservice/auth_service.go:- I verify the Google token with Google (
verifyGoogleIDTokenviahttps://oauth2.googleapis.com/tokeninfo). - I check
aud == GOOGLE_CLIENT_ID, ensureemail_verified, and enforceALLOWED_DOMAINS. - I upsert the user through
UserRepository(create if not found), then issue HS256 JWT with claimssub,email,name.
- I verify the Google token with Google (
- Why: Delegating identity to Google reduces auth surface area. Domain allowlist keeps the product scoped (e.g., campus/company). JWT keeps the server stateless.
- GetMe
- What: Return the authenticated user.
- How: I read
user_idfrom context (set bymiddleware.AuthInterceptor) and fetch viaUserService.GetUserByID→UserRepository.FindByID. - Why: Context‑injected identity avoids passing user IDs over the wire for self‑queries.
- GetUser
- What: Fetch any user by ID.
- How: Simple pass‑through to service/repo with not‑found handling.
- Why: Needed to render counterpart profiles.
- UpdateMe
- What: Update my profile fields (name, photo_url, geohash).
- How: Load current user, mutate only provided fields, save via
UserRepository.Update. - Why: Partial updates prevent unintended overwrites and keep the RPC small.
- ListUsers
- What: Batch fetch a small set of users by IDs.
- How: Iterate IDs and call
GetUserByIDfor each. - Why: The list is expected to be small (chat headers, match cards); this keeps logic simple.
- CreateOffer
- What: Drivers post an offer (route, time, seats, fare).
- How: Handler validates required fields and auth; service trims input, enforces future time and positive seats, generates ID, verifies driver exists via
UserRepository, and persists viaRideOfferRepository.Create. - Why: Validations protect integrity; verifying driver existence catches dangling refs.
- GetOffer
- How/Why: Lookup by ID via repo; returns
NotFoundif missing. Straightforward read path.
- How/Why: Lookup by ID via repo; returns
- UpdateOffer
- What: Partial update (fare/seats/status) by the owner.
- How: I load current offer, authorize that caller is the driver (in handler), apply only provided fields, and
Save. - Why: Owner‑only updates and partial mutation keep state consistent.
- DeleteOffer
- How/Why: Owner check in handler, then
Deleteby ID. Prevents unauthorized deletions.
- How/Why: Owner check in handler, then
- ListNearbyOffers
- What: Query offers by
from_geogeohash prefix. - How: Repo uses
LIKE geohash_prefix%with optionalLIMIT, ordered by time ASC. - Why: Prefix queries are a simple, fast approximation for proximity without a geo index.
- What: Query offers by
- ListMyOffers
- What: Caller’s offers.
- How: Read
callerIDfrom context; repo filters bydriver_idwith optional limit. - Why: Common dashboard view for drivers.
- CreateRequest
- What: Riders post a request (route, time, seats).
- How: Service validates, trims, ensures future time and positive seats, sets default
status=active, generates ID, verifies user exists, then persists viaRideRequestRepository.Create. - Why: Symmetric to offers; keeps model consistent.
- GetRequest
- How/Why: Lookup by ID; errors map to
NotFoundat the handler.
- How/Why: Lookup by ID; errors map to
- UpdateRequestStatus
- What: Change status of my request.
- How: Handler authorizes ownership; service trims inputs and updates status via repo.
- Why: Only request owner should transition their request.
- DeleteRequest
- How/Why: Owner check in handler; repo delete by ID.
- ListNearbyRequests
- How/Why: Same geohash prefix approach as offers, ordered by time ASC.
- ListMyRequests
- How/Why: Caller’s requests filtered by
user_idwith optional limit.
- How/Why: Caller’s requests filtered by
- RequestToJoin
- What: Rider asks to join an existing offer.
- How: Handler sets
rider_idfrom context; service validates, loads offer byride_id, copiesdriver_idfrom the offer, setsstatus=requested, stampscreated_at, and creates the match. - Why: Centralizes driver identity on the server, avoids spoofing.
- AcceptRideRequest
- What: Driver accepts a rider’s request (creates an offer+match and marks the request matched).
- How: Service loads the request, checks
active, prevents self‑accept, synthesizes a new offer for the driver (statusmatched), creates a match withacceptedstatus, and updates the original request tomatched. - Why: Supports the inverse flow (driver initiates) while preserving invariants atomically at the service layer.
- AcceptRequest / RejectRequest
- What: Driver decision on a
requestedmatch. - How: Service enforces caller is the
driver_id, checks currentstatus=requested, then moves toacceptedorrejected. - Why: Prevents riders from self‑approving; keeps a clear state machine.
- What: Driver decision on a
- CompleteMatch
- What: Mark a match completed.
- How: Service updates status to
completed. - Why: Minimal flow completion; permissioning is intentionally simple here.
- GetMatch / ListMatchesByRide / ListMatchesByRider / ListMyMatches
- How/Why: Standard reads.
ListMyMatchesuses caller identity for convenience.
- How/Why: Standard reads.
- SendMessage
- What: Send a message scoped to a ride.
- How: Service generates ID and timestamp, then authorizes the sender by loading matches for the ride and ensuring the sender is either the rider or driver on a match with
acceptedorcompletedstatus; writes viaChatMessageRepository.Create. - Why: Enforces that only matched participants can chat; prevents arbitrary ride spam.
- ListMessagesByRide / ListMessagesBySender / ListChatsForUser
- How: Repos filter by ride, sender, or user with
beforeandlimit, ordered by timestamp DESC. - Why: Pagination‑ready access patterns without complex indices.
- How: Repos filter by ride, sender, or user with
- UpsertLocation
- What: Save my last known location and geohash.
- How: Handler pulls
user_idfrom context and builds the location; service validates lat/lon bounds, setsupdated_at, and calls repo upsert (MySQLON CONFLICT‑style via GORM clauses). - Why: Idempotent writes let clients update frequently without worrying about record existence.
- GetLocationByUser
- How/Why: Simple lookup by
user_id, with a clearNotFoundmapping.
- How/Why: Simple lookup by
- ListNearby
- How/Why: Geohash prefix search with optional limit, ordered by
updated_atDESC to show freshest first.
- How/Why: Geohash prefix search with optional limit, ordered by
- DeleteMyLocation
- How/Why: Remove my row; useful for privacy or sign‑out flows.
- RideService/CreateOffer
grpcurl -plaintext \
-H "authorization: Bearer <jwt>" \
-d '{
"from_geo":"u4pruy",
"to_geo":"u4pruv",
"fare": 10.5,
"time": {"seconds": 1893456000},
"seats": 3
}' \
localhost:8080 proto.v1.RideService/CreateOffer- LocationService/UpsertLocation
grpcurl -plaintext \
-H "authorization: Bearer <jwt>" \
-d '{"latitude":28.61, "longitude":77.20, "geohash":"tsq4"}' \
localhost:8080 proto.v1.LocationService/UpsertLocationUser: id, name, email (unique), photo_url, geohash, last_seen; has oneUserLocationRideOffer: id, driver_id, from_geo, to_geo, fare, time, seats, statusRideRequest: id, user_id, from_geo, to_geo, time, seats, statusMatch: id, rider_id, driver_id, ride_id, status, created_atChatMessage: id, ride_id, sender_id, content, timestampReview: id, ride_id, from_user_id, to_user_id, score, comment, created_atUserLocation: user_id, latitude, longitude, geohash, updated_at
Auto-migrations run on startup for all the above.
Constructed via di/wire.go and generated di/wire_gen.go. If you change providers or constructor signatures, regenerate with Wire.