SpotWatch is an IoT-based parking detection system. It uses computer vision and hardware sensors to monitor parking spot availability in real-time, delivering data via gRPC to a centralized dashboard.
This is a Go and Next.js monorepo structured to scale with multiple microservices:
- /protos: Shared Protocol Buffer definitions (
.proto) and generated Go code. This is the source of truth for all service communication. - /services/dashboard: Go gRPC backend service that updates parking spot availability in Postgres and serves the dashboard API, containing business logic.
- /services/ingest: Go HTTP ingest service that receives TTN webhook JSON (uplinks), parses payloads, and forwards updates to dashboard via gRPC.
- /nextjs-dashboard: The frontend application built with Next.js, Tailwind CSS, and TypeScript.
- /docker-compose.yml: Orchestrates local development for databases and services.
Ensure you have the following installed:
- Go (v1.24+ recommended)
- Node.js (v18+) & pnpm
- Protocol Buffer Compiler (
protoc) - Go Plugins for protoc:
protoc-gen-goandprotoc-gen-go-grpc v1.6.0 - Docker & Docker Compose
- PostGres Sql Client
From repo root:
docker compose up -dLocal Database up to sync
cd nextjs-dashboard
npx prisma migrate devThis seeds (image_id, box_id) rows into boxes for local testing:
chmod +x nextjs-dashboard/scripts/seed_boxes.sh
./scripts/seed_boxes.shTo see the database visually in Prisma GUI, use the below command:
npx prisma studioBefore running the services, generate the latest Go code from the proto definitions:
cd protos
# Generate Go code from parking.proto
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
parking.protoIf you change protos/parking.proto, re-run the command and commit both the .proto and generated .pb.go files.
The dashboard service uses a local replace directive in go.mod to reference the generated protos.
cd services/dashboard
go mod tidy
go run .Open another terminal to run the ingest service...
cd services/ingest
go mod tidy
go run .Ingest endpoints:
GET /healthz
POST /ttn/uplink (TTN JSON uplink webhook)
cd nextjs-dashboard
pnpm install
pnpm devDashboard: http://localhost:3000
To receive live TTN uplinks on your local machine, use ngrok.
There are two ways to use ngrok:
Start ngrok:
ngrok http 8081This generates a new URL each time you start ngrok, so you must update the TTN webhook endpoint on every run to:
https://<your-ngrok-id>.ngrok-free.app/api/ttn-webhook
Using an ngrok dev domain (static domain) allows you to configure the TTN webhook once and never change it again, as long as you start ngrok with the same domain.
-
Get a dev domain in the ngrok dashboard
Example:
<your-ngrok-dev-domain>.ngrok-free.app -
Authenticate ngrok locally (required for dev domains):
ngrok config add-authtoken <YOUR_NGROK_AUTHTOKEN>
-
Set the TTN webhook endpoint ONCE in TTN Console → HTTP Webhook endpoint to:
https://<your-ngrok-dev-domain>.ngrok-free.app/ttn/uplink
-
Start the dashboard:
pnpm dev
-
Start ngrok using the dev domain:
ngrok http --domain=<your-ngrok-dev-domain>.ngrok-free.app 8081
As long as you use the same dev domain, you don't need to update the TTN webhook endpoint again.
Correct: --domain=<your-ngrok-dev-domain>.ngrok-free.app
Incorrect: --domain=https://<your-ngrok-dev-domain>.ngrok-free.app/
Ingest expects uplink_message.frm_payload to be base64 of:
bbox:<image_id>,<box_id>,<availability>;<image_id>,<box_id>,<availability>;...
You can manually test the webhook endpoint using curl:
PAYLOAD_B64=$(echo -n 'bbox:12,101,1;12,102,0;' | base64)
curl -X POST http://localhost:8081/ttn/uplink \
-H "Content-Type: application/json" \
-d '{
"end_device_ids": { "device_id": "test-device" },
"uplink_message": { "frm_payload": "'"$PAYLOAD_B64"'" }
}'- Edit
protos/parking.proto - Run
protoccommand to update the generated code - Commit both the
.protoand the.pb.gofiles to ensure other services stay in sync - Edit README.md documentation or pull request to comment on items done