ParkPulse SG allows you to locate nearest carparks to your destination, view their real-time slot availability, and navigate to them.
Watch video demo here: https://youtu.be/AdSQKVfwq1s
Create a .env file in the project root:
cp server/.env.example .envFill in the values:
# OneMap geocoding
ONEMAP_API_KEY=
# data.gov.sg carpark availability
DATA_GOV_API_KEY=
# HDB carpark dataset
DATASET_ID=d_23f946fa557947f93a8043bbef41dd09
# AWS credentials
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=ap-southeast-1
# AWS Cognito
USER_POOL_ID=
APP_CLIENT_ID=
APP_CLIENT_SECRET=Where to get the keys:
- OneMap API key — onemap.gov.sg/docs
- Data.gov.sg API key — data.gov.sg/developer
- AWS credentials — AWS IAM console
The backend runs as three services — the Node.js server (scalable), Nginx (load balancer), and Redis (cache).
cd server
docker compose up --build --scale server=3| Service | Exposed at |
|---|---|
| API (via Nginx) | http://localhost:8080 |
| Redis | internal only |
To stop:
docker compose downcd frontend
npm install
npm run devThe frontend will be available at http://localhost:5173 and talks to the backend at http://localhost:8080.
If you prefer to run the backend directly without Docker, you need Redis running locally first.
# Start Redis (macOS with Homebrew)
brew services start redis
# Start the backend (with hot reload)
cd server
npm install
npm run devThe backend will be available at http://localhost:3000. Update VITE_API_BASE_URL in your .env or frontend/src/lib/config.js if needed.
.
├── .env # environment variables (not committed)
├── frontend/ # React 18 + Vite SPA
│ └── src/
│ ├── App.jsx
│ ├── main.jsx
│ ├── pages.config.js
│ ├── api/ # localStorage-backed mock data layer
│ ├── components/
│ │ ├── carpark/ # carpark-specific UI components
│ │ └── ui/ # shadcn/Radix primitives (button, badge, dialog, …)
│ ├── hooks/
│ ├── lib/
│ ├── pages/
│ │ ├── Auth.jsx # login / signup
│ │ ├── Home.jsx # search + radius + EV filter
│ │ ├── Carparks.jsx # search results list
│ │ ├── Carpark.jsx # individual carpark detail
│ │ ├── Navigate.jsx # turn-by-turn navigation
│ │ ├── Saved.jsx # saved / favourite carparks
│ │ ├── SavePrompt.jsx # prompt to save after viewing
│ │ ├── Rate.jsx # submit a rating
│ │ └── ThankYou.jsx
│ └── utils/
└── server/ # Express 5 API
├── Dockerfile
├── docker-compose.yml # server (×N) + nginx + redis
├── server.js # entry point, mounts all routers
├── config/
│ ├── nginx.docker.conf # Nginx load balancer config (Docker)
│ ├── nginx.config # Nginx config for local multi-instance setup
│ └── redis.js # Redis client + getCache / setCache helpers
├── controllers/
│ ├── authController.js
│ ├── carparkController.js
│ ├── favoriteCarparkController.js
│ ├── locationController.js
│ ├── navigateController.js
│ └── rateCarparkController.js
├── data/
│ └── carparkDB.js # static HDB carpark dump (SVY21 coords)
├── db/
│ ├── dynamoClient.js # DynamoDB DocumentClient singleton
│ ├── dynamoAdapter.js # get / put / delete wrappers
│ └── index.js
├── helpers/
│ ├── authHelper.js
│ ├── carparkHelper.js
│ ├── coordConverter.js # SVY21 ↔ lat/lon via proj4 EPSG:3414
│ ├── favoriteCarparkHelper.js
│ └── navigateHelper.js
├── middlewares/
│ ├── portMiddleware.js # injects serving port into responses (load balancing debug)
│ └── rateLimitMiddleware.ts # per-IP rate limiter (Redis-backed sliding window, factory: createRateLimiter)
├── models/
│ ├── carpark.js # entity: toDB / fromDB / toJSON, SVY21 distance
│ ├── carparkAvailability.js
│ ├── carparkRating.js
│ ├── favoriteCarpark.js
│ ├── location.js
│ └── user.js
├── routes/
│ ├── authRoute.js # POST /api/auth/signup|login|logout
│ ├── carparkRoute.js # GET /api/carparks
│ ├── favoriteCarparkRoute.js # GET|POST|DELETE /api/favorites
│ ├── locationRoute.js # GET /api/location
│ ├── navigateRoute.js # GET /api/navigate/route
│ └── rateCarparkRoute.js # GET|POST /api/rating
├── services/
│ ├── authService.js # Cognito USER_PASSWORD_AUTH flow
│ ├── carparkService.js # geocode + filter + availability + rating join
│ ├── favoriteCarparkService.js
│ ├── locationService.js
│ ├── navigateService.js
│ └── rateCarparkService.js
└── tests/
├── carparkService.test.js
├── favoriteCarparkService.test.js
├── locationService.test.js
└── rateCarparkService.test.js