An anonymous world map for geotagging cat sightings. Anyone can drop a cat with a photo + description, and confirm other people's sightings. No accounts, no logins — just cats.
- Backend: Python · FastAPI · SQLAlchemy · Pillow · PostgreSQL
- Frontend: React · Vite · Leaflet (OpenStreetMap) · installable PWA
- Photos are stored in PostgreSQL; EXIF GPS is used automatically when present, otherwise you pin the location on a map.
- Runs in Docker and deploys to Render via a Blueprint.
- Tap +, take/choose a cat photo.
- The app reads the photo's EXIF GPS in the browser. If found, the pin is pre-filled; otherwise you drop a pin or tap My location.
- Add a description and post. A dot appears on the world map.
- Tap any dot to see the photo, description, and Confirm the sighting.
Anonymity: each device generates a random token stored in localStorage. It's
sent as X-Device-Token and used only to prevent double-confirming and to
attribute (not identify) a post. No personal data is collected. Uploaded photos
have their EXIF metadata stripped before storage.
cp .env.example .env # values are fine as-is for local dev
docker compose up --build- Frontend: http://localhost:5173
- Backend API + docs: http://localhost:8000/docs
- Health check: http://localhost:8000/healthz
The frontend image bakes
VITE_API_BASE=http://localhost:8000at build time (seedocker-compose.yml). If you change the backend port, rebuild the frontend image.
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# point DATABASE_URL at any Postgres instance
export DATABASE_URL=postgresql+psycopg://catmap:catmap@localhost:5432/catmap
uvicorn app.main:app --reloadcd frontend
npm install
# talk to a backend on :8000
echo "VITE_API_BASE=http://localhost:8000" > .env.local
npm run dev| Method | Path | Purpose |
|---|---|---|
| GET | /api/sightings?min_lat&max_lat&min_lng&max_lng |
Dots in a bounding box |
| POST | /api/sightings |
Create (multipart: image, lat, lng, description) |
| GET | /api/sightings/{id} |
Full detail |
| GET | /api/sightings/{id}/photo |
Full image bytes |
| GET | /api/sightings/{id}/thumbnail |
Thumbnail bytes |
| POST | /api/sightings/{id}/confirm |
Confirm once per device (idempotent) |
| POST | /api/sightings/{id}/report |
Report once per device; auto-hides at threshold |
| DELETE | /api/sightings/{id} |
Delete your own (device must be creator) |
| GET | /healthz |
Liveness + DB connectivity |
POST/confirm/report/DELETE require the X-Device-Token header. Create,
confirm, and report are rate-limited (see RATE_LIMIT_* env vars).
Set ADMIN_TOKEN to enable token-gated moderation (sent as X-Admin-Token):
| Method | Path | Purpose |
|---|---|---|
| GET | /api/admin/reports |
List reported sightings |
| POST | /api/admin/sightings/{id}/hide |
Hide a sighting |
| POST | /api/admin/sightings/{id}/unhide |
Restore a sighting |
| DELETE | /api/admin/sightings/{id} |
Delete a sighting |
Sightings reach status="hidden" automatically once AUTO_HIDE_THRESHOLD
distinct devices report them; hidden sightings vanish from the public map.
cd backend && pip install -r requirements-dev.txt && pytest (runs against
SQLite; covers create/EXIF/confirm/report-auto-hide/delete-ownership/rate-limit/
upload-hardening). CI (.github/workflows/ci.yml) runs lint + tests + builds.
- Push this repo to GitHub.
- Render Dashboard → New → Blueprint → pick the repo. Render reads
render.yamland provisions:catmap-db— managed PostgreSQLcatmap-backend— Docker web service (DATABASE_URLauto-wired)catmap-frontend— Docker web service (VITE_API_BASEbaked at build)
- After the first deploy, confirm the URLs match the values in
render.yaml(catmap-backend.onrender.com/catmap-frontend.onrender.com). If Render assigned different names, updateBACKEND_URL/BACKEND_HOST(frontend) andCORS_ORIGINS(backend) accordingly and redeploy. The frontend nginx proxies/apito the backend so the browser stays same-origin; add any custom frontend domain toCORS_ORIGINSonly if you call the backend URL directly.
The backend normalizes Render's postgresql:// connection string to the
postgresql+psycopg:// driver form automatically (app/database.py).
The frontend is an installable PWA: open it on a phone and Add to Home Screen for a full-screen, app-like experience with camera capture and geolocation. Because all logic lives in the React app, it can later be wrapped with Capacitor to ship real App Store / Play Store builds without rewriting features.
Schema changes are managed with Alembic. On startup the backend runs
alembic upgrade head (app/database.py).
cd backend
# apply migrations manually (same as startup)
alembic upgrade head
# create a new revision after editing models
alembic revision -m "describe change" --autogenerateIf you deployed before Alembic was added and tables already exist, the app
auto-stamps the 0001 baseline on first boot, then applies newer revisions
(e.g. 0002 adds cat_confidence). Fresh databases run all revisions from
scratch.
Uploads are checked server-side with a COCO SSD object detector (cat class). Tune via
env vars (CAT_DETECTION_ENABLED, CAT_DETECTION_THRESHOLD, CAT_DETECTION_STRICT).
The browser shows an optional pre-check hint; the server is authoritative.
Download the model for local backend dev:
cd backend && python scripts/fetch_model.py- For very large datasets, consider marker clustering and/or PostGIS.