Phase 0–1 Deliverable · Discovery & Architecture
Ma3ak Fe Tareeqak & Ma3ak Delivery
Community mobility & delivery platform for Zahraa El Asmaa / Zahraa Capital — Badr City, built to expand city-by-city without a rewrite.
01 Executive Summary
Zahraa Capital's Zone 3 has a real, narrow transportation problem: people already travel in and out of the area by car, but there is no structured way for someone with a spare seat to be found by someone who needs one. Ma3ak Fe Tareeqak solves that first, narrow problem — a community ride board, not a dispatched taxi service — and Ma3ak Delivery reuses the same trust, verification, and matching machinery for small local deliveries.
The two services share one account model, one verification pipeline, one admin panel, and one database. A resident registers once; becoming a driver or delivery provider is an additional, admin-verified capability on the same account, not a different product. This is the single most important structural decision in the whole system and it shapes almost every table and screen described below.
Given the deployment target is ordinary shared PHP/MySQL hosting (no Docker, no queue workers, no guaranteed shell access to install system packages), the architecture is deliberately a clean, dependency-light custom PHP MVC rather than a full framework — see §4 for the reasoning. Everything is designed so a future native mobile app, GPS/maps, online payments, and multi-city expansion can be added additively, without re-architecting the core.
This document is the output of Phase 0 (Discovery) and Phase 1 (Architecture). Per your own instructions, no implementation code follows this document. Section 18 lists the decisions and access I need from you before Phase 2 (Foundation) can start — most importantly, working SSH credentials for zahara.m3lsh.com (port 22 timed out with no prior config, so I could not inspect the real PHP version, extensions, or Composer availability before writing this — a few choices below are marked confirm on host for that reason).
02 Assumptions
Stated explicitly so they can be corrected rather than silently baked in.
- Legal form: this is a community-organized service, not a licensed taxi/courier dispatch company. No claim of regulatory compliance with Egyptian transportation law is made anywhere in the product (see §7 and the Legal Pages module) until you've had professional legal review.
- Single currency: EGP only for the MVP; no multi-currency support is built now (the
currencycolumn exists purely so it isn't a breaking schema change later). - Single timezone: Africa/Cairo, stored in UTC in the database, rendered in local time.
- Cash-only MVP: per your spec, no payment gateway integration now —
payment_methods/transactionsexist as a real but currently CASH-only abstraction. - No live GPS tracking in the MVP — locations are City → Area → Zone → landmark text, exactly as specified.
latitude/longitudecolumns exist and are nullable everywhere a location is recorded. - Single admin organization initially (one system admin, a handful of branch admins), not a multi-tenant SaaS product — branches model service lines (ride vs. delivery vs. future lines) inside one operator, not independent customers.
- Traffic volume is modest (a single residential community, growing to a handful of neighborhoods) — this justifies avoiding queues, caches, and horizontal scaling infrastructure now (see §4).
- Shared hosting has: PHP 8.1+, MySQL/MariaDB,
.htaccesssupport (Apache), cron access, and SSH for deployment. unconfirmed — see §18.
03 Risks & Concerns
Ride-matching for a fee sits close to informal transportation regulation in Egypt. The spec already asks for a "legal compliance placeholder," which is correct — I'm flagging it here as a genuine risk, not just a checkbox: do not launch publicly, and do not process real money at scale, before a lawyer has reviewed the Terms of Service, driver agreement, and liability model. The architecture keeps this swappable (see §7, §65) but cannot make the business legally safe by itself.
Shared hosting environments vary a lot in how strictly they isolate one account's files from another and in whether .htaccess directives are honored consistently. The document security model in §13 assumes we can (a) place private files outside the public web root, or (b) deny direct access inside it via .htaccess and serve everything through a PHP gate. Both are standard, but must be verified on the actual host before the first ID document is ever uploaded.
Explicitly called out in your spec, and correctly so — this is the single most common correctness bug in ride-sharing-style systems. Addressed structurally in §9 and §10 via atomic conditional UPDATE statements, never "read-then-write" from PHP. This is non-negotiable and will be unit- and load-tested (§14).
Without GPS, "safety" for the MVP reduces to: verified identity, a visible rating history, and a working complaint/admin-intervention path. That is real but limited — it should be communicated honestly to users, not oversold as a "safety platform," until Phase 2+ safety features (§69) exist.
Not a technical risk, but an architectural one: a ride/delivery board with too few active drivers looks broken. The "I'm going there" proactive-publish flow (§6, §10) exists specifically to solve this — it lets a handful of regular commuters seed supply even before formal ride requests exist.
04 Design Decisions Made (and why)
Your spec explicitly asked me to push back where something would be weak, unsafe, or overengineered rather than follow it blindly. Here is every place I made a judgment call.
No framework — a small custom MVC, not Laravel
Laravel is an excellent framework, but on unconfirmed shared hosting it adds real deployment risk: it needs Composer to be runnable on the host (or a build-and-upload step), a writable storage/ tree with specific permissions, a scheduler that depends on cron being configurable, and generally assumes more control over the PHP environment than shared hosting reliably grants. None of Laravel's flagship value (Eloquent, Artisan, queues, Blade) is load-bearing for a system this size — a request/response cycle, PDO with prepared statements, a router, and a handful of service classes cover 100% of the requirements above. Decision: a small, explicit PHP 8.x MVC (see §8), Composer used only for autoloading and 2–3 tiny, framework-agnostic libraries if the host supports it, with a documented zero-Composer fallback if it doesn't.
Tailwind CSS + Alpine.js, not Bootstrap, not a JS framework
Bootstrap's RTL build is turnkey but visually generic, and the brand asks for a distinctive, restrained pastel identity — fighting Bootstrap's own opinionated components to get that look creates more work, not less. A full JS framework (React/Vue) would need a build pipeline and turns every page into an API-consuming SPA, which is unnecessary for a server-rendered, mobile-first app and complicates the "PHP renders HTML" mental model this system otherwise has throughout. Decision: Tailwind CSS (compiled once at build time into a static CSS file — no Node.js needed on the production host) for layout/visual system, with Alpine.js (≈15 KB, no build step) for the sprinkles of interactivity server-rendered pages need — bottom sheets, tabs, toasts, confirm dialogs. This is a well-worn, low-risk pairing for exactly this kind of project.
One polymorphic documents table, not three
Your spec's suggested schema lists vehicle_documents and user_documents separately. They have identical lifecycle, state machine, storage, and review UI — the only difference is what they're attached to. Duplicating the table means duplicating the review queue, the expiration job, and the access-control code three times, which is exactly the "repeated logic" your spec asks me to avoid. Decision: one documents table with owner_type (USER / VEHICLE) + owner_id, and a document_types lookup table so admins can add new required document types later without a schema change.
Ride "offers" and "requests" collapse into three tables, not five
Read literally, the spec's ride section implies separate ride_requests, ride_offers, and rides tables, with an ambiguous relationship between a driver's "offer" (in the pricing sense) and a driver's "offer" (in the published-trip sense, §10). I've resolved this into: ride_offers = a driver's published trip ("I'm going there"), ride_requests = a passenger's search intent (optional — a passenger can also just browse and book an offer directly), and ride_bookings = the actual stateful link between one passenger and one ride offer, carrying the full state machine from §13. This is fewer tables, no loss of the required states, and one obvious place to look for "what's the status of this ride."
Roles ARE capabilities — implemented as many-to-many, not a single role column
This is the load-bearing decision from your §4. user_roles is a join table, not a single enum column on users — a user can hold DRIVER and DELIVERY_PROVIDER simultaneously. Current availability (§9) is a completely separate concept from capability (§5) and is modeled in its own table, keyed by which capability is currently active.
Sessions for the web app, bearer tokens for the future API — not JWT, not OAuth2
The web/PWA app authenticates with ordinary secure PHP sessions (simplest, safest default for a server-rendered app). The REST API (needed today for the PWA's own AJAX calls, and reused unchanged by a future native app) authenticates with a long-lived, hashed personal_access_token per device — the same pattern Laravel Sanctum popularized, implemented directly, without pulling in a JWT library or standing up an OAuth2 authorization server neither of which this system's threat model or client mix actually needs yet.
No maps/GPS, no chat, no queue, no cache — deliberately deferred
Consistent with §78 of your spec ("do not overengineer"). Matching in the MVP is a deterministic SQL query (area/zone match, time window, seat count), not a geospatial one. Notifications are DB-backed and rendered in-app/PWA; there's no Redis, no message queue, no job runner — a lightweight cron script handles the two time-based jobs the system actually needs (document expiration sweep, stale-offer expiry sweep).
05 Roles, Capabilities & Permissions
Two layers that must not be confused: capabilities (what a user is allowed to be — passenger, driver, delivery provider, admin) and availability (what a verified provider is doing right now — offline / available / busy). Only the first layer maps to permissions.
Capability catalog
| Capability key | Granted | Requires verification? |
|---|---|---|
NORMAL_USER | Automatically, on registration | No |
DRIVER | By admin, after driver + vehicle document review | Yes — §6, §7 |
DELIVERY_PROVIDER | By admin, after identity (+ vehicle, if applicable) review | Yes |
BRANCH_ADMIN | By a system admin, scoped to one or more branches | No (internal staff) |
SYSTEM_ADMIN | By another system admin (never self-service) | No |
Permission matrix
| Permission | Normal user | Driver | Delivery provider | Branch admin | System admin |
|---|---|---|---|---|---|
ride.request.create | ✅ | ✅ | ✅ | — | ✅ |
ride.offer.publish | — | ✅ if verified | — | — | — |
ride.booking.accept_reject | — | own offers only | — | — | — |
delivery.request.create | ✅ | ✅ | ✅ | — | ✅ |
delivery.accept_reject | — | — | own only, if verified | — | — |
document.upload_own | ✅ | ✅ | ✅ | — | — |
document.review | — | — | — | own branch | ✅ all |
document.view_sensitive | — | — | — | own branch, logged | ✅, logged |
user.manage | — | — | — | own branch scope | ✅ |
user.suspend | — | — | — | own branch scope | ✅ |
branch.manage | — | — | — | — | ✅ |
area.manage | — | — | — | — | ✅ |
pricing_policy.manage | — | — | — | — | ✅ |
complaint.manage | own only (create/view) | own only | own only | own branch | ✅ |
audit_log.view | — | — | — | own branch actions | ✅ |
admin.manage | — | — | — | — | ✅ |
Permissions are checked server-side, on every request, via a small policy class per resource (e.g. RideBookingPolicy::canAccept($user, $booking)) — never by hiding a button in the template. Ownership checks (§13 IDOR requirement) live in the same policy layer: a driver can only accept bookings on their own ride offers, a branch admin can only review documents belonging to users assigned to their branch.
06 Complete User Flows
6.1 Registration → becoming a verified driver
6.2 Ride: from search to completion (passenger side)
6.3 "I'm going there" (driver proactively publishes)
6.4 Delivery: creation to completion
6.5 Document rejection loop
6.6 Document expiration (automatic, cron-driven)
07 Business Rules (centralized, server-enforced)
Every rule below is enforced in one place — a service class the API/controllers call — never duplicated between the web controller and a future mobile API controller.
Driver / provider eligibility (checked before every state-changing action, not just at go-available time)
- Cannot publish a ride offer or go
AVAILABLEunlessdriver_profiles.verification_status = APPROVEDand every required document for that user+vehicle isAPPROVEDand not expired, andusers.status = ACTIVE. - A document expiring mid-availability immediately forces
AVAILABLE → OFFLINE(checked by the expiration sweep, and re-checked defensively on every accept/publish action, not trusted from a cached flag). - Cannot accept a booking/delivery beyond capacity — enforced by the atomic seat/slot update in §9, never by an application-level "check then write."
- Cannot accept a delivery already accepted by someone else — same atomic-conditional-update pattern, first writer wins, everyone else gets a clean "تم قبول هذا الطلب من مزود آخر" instead of a race.
Passenger / customer rules
- Cannot accept/request an
EXPIREDor already-FULLoffer (re-validated server-side at request time, not just filtered out of the list client-side). - Cannot rate a ride/delivery that isn't
COMPLETED, and cannot rate the same completed service twice (UNIQUE(service_type, service_id, rater_id)).
Account status gates
SUSPENDED/BLOCKEDusers cannot create requests, publish offers, accept anything, or message support beyond viewing their own complaint. Existing in-flight rides/deliveries are not silently deleted — they either complete normally or an admin explicitly cancels/reassigns them (audit-logged).
08 System Architecture
A single PHP application, server-rendered, following a light MVC + service-layer split. No microservices, no separate API server — the REST API (§11) is served by the same app, same codebase, same database, just a different route prefix and auth guard.
Browser / PWA ──┐
│ HTTPS
Future Mobile App┘
│
▼
Apache/LiteSpeed (shared hosting)
│ .htaccess routes everything to public/index.php
▼
Front Controller (public/index.php)
│
├─ Middleware pipeline: HTTPS-enforce → session/token auth → CSRF (web only)
│ → rate limit → route → permission policy
▼
Router ──▶ Controllers (thin: parse input, call service, render/respond)
│
▼
Services (business rules, state machines, transactions) ◀── this is where §7 lives
│
▼
Repositories (PDO, prepared statements only)
│
▼
MySQL 8 / MariaDB (utf8mb4, InnoDB, FKs)
Cross-cutting: Logger, Notification dispatcher (IN_APP now; PUSH/SMS/EMAIL/WHATSAPP
as pluggable channel drivers later), Document storage adapter (local disk now;
swappable for S3-compatible storage later), Audit log writer.
Proposed project structure
/app
/Controllers Web/{...}.php Api/{...}.php
/Services RideService, DeliveryService, VerificationService,
AvailabilityService, NotificationService, ...
/Repositories one per aggregate (RideOfferRepository, ...)
/Policies ownership + permission checks per resource
/Models thin data objects, no active-record magic
/Middleware
/Validators
/Support Fsm.php (generic state-machine helper), Money.php, Slug.php
/config config.php reads getenv(), never hard-coded secrets
/database
/migrations plain numbered .sql files, applied by a small migrate.php runner
/seeders seed_dev.php (clearly marked demo data), seed_reference.php (cities/areas/zones, doc types, roles)
/public
index.php single front controller
/assets compiled Tailwind CSS, JS, icons, manifest.json, sw.js
/resources
/views PHP templates, partial-based, RTL by default
/lang ar.php (primary), en.php (admin fallback where useful)
/routes
web.php session-authenticated, CSRF-protected
api.php token-authenticated, versioned under /api/v1
/storage
/private/documents/{users,vehicles}/... never web-served directly
/logs
/tests
/docs
09 Database Architecture
39 tables, grouped by concern. utf8mb4_unicode_ci, InnoDB, explicit foreign keys, soft-delete (deleted_at) on user-facing entities that must survive for history/audit, hard timestamps everywhere. Full column-level DDL is written during Phase 2 from this design — reproduced here at entity/relationship level so it can be reviewed before a single CREATE TABLE is run.
9.1 Identity, roles & capabilities
| Table | Purpose | Key columns |
|---|---|---|
users | One row per person, ever. | id, full_name, phone (unique), email (nullable, unique), password_hash, profile_photo_path, status ENUM(PENDING,ACTIVE,SUSPENDED,BLOCKED,DELETED), created_at, updated_at, deleted_at |
roles | Capability catalog (§5). | id, key UNIQUE, name_ar, name_en, description |
permissions | Fine-grained permission catalog. | id, key UNIQUE, name_ar, description |
role_permissions | M:N role↔permission. | role_id FK, permission_id FK, PK(role_id, permission_id) |
user_roles | M:N user↔capability — the "one account, many capabilities" table. | user_id FK, role_id FK, granted_at, granted_by FK→users, revoked_at NULL |
personal_access_tokens | Bearer tokens for API/future mobile clients. | id, user_id FK, token_hash UNIQUE, device_label, abilities JSON, last_used_at, expires_at, created_at |
password_resets | One-time reset tokens. | id, user_id FK, token_hash, expires_at, used_at |
login_attempts | Throttling / brute-force detection. | id, identifier (phone/email), ip_address, success BOOL, created_at, index on (identifier, created_at) |
9.2 Availability
| Table | Purpose | Key columns |
|---|---|---|
user_availability | Current, temporary status per capability. One row per (user, capability). | user_id FK, capability ENUM(DRIVER,DELIVERY_PROVIDER), status ENUM(OFFLINE,AVAILABLE,BUSY), current_area_id FK, destination_area_id FK NULL, note, updated_at, UNIQUE(user_id, capability) |
9.3 Geography / service areas
| Table | Purpose | Key columns |
|---|---|---|
cities | Top-level, admin-managed. | id, name_ar, name_en, is_active |
areas | e.g. Zahraa Capital, within a city. | id, city_id FK, name_ar, name_en, is_active |
zones | e.g. Zone 3, within an area. | id, area_id FK, name_ar, name_en, latitude NULL, longitude NULL, is_active |
A "location" on a ride/delivery row is not a foreign key to one universal locations table — it's an inline snapshot (*_city_id, *_area_id, *_zone_id, *_landmark, *_latitude, *_longitude) on the ride/delivery row itself, because a request's pickup point is a fact about that request at that moment, not a reusable master-data record. Only the City/Area/Zone taxonomy itself is master data.
9.4 Branches
| Table | Purpose | Key columns |
|---|---|---|
branches | Service lines under one operator. | id, name_ar, name_en, service_type ENUM(RIDE,DELIVERY,GENERAL), is_active, settings JSON |
branch_admins | Which admin manages which branch. | branch_id FK, user_id FK, PK(branch_id, user_id) |
9.5 Verification: driver, vehicle, delivery provider, documents
| Table | Purpose | Key columns |
|---|---|---|
driver_profiles | 1:1 extension of users for driver-specific data. | user_id PK/FK, license_number, license_expiry, verification_status ENUM(NOT_SUBMITTED,PENDING,UNDER_REVIEW,APPROVED,REJECTED,EXPIRED), rejection_reason, verified_by FK, verified_at |
delivery_provider_profiles | 1:1 extension for delivery-provider-specific data. | user_id PK/FK, verification_status (same enum), rejection_reason, verified_by, verified_at |
vehicle_types | Admin-managed (CAR, MOTORCYCLE, BICYCLE, OTHER, extensible). | id, key, name_ar, icon, is_active |
vehicles | A user may own several. | id, user_id FK, vehicle_type_id FK, brand, model, year, color, plate_number UNIQUE, seats, verification_status, is_active |
document_types | Admin-managed catalog of what can be uploaded. | id, key, name_ar, applies_to ENUM(USER,VEHICLE), is_required, max_size_mb, allowed_mime_types JSON, validity_period_days NULL |
documents | Polymorphic — every uploaded file, one lifecycle (§decisions). | id, owner_type ENUM(USER,VEHICLE), owner_id, document_type_id FK, storage_path (private, randomized), original_filename (metadata only, never used as path), mime_type, size_bytes, status ENUM(NOT_SUBMITTED,PENDING,UNDER_REVIEW,APPROVED,REJECTED,EXPIRED), rejection_reason, expires_at, reviewed_by FK, reviewed_at, uploaded_at |
document_access_logs | Every view/download of a sensitive document. | id, document_id FK, accessed_by FK, action ENUM(VIEW,DOWNLOAD), ip_address, created_at |
9.6 Rides
| Table | Purpose | Key columns |
|---|---|---|
ride_offers | A driver's published trip ("I'm going there"). | id, driver_id FK, vehicle_id FK, from_city_id/from_area_id/from_zone_id/from_landmark, to_* (same shape), departure_time, available_seats, seats_remaining, price, currency, note, status ENUM(ACTIVE,FULL,EXPIRED,CANCELLED,COMPLETED), created_at |
ride_requests | A passenger's search intent (optional — browsing offers directly skips this). | id, passenger_id FK, from_*/to_* (same shape), passengers_count, preferred_time, note, status ENUM(SEARCHING,MATCHED,EXPIRED,CANCELLED), created_at |
ride_bookings | The stateful link: one passenger's seat(s) on one offer. This is the ride state machine (§10). | id, ride_offer_id FK, passenger_id FK, ride_request_id FK NULL, seats_booked, price, status ENUM(REQUESTED,ACCEPTED,CONFIRMED,DRIVER_ON_WAY,PASSENGER_PICKED_UP,IN_PROGRESS,COMPLETED,CANCELLED_BY_PASSENGER,CANCELLED_BY_DRIVER,REJECTED,EXPIRED,NO_SHOW,DISPUTED), cancelled_by, cancelled_at, cancellation_reason, completed_at, created_at |
9.7 Deliveries
| Table | Purpose | Key columns |
|---|---|---|
delivery_item_categories | Admin-managed (DOCUMENTS, FOOD, GROCERIES, PACKAGE, PERSONAL_ITEM, OTHER, extensible). | id, key, name_ar, icon, is_active |
delivery_requests | The delivery job + its state machine. | id, customer_id FK, from_*/to_* (same location shape as rides), item_category_id FK, item_description, item_size ENUM(SMALL,MEDIUM,LARGE), preferred_time, suggested_price, notes, status ENUM(CREATED,AVAILABLE,OFFERED,ACCEPTED,PICKUP_PENDING,PICKED_UP,IN_TRANSIT,DELIVERED,COMPLETED,CANCELLED,EXPIRED,DISPUTED,FAILED), accepted_provider_id FK NULL, accepted_at, cancelled_by, cancelled_at, cancellation_reason, completed_at, created_at |
delivery_offers | Optional price counter-offers before acceptance. | id, delivery_request_id FK, provider_id FK, offered_price, note, status ENUM(PENDING,ACCEPTED,REJECTED,WITHDRAWN), created_at |
9.8 Ratings, notifications, complaints, payments, audit
| Table | Purpose | Key columns |
|---|---|---|
ratings | Post-completion feedback, either direction. | id, service_type ENUM(RIDE,DELIVERY), service_id, rater_id FK, ratee_id FK, stars TINYINT(1-5), review NULL, created_at, UNIQUE(service_type, service_id, rater_id) |
notifications | Central notification store, channel-agnostic. | id, user_id FK, type, title, body, data JSON, channel ENUM(IN_APP,PUSH,SMS,EMAIL,WHATSAPP), is_read, read_at, created_at |
complaints | User-reported problems. | id, complainant_id FK, category ENUM(DRIVER,PASSENGER,DELIVERY_PROVIDER,PRICE,PAYMENT,SERVICE,SAFETY,OTHER), related_service_type NULL, related_service_id NULL, subject, description, status ENUM(OPEN,UNDER_REVIEW,WAITING_FOR_USER,RESOLVED,CLOSED,REJECTED), assigned_admin_id FK NULL, created_at, updated_at |
complaint_messages | Thread on a complaint, admin notes flaggable as internal. | id, complaint_id FK, sender_type ENUM(USER,ADMIN), sender_id FK, message, is_internal_note BOOL, created_at |
payment_methods | Admin-managed (CASH active now; CARD/WALLET/ONLINE inactive placeholders). | id, key, name_ar, is_active |
transactions | Money-movement ledger, currently all CASH/self-reported. | id, service_type ENUM(RIDE,DELIVERY), service_id, payer_id FK, payee_id FK, amount, currency, payment_method_id FK, platform_fee, status ENUM(PENDING,COMPLETED,FAILED,REFUNDED), created_at |
audit_logs | Every sensitive admin action. | id, admin_id FK, action, target_type, target_id, ip_address, user_agent, metadata JSON, created_at |
system_settings | Key/value config editable from the admin panel. | id, key UNIQUE, value, value_type, description, updated_by FK, updated_at |
legal_pages | ToS, privacy policy, etc. — versioned, admin-editable. | id, slug UNIQUE, title_ar, content_ar, version, published_at, updated_by FK |
10 State Machines
10.1 Ride booking (per passenger, per offer)
REQUESTED → ACCEPTED is the concurrency-critical transition: accepting attempts an atomic UPDATE ride_offers SET seats_remaining = seats_remaining - :n WHERE id = :offer_id AND seats_remaining >= :n inside a transaction. Zero affected rows ⇒ the accept fails cleanly with "لم تعد هناك مقاعد متاحة" and the booking is auto-set to a system-cancelled outcome instead of silently double-booking. ACCEPTED → CONFIRMED only fires once that update actually succeeds — they are kept as two distinct states specifically so "the driver said yes" and "the seat is actually locked" are never conflated in the data.
Terminal states are truly terminal — no code path transitions out of COMPLETED, CANCELLED_*, REJECTED, or EXPIRED. The generic Fsm helper (§8) is given an explicit adjacency map per entity and refuses any transition not in it, so this is enforced once, in code, not by convention.
10.2 Ride offer (the trip itself, independent of any one passenger)
10.3 Delivery request
AVAILABLE → ACCEPTED uses the same atomic-conditional pattern: UPDATE delivery_requests SET status='ACCEPTED', accepted_provider_id=:pid WHERE id=:id AND status IN ('AVAILABLE','OFFERED'). Zero affected rows ⇒ "تم قبول هذا الطلب من مزود آخر."
10.4 Document verification
11 API Architecture
Versioned under /api/v1. Web pages authenticate with the session cookie + CSRF token; the same endpoints accept an Authorization: Bearer <token> header for the PWA's own fetch calls today and any future native app tomorrow — one implementation, two auth guards.
Response envelope (uniform, exactly as you specified)
// success
{
"success": true,
"message": "تم تنفيذ العملية بنجاح",
"data": { },
"errors": []
}
// validation / business error
{
"success": false,
"message": "حدث خطأ",
"data": null,
"errors": [ { "field": "phone", "message": "رقم الهاتف غير صحيح" } ]
}
Auth & profile
| Method | Endpoint | Auth | Notes |
|---|---|---|---|
| POST | /api/v1/auth/register | — | Rate-limited by IP + phone; creates user with capability NORMAL_USER, status PENDING until phone verified. |
| POST | /api/v1/auth/verify-phone | — | OTP confirm → status ACTIVE. |
| POST | /api/v1/auth/login | — | Throttled (login_attempts); issues session or token. |
| POST | /api/v1/auth/logout | token/session | Revokes current token / destroys session. |
| POST | /api/v1/auth/password/forgot | — | |
| POST | /api/v1/auth/password/reset | — | |
| GET | /api/v1/me | required | Profile + capabilities + current availability. |
| PUT | /api/v1/me | required | Name, photo — never phone/status via this endpoint. |
Verification (driver / vehicle / delivery provider)
| Method | Endpoint | Auth | Notes |
|---|---|---|---|
| POST | /api/v1/driver-profile | required | Creates/updates driver_profiles → PENDING. |
| POST | /api/v1/vehicles | required | Owner = current user only. |
| POST | /api/v1/documents | required | Multipart upload; server validates real MIME/size/extension, stores under randomized name (§13). |
| GET | /api/v1/documents/{id} | owner or reviewer | Streams the file only after an ownership/permission check + logs to document_access_logs. |
| POST | /api/v1/delivery-profile | required |
Availability
| Method | Endpoint | Permission | Notes |
|---|---|---|---|
| PUT | /api/v1/availability/driver | DRIVER + verified | Sets OFFLINE/AVAILABLE/BUSY, area, destination. |
| PUT | /api/v1/availability/delivery | DELIVERY_PROVIDER + verified |
Rides
| Method | Endpoint | Permission | Notes |
|---|---|---|---|
| POST | /api/v1/ride-offers | DRIVER, verified | "I'm going there" publish. |
| GET | /api/v1/ride-offers | any authenticated | Filtered by from/to/time — the deterministic matcher (§6 rules). |
| POST | /api/v1/ride-offers/{id}/cancel | owner driver | Cascades to CONFIRMED bookings → CANCELLED_BY_DRIVER + notification. |
| POST | /api/v1/ride-requests | any authenticated | Optional search-intent record. |
| POST | /api/v1/ride-offers/{id}/book | any authenticated | Creates ride_bookings row, status REQUESTED. |
| POST | /api/v1/ride-bookings/{id}/accept | owner driver of the offer | Atomic seat decrement (§10.1). |
| POST | /api/v1/ride-bookings/{id}/reject | owner driver | |
| POST | /api/v1/ride-bookings/{id}/cancel | passenger or driver, own booking | cancelled_by / reason recorded. |
| POST | /api/v1/ride-bookings/{id}/transition | owner driver | DRIVER_ON_WAY → PASSENGER_PICKED_UP → IN_PROGRESS → COMPLETED, one legal step at a time. |
Deliveries
| Method | Endpoint | Permission | Notes |
|---|---|---|---|
| POST | /api/v1/deliveries | any authenticated | |
| GET | /api/v1/deliveries | any authenticated | Provider view = matching AVAILABLE requests in their area; customer view = own requests. |
| POST | /api/v1/deliveries/{id}/accept | DELIVERY_PROVIDER, verified | Atomic first-writer-wins (§10.3). |
| POST | /api/v1/deliveries/{id}/reject | invited provider | |
| POST | /api/v1/deliveries/{id}/transition | accepted provider | PICKUP_PENDING → … → DELIVERED. |
| POST | /api/v1/deliveries/{id}/complete | customer | Customer confirms receipt → COMPLETED. |
| POST | /api/v1/deliveries/{id}/cancel | customer or provider, own |
Ratings, notifications, complaints
| Method | Endpoint | Permission | Notes |
|---|---|---|---|
| POST | /api/v1/ratings | participant of a COMPLETED service | Unique-rating guard. |
| GET | /api/v1/notifications | required | Paginated, own only. |
| POST | /api/v1/notifications/{id}/read | owner | |
| POST | /api/v1/complaints | required | |
| GET | /api/v1/complaints/{id} | complainant or admin |
Admin (web-first, mirrored to API as needed)
| Method | Endpoint | Permission | Notes |
|---|---|---|---|
| GET | /admin/verification-queue | document.review | Branch-scoped unless system admin. |
| POST | /admin/documents/{id}/approve | document.review | Audit-logged. |
| POST | /admin/documents/{id}/reject | document.review | Requires reason; audit-logged. |
| GET | /admin/users | user.manage | Search/filter §54. |
| POST | /admin/users/{id}/suspend | user.suspend | Reason required; audit-logged; forces availability OFFLINE. |
| GET | /admin/reports/* | branch-scoped read | §55 report set. |
| GET | /admin/audit-logs | audit_log.view |
12 UI/UX Architecture
Visual system
Neutral warm-white surfaces, near-black ink text, soft borders — the four pastel brand tones (#FBF0B2 #FFC7EA #D8B4F8 #CAEDFF) appear only as small accents: status badges, capability chips, empty-state illustration backgrounds, and one accent color per major section of the app (rides lean blue, deliveries lean yellow) so a user develops a quiet visual sense of "which part of the app am I in" without a loud color scheme anywhere. Type: Cairo throughout, RTL by default (<html dir="rtl" lang="ar">), with numerals kept as Arabic-Indic or Western digits per a single configurable setting (Western digits recommended for price/phone clarity).
Screen inventory
| Zone | Screens |
|---|---|
| Public | Landing, Login, Register, Phone verify, Forgot/reset password, Legal pages (ToS/Privacy/Guidelines) |
| User | Home dashboard (§21), Profile, Ride search, Ride offer detail, My bookings, Delivery request form, My deliveries, Notifications, Ratings given/received, Complaint form & history |
| Driver | Become-a-driver onboarding (profile+vehicle+documents), Driver dashboard (§22), Publish a trip, My published trips, Incoming booking requests, Trip-in-progress screen (status transitions) |
| Delivery provider | Become-a-provider onboarding, Provider dashboard (§23), Nearby requests feed, Delivery-in-progress screen |
| Branch admin | Verification queue (scoped), Users (scoped), Rides/Deliveries (scoped), Complaints (scoped), Reports (scoped) |
| System admin | Full dashboard (§24), Branch management, Service-area management (cities/areas/zones), Document-type & category management, Pricing policy settings, Admin management, Audit log, System settings |
Reusable component set
Button, Input, Select, Textarea, Modal, Bottom Sheet, Card, Badge, Alert, Toast, Tabs, Bottom Navigation (user-facing), Top Navigation + Sidebar (admin), Status Indicator (dot + Arabic label, never color alone), Document Card, Vehicle Card, Ride Card, Delivery Card, User Card, Rating stars, Empty State, Loading skeleton, Error state, Confirmation Dialog — each a single PHP view-partial + Tailwind classes, never copy-pasted markup.
🟢 متاح الآن · 🟡 قيد المراجعة · 🔴 غير متاح · ⛔ تم الرفض
13 Security Architecture
Authentication
password_hash()withPASSWORD_BCRYPT(cost 12), never a custom hash.- PHP sessions:
httponly,secure,samesite=Laxcookies; session ID regenerated on login and on privilege change (capability grant). - Login throttling via
login_attempts: exponential backoff per (identifier, IP) pair; generic "بيانات الدخول غير صحيحة" for both wrong-password and unknown-account, to resist enumeration. - Bearer tokens hashed at rest (never store the raw token), revocable per-device, expirable.
Authorization & IDOR
- Every resource-fetching controller action resolves the resource, then calls its Policy with
(current_user, resource)before touching the response — never trusts that a valid-looking ID in the URL implies access. This is a hard rule, not a guideline: repositories return the row, the policy decides if the caller may see it, the controller never skips the policy call. - Branch-scoping is enforced at the query layer (branch admins' list queries are pre-filtered by their assigned branches; there is no admin screen that shows unscoped data by omission).
Document security (§8 of your spec, non-negotiable)
- Stored under
/storage/private/documents/{users|vehicles}/{randomized-uuid}.{ext}, outside the web root if the host allows a directory abovepublic_html; if not, inside it but behind a harddeny from all.htaccess, with a PHP streaming endpoint as the only access path. - Server-side validation on every upload: real MIME sniffed via
finfo(never the client'sContent-Type), extension cross-checked against an allow-list fromdocument_types.allowed_mime_types, size checked againstmax_size_mb, and the file is never trusted by its original name — a fresh UUID filename is generated server-side. - Every view/download is authorization-checked and written to
document_access_logs— "who looked at whose ID card, when" must always be answerable.
Standard web hardening
- SQL injection: PDO prepared statements exclusively — no string-concatenated queries anywhere, enforced by code review discipline plus a lint check in CI.
- XSS: all output through a single
e()escaping helper in views; no rawecho $_.... - CSRF: per-session token, required on every state-changing web form; the API's bearer-token requests are exempt (no ambient credential to forge) but still origin-checked.
- Security headers:
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Content-Security-Policy(script-src self + Google Fonts only),Strict-Transport-Securityonce HTTPS is confirmed on the host. - Rate limiting on auth + write endpoints, keyed by IP and by user, stored in a small counter table (no Redis needed at this scale).
Concurrency-as-a-security-property
Double-booking a seat or double-accepting a delivery isn't just a bug — it's a fairness/trust failure for a community platform. Both are closed exclusively via atomic, single-statement conditional UPDATE ... WHERE patterns inside a transaction, never a PHP-level "read the count, then decide" (§10, §61).
14 Testing Strategy
| Layer | Coverage | Tooling |
|---|---|---|
| Unit | FSM transition legality, pricing/seat math, validators, policy logic in isolation | PHPUnit |
| Integration | Full service+repository+DB round-trips for each workflow (register→verify→available→ride→complete→rate) | PHPUnit against a disposable test schema |
| API | Auth, permission denial paths, validation error shape, response envelope conformance | PHPUnit HTTP tests |
| Security | SQLi injection attempts, XSS payloads round-tripped, CSRF-missing rejected, IDOR cross-user access attempts, upload of a renamed .php file as ".jpg", path traversal filenames | Manual + scripted checklist per release |
| Concurrency | N simultaneous accept requests against 1 remaining seat/slot; exactly one must win | A small PHP CLI script firing parallel requests via curl_multi |
| UI | 320/375/390/414px + common desktop widths; RTL layout, Arabic number/date/currency formatting | Manual pass + browser devtools device emulation |
| Workflow (E2E) | The full scenario in §70 of your spec, run start to finish | Manual scripted QA pass each release, until volume justifies automation |
Required edge cases (from your §71 — all must have an explicit test)
- Two users request the last seat simultaneously → exactly one CONFIRMED, one clean rejection.
- Driver cancels after acceptance → cascades to affected bookings + notifications, never a silent orphan.
- Document expires while provider is AVAILABLE → forced OFFLINE, verified by the expiration sweep test.
- Suspended user with an active ride → ride still resolves; user cannot start new ones.
- Delivery provider double-accept race → same atomic guarantee as rides.
- User requests another user's document by ID → 403, logged as a suspicious access attempt.
- Ride/delivery ID substitution (IDOR) across every mutating endpoint.
- Negative/zero/absurd price or passenger count → rejected server-side even if the client-side form was bypassed.
- Malicious upload: PHP payload renamed
.jpg, oversized file, disallowed MIME → all rejected before ever touching disk with the wrong name. - Direct role/capability tampering via a crafted request → rejected, since capabilities are only ever written by the verification service, never accepted as client input.
- Session expiry mid-multi-step form (e.g., mid document upload) → clear re-login prompt, no partial/corrupt state persisted.
15 MVP Scope
Auth & profiles · Driver + vehicle verification · Document security & review queue · Availability (driver + delivery) · Ride offers, requests, bookings, full state machine · Delivery requests, accept/reject, full state machine · Ratings · In-app notifications · Admin verification workflow · Core web security (§13)
Complaints module · Branches · Service-area admin UI · Reports/dashboard charts · Audit log viewer · PWA installability
Live GPS/maps · Chat · Wallet/online payment · Coupons/referrals · Driver incentives · Scheduled/recurring rides · Corporate accounts · SOS/safety features · AI-based matching · Demand forecasting
16 Phase-by-Phase Implementation Plan
| Phase | Delivers | Exit check before next phase |
|---|---|---|
| 0 — Discovery | This document. | Your sign-off + answers in §18. |
| 1 — Architecture | Finalized ERD/migrations plan, confirmed host constraints. | Host access verified; PHP/MySQL versions confirmed. |
| 2 — Foundation | Project skeleton, DB connection/config, migrations runner, auth, roles/permissions, logging, error handling, security middleware. | Register→login→logout works end-to-end; CSRF/session tests pass. |
| 3 — Verification | Driver/delivery profiles, vehicles, documents, private storage, admin review queue, expiration sweep, audit logs. | Full upload→review→approve/reject→expire cycle tested; IDOR tests on documents pass. |
| 4 — Ride service | Availability, publish trip, search/match, booking + full state machine, cancellation, completion, rating. | Concurrency test (last-seat race) passes; full ride E2E scenario passes. |
| 5 — Delivery service | Delivery requests, provider matching, accept/reject, state machine, completion, rating. | Concurrency test (double-accept race) passes; full delivery E2E passes. |
| 6 — Admin | Dashboard, users, branches, areas, complaints, reports, settings. | Branch-scoping verified (a branch admin cannot see another branch's data). |
| 7 — PWA | Manifest, service worker (safe caching only), install prompts, responsive pass. | Installable on Android Chrome + iOS Safari "Add to Home Screen." |
| 8 — QA hardening | Full security/edge-case/UI pass from §14. | All P0/P1 items in §85 (your Final Acceptance Criteria) checked off. |
Per your §87, each phase ends with: tests run, DB integrity checked, security re-checked, mobile UI + RTL checked, authorization re-checked, docs updated — then the next phase starts. No phase is skipped or parallelized into "build everything, test at the end."
17 Deployment Architecture (shared hosting)
No Docker, no container registry, no orchestrator — none of that is available on typical shared hosting, and none of it is needed at this scale. The realistic deployment shape is:
Local/dev machine
│ git push
▼
GitHub repository (private)
│ either:
│ (a) SSH + git pull on the host, if the shared plan exposes SSH + git, or
│ (b) SFTP/rsync of changed files, if it doesn't
▼
zahara.m3lsh.com (public_html/ = /public, everything else one level up if the
plan allows a directory outside the web root; otherwise
/storage/private/ locked down via .htaccess as the fallback)
│
├─ Apache + PHP-FPM (or mod_php) — confirm version on host
├─ MySQL/MariaDB database (separate credentials, never in Git)
├─ Cron (host-provided, cPanel-style) running:
│ - php artisan-equivalent scripts/expire_documents.php (daily)
│ - php scripts/expire_stale_offers.php (hourly)
└─ .env (uploaded once, out of Git, permissions 600)
Migrations are plain numbered .sql files applied by a tiny migrate.php CLI runner (tracks applied migrations in a migrations table) — no framework migration tool required. This keeps "how do I stand up a fresh copy of this database" a one-command answer on a host where Composer/Artisan may not be available at all.
18 Questions & Decisions Requiring Your Confirmation
SSH to zahara.m3lsh.com timed out on the default port 22 with no prior credentials on file. Please share: SSH host/port, username, and either a password or a public key to install — or confirm SSH isn't offered on this plan and cPanel File Manager/FTP is the intended deployment path instead. This single answer changes several items in §17 and confirms whether Composer is usable at all.
Needed to confirm: PHP ≥ 8.1, pdo_mysql, fileinfo (for real MIME sniffing), gd or imagick (image re-validation), and whether the plan allows a directory outside public_html for private document storage.
Real SMS OTP requires a paid SMS gateway account (not free). For MVP, is admin-assisted manual verification acceptable (admin marks a phone verified after a call/WhatsApp check), or should I budget for integrating a specific SMS provider now?
Confirming per §3/§65: this launches as a community service with a clear "not a licensed transportation company" disclaimer, and the real Terms of Service / driver agreement wording will come from your own legal review before public launch, not from me. I'll ship the legal-pages module and reasonable placeholder copy, not final legal text.
Please confirm the exact list of Zahraa Capital zones/landmarks you want seeded on day one (Zone 1–4 assumed from your spec — any more granular pickup points/landmarks worth pre-loading?), and who the first system admin account should belong to (name + phone).
Is the app served at zahara.m3lsh.com itself, or a subdomain (e.g. app.zahara.m3lsh.com or a separate domain)? Any existing logo/icon set for the PWA manifest, or should I produce a simple wordmark from the brand palette for now?