A mobile application connecting vehicle owners with local garages and automotive marketplaces in Morocco.
Built with React Native (Expo) + Node.js/Express + PostgreSQL/PostGIS following a strict Feature-Based Redux-Saga architecture.
- Tech Stack
- Project Structure
- Getting Started
- Architecture Rules
- API Reference
- Database Schema
- Environment Variables
- Troubleshooting
| Layer | Technology |
|---|---|
| Mobile App | React Native 0.73 + Expo SDK 50 |
| State Management | Redux 5 + Redux-Saga |
| Styling | React Native StyleSheet |
| API Client | Axios |
| Backend | Node.js + Express |
| Database | PostgreSQL 17 + PostGIS 3.5 |
| Auth | JWT (jsonwebtoken + bcrypt) |
| Infrastructure | Docker + Docker Compose |
mecanic_hub3/
βββ docker-compose.yml # Database service (PostgreSQL + PostGIS)
βββ init.sql # Full DB schema + seed data (auto-run by Docker)
β
βββ backend/
β βββ .env # β οΈ NOT committed β see Environment Variables below
β βββ server.js # Express app entry point
β βββ package.json
β βββ config/
β β βββ database.js # PostgreSQL connection pool
β βββ middleware/
β β βββ auth.js # JWT verification middleware
β βββ routes/
β βββ auth.js # POST /auth/login, /auth/register
β βββ garages.js # GET /garages/search, /garages/:id
β βββ marketplace.js # GET/POST /marketplace/posts
β βββ profile.js # GET /profile/client, /profile/garage
β βββ dashboard.js # GET /dashboard/client, /dashboard/garage
β
βββ src/
βββ features/ # Feature modules (see Architecture Rules)
β βββ Auth/
β βββ GarageDiscovery/
β βββ Marketplace/
β βββ Profile/
β βββ ClientProfile/
β βββ GarageProfile/
βββ components/ # Reusable UI components
β βββ AppButton.js
β βββ AppCard.js
β βββ AppInput.js
β βββ GarageCard.js
β βββ CustomTabNavigator.js
βββ navigation/
β βββ AppNavigator.js # Root navigator (Auth vs Main flow)
βββ services/
β βββ api.js # Axios instance β update API_URL for your machine
βββ store/
βββ index.js
βββ rootReducer.js
βββ rootSaga.js
- Node.js v18+
- Docker Desktop (running)
- Android Studio (for emulator) or Expo Go app on a physical device
# From project root
docker-compose up -d
# Verify it's healthy
docker ps
# Expected: mecanic_hub_db Up (healthy)
# Verify seed data
docker exec -it mecanic_hub_db psql -U postgres -d garagena -c "SELECT name, city FROM garages;"The database schema and seed data from
init.sqlrun automatically on the first start.
cd backend
npm installCreate backend/.env (or verify it exists):
PORT=3000
NODE_ENV=development
DB_HOST=localhost
DB_PORT=5432
DB_NAME=garagena
DB_USER=postgres
DB_PASSWORD=0000
JWT_SECRET=change_this_in_production
JWT_EXPIRES_IN=7dnode server.js
# Expected:
# β
Database connected successfully
# π Server running on port 3000Test the backend:
# Should return garages in Rabat
curl "http://localhost:3000/api/garages/search?city=Rabat"Find your machine's local IP:
# Windows PowerShell
ipconfig
# Look for "IPv4 Address" under your active network adapterUpdate the API URL in src/services/api.js:
const API_URL = 'http://YOUR_LOCAL_IP:3000/api';
// Example: 'http://192.168.1.50:3000/api'
// β οΈ Use 10.0.2.2 instead of localhost for Android Emulator# From project root
npm install
npx expo start --lan- Press
aβ Android Emulator - Scan QR code with Expo Go β Physical device (must be on same WiFi)
| Role | Password | |
|---|---|---|
| Client | youssef@gmail.com |
secret |
| Client | fatima@gmail.com |
secret |
| Garage owner | garage1@gmail.com |
secret |
| Admin | admin@garagena.ma |
secret |
Every feature must follow this exact structure. No exceptions.
src/features/[FeatureName]/
βββ actionTypes/index.js # String constants (e.g. FETCH_GARAGES_REQUEST)
βββ actions/index.js # Action creator functions
βββ initialState/index.js # Default Redux state shape
βββ reducer/index.js # Pure reducer function
βββ sagas/index.js # All API calls (side effects)
βββ selectors/index.js # Reselect memoized selectors
βββ useCases/
β βββ service.js # Business logic (custom React hook)
β βββ main.js # UI component (renders only)
βββ index.js # Public exports
| Rule | Detail |
|---|---|
| β API calls | Only in sagas/index.js via call(api.get, ...) |
| β Business logic | Only in useCases/service.js |
| β UI / JSX | Only in useCases/main.js |
| β State reads | Only via selectors/ using useSelector |
| β State writes | Only via actions/ dispatched through Redux |
| β Never | Direct axios calls inside components |
| β Never | useState for server data |
| β Never | Business logic inside main.js |
- Create folder
src/features/NewFeature/ - Define
actionTypes/index.js - Create
actions/index.js - Define
initialState/index.js - Create
reducer/index.js - Create
sagas/index.js(add tostore/rootSaga.js) - Create
selectors/index.js - Create
useCases/service.js - Create
useCases/main.js - Export from
index.js - Add reducer to
store/rootReducer.js - Add to navigation in
AppNavigator.jsif needed
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
No | Register new user |
| POST | /api/auth/login |
No | Login, returns JWT |
| GET | /api/auth/me |
Yes | Get current user |
Register body:
{ "email": "user@example.com", "password": "Test1234!", "name": "John", "role": "client" }| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/garages/search |
No | Search garages |
| GET | /api/garages/:id |
No | Get garage detail |
| POST | /api/garages/:id/interaction |
Yes | Track a click |
Search query params:
| Param | Type | Example | Description |
|---|---|---|---|
city |
string | Rabat |
Filter by city |
category |
string | SERVICE |
SERVICE, RETAIL, BOTH |
lat + lng |
float | 34.01,-6.83 |
GPS center point |
radius |
int | 10 |
Radius in km (default: 50) |
sortBy |
string | rating |
rating or distance |
minRating |
float | 4.0 |
Minimum rating filter |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/marketplace/posts |
No | List items |
| POST | /api/marketplace/posts |
Yes | Create listing |
| GET | /api/marketplace/posts/:id |
No | Item detail |
| PUT | /api/marketplace/posts/:id |
Yes | Update listing |
| DELETE | /api/marketplace/posts/:id |
Yes | Delete listing |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/profile/client |
Yes | Client profile |
| PUT | /api/profile/client |
Yes | Update client profile |
| GET | /api/profile/client/vehicles |
Yes | List vehicles |
| POST | /api/profile/client/vehicles |
Yes | Add vehicle |
| GET | /api/profile/client/favorites |
Yes | Favorite garages |
| POST | /api/profile/client/favorites/:id |
Yes | Add favorite |
| GET | /api/profile/garage |
Yes | Garage profile |
| PUT | /api/profile/garage |
Yes | Update garage profile |
See init.sql for the complete schema. Summary:
users
βββ garages (one user β one garage for garage role)
βββ vehicles (one user β many vehicles)
βββ favorites (user β garage junction)
βββ notifications (one user β many notifications)
βββ messages (user β user)
garages
βββ appointments (client user + garage + vehicle)
βββ posts (reviews: user + garage + rating)
βββ marketplace_items
βββ interactions (analytics: whatsapp_click, call_click, etc.)
βββ garage_certifications
Garages use PostGIS GEOMETRY(Point, 4326) for GPS-based radius search.
PORT=3000
NODE_ENV=development
DB_HOST=localhost
DB_PORT=5432
DB_NAME=garagena
DB_USER=postgres
DB_PASSWORD=0000
JWT_SECRET=change_this_in_production
JWT_EXPIRES_IN=7dconst API_URL = 'http://<YOUR_LOCAL_IP>:3000/api';For Android Emulator: use
10.0.2.2instead oflocalhost
| Problem | Solution |
|---|---|
Error: connect ECONNREFUSED ::1:5432 |
Docker DB not running: docker-compose up -d |
Network Error on device |
Update API_URL in api.js with your PC's IP. Ensure same WiFi. |
| Port 5432 already in use | Stop local Postgres: net stop postgresql-x64-17 |
| Port 3000 already in use | netstat -ano | findstr :3000 then kill the PID |
| Blank screen on login | Check Metro console for JS errors. Run npx expo start --clear |
| GPS not working on emulator | Set a manual location in Android Emulator's "..." β Location tab |
Cannot read property 'latitude' of null |
City search does not require GPS β this is handled automatically |
| DB schema out of date | Drop and recreate: docker-compose down -v && docker-compose up -d |