A web application for detecting and editing parking lot polygons from satellite imagery using AI.
- AI-Powered Detection: Uses a SegFormer model (UTEL-UIUC/SegFormer-large-parking) to detect parking lots from Google Maps satellite imagery
- Worker Queue: Inference jobs run in a dedicated RabbitMQ-backed worker container, keeping the API responsive
- Live Progress: SSE-based progress streaming so the browser shows real-time inference status
- Interactive Map Editor: Edit, add, delete, and split parking lot polygons on a Leaflet map
- City Boundary Search: Look up official downtown/city boundaries by name (via ArcGIS Hub) to use as project bounds
- Multi-User Support: Role-based access control (Admin, Owner, Reviewer)
- Email-First Signup: Public "Request Access" flow + Owner-initiated invites, both completed via a signed signup link
- Project Management: Create projects for different areas, track status through a review workflow
- Backend: FastAPI (Python 3.11+)
- Worker: Same backend image, runs RabbitMQ consumer for inference jobs
- Frontend: React + TypeScript, served via nginx
- Database: PostgreSQL 15 + PostGIS
- Message Queue: RabbitMQ 3.13
- Maps: Google Maps satellite tiles (proxied + cached by backend)
- Auth: JWT-based authentication
- Email: Resend (transactional sends for signup links + Owner notifications)
Working on this project with Claude Code? See .devcontainer/README.md for the dev container setup (auto mode, isolated environment, persistent auth).
- Docker and Docker Compose
- A Google Maps API key (set in
.env)
-
Clone the repository and set up your environment:
cp .env.example .env # Edit .env and set GOOGLE_MAPS_API_KEY and SECRET_KEY -
Start all services:
docker compose up -d
-
Create the first admin user (only needed once, to bootstrap):
docker compose exec backend python scripts/create_admin.py admin@example.com yourpasswordSubsequent users go through the email signup flow — see Access & Signup.
-
Access the application:
- Frontend: http://localhost:5173
- Backend API: http://localhost:8000
- API Docs: http://localhost:8000/docs
- RabbitMQ Management: http://localhost:15672 (user:
parking/parking)
Backend (.env):
DATABASE_URL=postgresql://postgres:postgres@db:5432/parking_lots
SECRET_KEY=your-secret-key
GOOGLE_MAPS_API_KEY=your-google-maps-key
CORS_ORIGINS=["http://localhost:5173"]
RABBITMQ_URL=amqp://parking:parking@rabbitmq:5672/
# Email — needed for the signup flow
RESEND_API_KEY=re_your_resend_api_key # optional in dev; emails log to stdout if unset
EMAIL_FROM=onboarding@resend.dev # in prod, must be an address on a verified Resend domain
APP_BASE_URL=http://localhost:5173 # in prod, your public URL — used to build signup links
In production, EMAIL_FROM must be on a domain you've verified at resend.com/domains; onboarding@resend.dev only delivers to your own Resend account email. APP_BASE_URL is embedded in outbound emails, so it must be the public frontend URL, not localhost.
- Create a Project: Click "New Project" on the dashboard, then either draw a boundary on the map or search for a city/downtown boundary by name
- Run Detection: Click "Run Detection" — the job is queued and progress streams to the UI in real time
- Edit Results: Use the map tools to edit, add, delete, or split polygons
- Submit for Review: When editing is complete, submit for admin review
- Approve: Admins can approve the final results
- Draw: Add new polygons
- Edit: Modify existing polygon vertices
- Delete: Remove polygons
- Split: Click on a polygon, then draw a line to split it
- Admin: Manages user roles and creates users directly. Can approve projects.
- Owner: Approves access requests and sends invites. Receives email notifications when someone requests access.
- Reviewer: Can create projects, run detection, and edit polygons. The default role assigned to new signups.
There are two paths into the app, both ending at a "Set Password" page reached via a signed link:
Public "Request Access" — anyone can submit their email at /register:
- Request creates a pending user row and emails all active Owners.
- An Owner opens the Admin page and activates the request.
- Activation emails the requester a single-use signup link (signed JWT, 72h TTL).
- The requester clicks the link, sets a password, and is logged in.
Owner-initiated invite — Owners can skip the request step from the Admin page:
- Owner enters an email in the "Invite by Email" section.
- Recipient is emailed a signup link directly (no approval step).
- Duplicate invites / existing requests / existing accounts surface a clear 409 error.
Admins keep the ability to create users directly via POST /auth/users (used by scripts/create_admin.py for bootstrapping) and to change roles. Only Owners can activate pending requests.
parking-lot-app/
├── backend/
│ ├── app/
│ │ ├── api/ # FastAPI routes (auth, projects, polygons, inference, cities, maps)
│ │ ├── core/ # Security & permissions
│ │ ├── models/ # SQLAlchemy models
│ │ ├── schemas/ # Pydantic schemas
│ │ ├── services/ # Business logic (inference, city_resolver, queue, sse, tiles, osm)
│ │ └── worker_main.py # RabbitMQ inference worker entry point
│ ├── alembic/ # Database migrations (001–008)
│ └── scripts/ # Utility scripts
├── frontend/
│ └── src/
│ ├── components/ # React components
│ ├── pages/ # Page components
│ ├── services/ # API client
│ └── store/ # State management (zustand)
├── model/ # Custom model checkpoint (volume-mounted, not committed)
└── docker-compose.yml
The app uses UTEL-UIUC/SegFormer-large-parking from HuggingFace, downloaded automatically on first run and cached in a Docker volume.
To use a custom-trained checkpoint:
- Place your model file in the
model/directory - Set
MODEL_PATHin your environment configuration
If you use this tool or its outputs in research, please cite the model it is built on:
@inproceedings{qiam2025pipeline,
title={A Pipeline and NIR-Enhanced Dataset for Parking Lot Segmentation},
author={Qiam, Shirin and Devunuri, Saipraneeth and Lehe, Lewis J},
booktitle={2025 IEEE/CVF Winter Conference on Applications of Computer Vision (WACV)},
pages={1227--1236},
year={2025},
organization={IEEE}
}The app deploys to a DigitalOcean Droplet via GitHub Actions. Images are built and pushed to GHCR on every push to main, then pulled onto the Droplet.
# Install Docker
apt update && apt install -y docker.io docker-compose-plugin git jq
usermod -aG docker $USER && newgrp docker
# Clone repo
git clone https://github.com/wordsandnumbers/parking-lot-app /opt/lotmapper
cd /opt/lotmapper && git checkout main
# Write .env (never committed — contains real secrets)
cat > .env << EOF
SECRET_KEY=$(openssl rand -hex 32)
GOOGLE_MAPS_API_KEY=<your-key>
DOMAIN=<your-domain>
EOF
# Initial start (builds images locally before CI/CD has pushed any)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
# Create first admin user
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec backend \
python scripts/create_admin.py admin@example.com <password>| Secret | Description |
|---|---|
DO_HOST |
Droplet public IP |
DO_USER |
SSH user (root or deploy user) |
DO_SSH_KEY |
Private SSH key for Droplet access |
GITHUB_TOKEN is automatic — no setup needed for GHCR push.
Push to main → builds backend + frontend images → pushes to GHCR → SSHs into Droplet → docker compose pull && up. Takes ~3–4 minutes. db, rabbitmq, and caddy are not restarted on deploys.
Trigger the Rollback workflow from the GitHub Actions UI. Enter the short git SHA (e.g. abc1234) to roll back to. Migrations are not reversed — they are forward-only.
ssh root@<droplet-ip>
cd /opt/lotmapper
# Deploy a specific version
git pull origin main
sed -i "s/^IMAGE_TAG=.*/IMAGE_TAG=git-<sha>/" .env
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull backend worker frontend
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps backend worker frontend
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec -T backend alembic upgrade headnpm version patch # 1.0.0 → 1.0.1 (bug fix)
npm version minor # 1.0.0 → 1.1.0 (new feature)
npm version major # 1.0.0 → 2.0.0 (breaking change)
git push && git push --tagsnpm version bumps package.json, commits, and creates a git tag automatically. The version is baked into every Docker image as v1.0.0+git-<sha> and visible at GET /health, in worker logs, the /docs page, and the browser console.
- Frontend changes require a rebuild:
docker compose build frontend && docker compose up -d frontend - Backend changes must be copied in (source is not volume-mounted):
docker compose cp <file> backend:/app/<path> - Migrations must also be copied before running:
docker compose cp backend/alembic/versions/XXX.py backend:/app/alembic/versions/ - The worker container shares the backend image — restart it after backend changes:
docker compose restart worker
Once the backend is running, visit:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
MIT