All case studies
Mobility & Trip Management· 9 min read

Building MURI

How I architected a student transportation platform serving three roles — client, driver, and admin — each with its own app, backed by a modular Django API with PostGIS routing, subscription packages, and real-time trip tracking. Live at muri.sa.

Live PlatformRole: Backend Engineer & Team Lead · ASAS IT

01The Problem

Client,driver,adminoneplatform.

MURI is a student transportation platform. On the surface it looks simple — a client books a trip, a driver accepts it, an admin oversees operations. The complexity is in the seams: three completely different user types, each needing their own experience, all sharing one backend that enforces the right rules without tangling the logic.

Each role gets a dedicated application: the Student App (Flutter) for clients, the Driver App (Flutter) for drivers, and the Admin Dashboard (Angular 19) for administrators. The Django backend mirrors this split — separate modules under UsersModules/ for client, driver, and administrator, each with its own serializers, views, and permission classes.

MURI added a further dimension: subscription packages. Clients don't pay per trip — they buy packages. This means trip availability is gated by subscription state, expiry, and remaining quota — all of which must be checked atomically to avoid race conditions when two requests arrive simultaneously.

And throughout, the system needs to answer geographic questions in real time: which drivers are within range? What regions are serviced? What is the estimated route? A plain latitude/longitude column in PostgreSQL cannot answer these efficiently at scale.

Core requirements

  • Three role-specific apps (Student, Driver, Admin) backed by one modular Django API
  • Isolated permission scopes for client, driver, and administrator user types
  • Geospatial queries for driver availability and service region management
  • Subscription-gated trip booking with atomic quota checks
  • HyperPay payments, driver wallet accounting, and Firebase push notifications
  • Full Arabic and English localization across all apps and the admin dashboard

02Architecture

Threeapps.OneAPI.Cleardataflow.

Each role gets its own frontend; the Django backend is organized into modular apps — one per role and concern — sharing a single PostGIS-enabled database with Celery for async work and Firebase for push delivery.

Student App (Flutter)

Client role — subscriptions, trips, live tracking, payments

Driver App (Flutter)

Driver role — trip acceptance, navigation, wallet & earnings

Admin Dashboard (Angular 19)

Administrator role — users, vehicles, regions, payments, ops

Django REST API

Modular UsersModules (client / driver / admin), JWT auth, DRF

PostGIS + PostgreSQL

Geospatial routing, region polygons, trip records

Redis + Celery

Task queue, caching, subscription sync, payment webhooks

+ HyperPay

Real-time Layer

Firebase push + WebSocket trip updates to client and driver apps

Payments + AWS S3 (async)

HyperPay gateway with webhook reconciliation, driver wallet accounting, and S3 media uploads — all off the request cycle via Celery

03Key Decisions

Whatwechoseandwhy.

01

PostGIS over plain lat/lng columns

Storing coordinates as two float columns works until you need to query 'find all drivers within 5km'. That query becomes a full table scan with manual Haversine math. PostGIS gives us native spatial indexes, proximity queries in a single SQL call, and region polygon support — all without a separate geospatial service.

02

One Django module per role — mirrored in the frontends

The backend splits client, driver, and administrator into distinct Django apps under UsersModules/, each with its own models, serializers, views, and permission classes. The frontends mirror this: Student App and Driver App (Flutter, clean architecture) and Admin Dashboard (Angular 19) each talk only to their role-scoped API surface.

03

WebSocket push for trip tracking

Polling every 2 seconds for driver location means 30 requests per minute per active trip. At modest scale this becomes expensive. WebSocket connections stay open for the duration of the trip, pushing location updates server-side. Driver location is updated via Celery → Redis → WebSocket channel group.

04

Atomic subscription quota checks with Redis locks

When two concurrent requests try to book the last trip in a subscription package, a naive check-then-decrement approach creates a race condition. Redis distributed locks ensure the quota check and decrement happen atomically — one request wins, the other gets a clear 'quota exhausted' response.

04Outcomes

Asystembuilttoscalewiththemarket.

3

User roles

Client · Driver · Admin — fully isolated permissions

PostGIS

Geospatial engine

Region-aware routing with native geo-query performance

<1s

Trip status update delivery

WebSocket push vs. polling

2

Languages supported

Full Arabic and English localization (i18n)

05Challenges & Lessons

Whatmadethishard.

Geospatial complexity

PostGIS documentation is dense. Getting spatial indexes right for proximity queries took iteration — the wrong index type caused slow queries under load that only surfaced in staging.

Real-time consistency

Keeping trip state in sync between client, driver, and admin views required careful ordering of WebSocket events. We built an event log to replay missed updates on reconnection.

Permission surface area

Multi-role systems have a large permission surface. We built a custom permission matrix tested with over 50 test cases covering edge conditions — an admin shouldn't be able to create a trip as a client.

Next

Read the Hayyak case study

How I integrated Opera PMS and dual payment gateways into a multi-tenant hotel system.