LaGRID is a live, community-powered traffic incident map for Lagos. It gives people a fast way to see road disruptions, report what they encounter, confirm reports from other drivers, and mark incidents as cleared.
The project pairs a Next.js/Leaflet map experience with an Express, WebSocket, and PostgreSQL backend designed to broadcast incident updates in real time.
- Centers an interactive map on Lagos and displays active incident markers.
- Supports traffic, accidents, road closures, flooding, construction, and other road issues.
- Lets users select a location, add details, and drop a report pin.
- Filters incidents by type and shows live per-category totals.
- Lets users confirm reports (upvote) or mark them cleared.
- Provides a real-time server protocol for incident reports, upvotes, resolutions, and viewer presence.
- Limits server-submitted incident locations to a Lagos bounding box.
The web app is presently a polished front-end prototype: it starts with local seed data and simulates connection/report confirmation state. The server and its WebSocket event handlers are implemented separately, but the browser hook is not yet wired to that server. See Roadmap for the main integration work remaining.
| Area | Technology |
|---|---|
| Web client | Next.js 16, React 19, TypeScript |
| Mapping | Leaflet and React Leaflet; CARTO dark map tiles |
| API | Express 5 |
| Real time | ws WebSocket server |
| Database | PostgreSQL, Drizzle ORM |
| Tooling | npm workspaces, TypeScript, tsx |
.
├── apps/
│ ├── web/ # Next.js map interface
│ │ ├── app/ # App Router entry point and global styles
│ │ ├── components/ # Map, reporting UI, filters, summary, status UI
│ │ ├── hooks/ # Local incident state and connection-state hook
│ │ └── lib/ # Map constants, seed data, shared client types
│ └── server/ # Express + WebSocket service
│ ├── src/http/ # Health endpoint and incident REST routes
│ ├── src/ws/ # Rooms, presence, and incident event handlers
│ ├── src/db/ # Drizzle database client and schema
│ └── drizzle/ # PostgreSQL migration files
└── package.json # Workspace commands
- Node.js 20 or newer
- npm 10 or newer
- A PostgreSQL database (for the API/WebSocket server)
npm installCreate apps/server/.env:
DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DATABASE
MAP_ROOM=lagos-map
PORT=8080DATABASE_URL is required when starting the server because the database client initializes at startup. MAP_ROOM is the shared WebSocket room used for the Lagos map.
The initial SQL migration is at apps/server/drizzle/0000_powerful_husk.sql. Apply it to the database specified by DATABASE_URL using your preferred PostgreSQL migration workflow.
For example, from apps/server, you can use Drizzle Kit with the included configuration:
npx drizzle-kit pushnpm run devThis starts:
- Web client:
http://localhost:3000 - API and WebSocket server:
http://localhost:8080andws://localhost:8080
You can also run either workspace independently:
npm run dev:web
npm run dev:server| Command | Description |
|---|---|
npm run dev |
Run the web client and server concurrently. |
npm run dev:web |
Run only the Next.js development server. |
npm run dev:server |
Run only the Express/WebSocket server in watch mode. |
npm run build --workspace=apps/web |
Create a production web build. |
npm run build --workspace=apps/server |
Compile the server to apps/server/dist. |
npm run lint --workspace=apps/web |
Lint the web client. |
The REST server mounts incident routes at /incidents.
| Method | Endpoint | Current behavior |
|---|---|---|
GET |
/ |
Health response with status and timeStamp. |
GET |
/incidents |
Returns all incidents in the database. |
GET |
/incidents/:id |
Route placeholder; currently returns the requested ID. |
POST |
/incidents |
Route placeholder for incident creation. |
PATCH |
/incidents/:id/upvote |
Route placeholder for an upvote. |
PATCH |
/incidents/:id/resolve |
Route placeholder for resolution. |
Real-time incident writes currently happen over WebSockets rather than these placeholder REST mutations.
Connect to ws://localhost:8080. On a successful connection the server sends:
{ "type": "CONNECTED", "message": "Websocket established" }Clients should then join the shared map room:
{ "type": "JOIN_MAP" }| Event | Payload |
|---|---|
JOIN_MAP |
No payload. Adds the client to the map room and begins presence tracking. |
INCIDENT_REPORT |
{ "type", "description", "latitude", "longitude", "temId" } |
INCIDENT_UPVOTE |
{ "incidentId" } |
INCIDENT_RESOLVE |
{ "incidentId" } |
INCIDENT_REPORT.type must be one of TRAFFIC_JAM, ACCIDENT, ROAD_CLOSURE, FLOODING, CONSTRUCTION, or OTHERS. Coordinates must fall within latitude 6.3–6.7 and longitude 3.1–3.6.
The temId field is echoed in the broadcast response so a client can replace an optimistic marker with the persisted incident. Its spelling reflects the current server contract.
| Event | Meaning |
|---|---|
JOINED |
Confirms map-room membership and includes viewerCount. |
VIEWER_COUNT |
Broadcasts the current viewer count. |
INCIDENT_REPORTED |
Broadcasts a newly persisted incident and its originating temId. |
INCIDENT_UPVOTED |
Broadcasts an incident ID and its new upvotes total. |
INCIDENT_RESOLVED |
Broadcasts the resolved incident ID. |
ERROR |
Indicates an unknown message type, invalid payload, invalid Lagos location, or JSON parsing error. |
DB_ERROR |
Indicates a database operation failed. |
Each database incident includes a UUID, category, description, latitude/longitude, reporter information, upvote count, resolved status, creation time, and expiry time. New WebSocket reports are set to expire 15 minutes after creation.
- Connect
useWebSocketin the web app to the server protocol. - Replace seeded incident data with an initial
GET /incidentsfetch. - Align client incident names (
TRAFFIC,CLOSURE,OTHER) with server enum values (TRAFFIC_JAM,ROAD_CLOSURE,OTHERS). - Implement the REST route placeholders and fetch incidents by ID.
- Add expiry processing to remove or hide expired incidents.
- Add server-side validation, rate limiting, authentication/moderation, and automated tests before production use.
This project is licensed under the ISC License.
Built with 💚 by Omoshola