Building MURI
How I architected a multi-role transportation platform with geospatial routing, real-time trip tracking, and package-based subscription management — deployed and live at muri.sa.
01The Problem
Threeusertypes.Onecoherentsystem.
Transportation platforms look simple on the surface — a passenger books, a driver accepts, the trip happens. The complexity is in the seams: how do you give three completely different user types — clients, drivers, and administrators — a single backend that enforces the right rules for each without tangling the logic?
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
- Isolated permission scopes for clients, drivers, and administrators
- Geospatial queries for driver availability and region management
- Subscription-gated trip booking with atomic quota checks
- Real-time trip status pushed to both client and driver without polling
- Full Arabic and English localization across all user-facing content
02Architecture
Fivelayers.Role-awareateverylevel.
The system is organized into modular Django apps — one per role and concern — sharing a single PostGIS-enabled database with a clear async layer for tasks and real-time updates.
Client · Driver · Admin Apps
Role-based entry points with separate permission scopes
Django REST API
Business logic, authentication (Knox), data validation
PostGIS + PostgreSQL
Geospatial routing, region management, trip records
Redis + Celery
Message broker, async task queue, hot-path caching
WebSocket Layer
Real-time trip tracking pushed to client and driver apps
AWS S3 (async)
Media storage — driver documents, profile images — handled as Celery tasks, never blocking the request cycle
03Key Decisions
Whatwechoseandwhy.
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.
Modular app structure by role, not by feature
The alternative — a single 'users' model with a role field — leads to spaghetti permissions. Separating client, driver, and administrator into distinct Django apps meant each had its own serializers, viewsets, and permission classes. Onboarding a new role never touches existing code.
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.
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.
User roles in one system
Client · Driver · Admin — fully isolated permissions
Geospatial engine
Region-aware routing with native geo-query performance
Trip status update delivery
WebSocket push vs. polling
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.