A map-based photo gallery. Point it at a Google Cloud Storage bucket full of geotagged JPEGs and it produces an interactive world map where each gallery is a pin, with per-picture maps, full-size overlays, and EXIF / weather / location metadata — all fetched automatically at import time.
- Upload JPEGs to a GCS bucket, organised in folders by location (
Iceland/,Azores/, …). - Hit
POST /import— the backend extracts GPS, EXIF, weather (Open-Meteo) and location names (OpenStreetMap Nominatim), resizes images, and writes everything to Firestore. - The React frontend reads the REST API and renders the gallery.
| Layer | Technology |
|---|---|
| Backend | Java 21 + Spring Boot 3.x (DDD / hexagonal) |
| Database | Cloud Firestore |
| File storage | Google Cloud Storage |
| EXIF extraction | metadata-extractor |
| Image resizing | Scrimage |
| Location names | OSM Nominatim (free, no key) |
| Weather | Open-Meteo historical archive (free, no key) |
| Frontend | React + Vite + Leaflet |
| Hosting | Firebase Hosting |
| Compute | Cloud Run (scales to zero) |
| IaC | Terraform |
| CI/CD | GitHub Actions + Workload Identity Federation |
No GCP account or credentials needed. The local Spring profile wires in-memory adapters and stubs, and seeds 3 real galleries (Azores, Iceland, Italy) from test fixtures at startup.
| Tool | Version |
|---|---|
| Java | 21+ |
| Maven wrapper | included (./mvnw) |
| Node.js | 20+ |
| npm | included with Node |
cd backend
./mvnw spring-boot:run -Dspring-boot.run.profiles=localThe server starts on http://localhost:8081 in a few seconds. You should see log lines like:
Seeding local dev data from .../src/test/resources/galleries…
Seeded Azores/DSC_0892.JPG
Seeded Iceland/DSC_0114.JPG
…
Local dev seed complete — 3 galleries, 4 pictures
In a second terminal:
cd frontend
npm install # first time only
npm run devOpen http://localhost:5173 — the Vite dev server proxies /api/* to the backend automatically.
- World map with markers for Azores, Iceland and Italy (real GPS from the fixture JPEGs).
- Click a marker → gallery page with per-picture thumbnail markers on a local map.
- Click a thumbnail or map pin → full-size overlay with EXIF, GPS coordinates, altitude, and weather panel.
| Method | Path | Description |
|---|---|---|
GET |
/api/galleries |
List all galleries with average GPS and picture count |
GET |
/api/galleries/{id} |
Gallery detail with all picture metadata |
POST |
/import |
Start an async import job; returns 202 with a jobId |
GET |
/import/{jobId} |
Live import progress (galleries/pictures processed, errors) |
Example:
# Start import
curl -X POST http://localhost:8081/import
# Check progress
curl http://localhost:8081/import/{jobId}cd backend
# Unit tests only (fast, no Docker needed)
./mvnw test
# Unit + integration tests (Firestore emulator via Testcontainers — needs Docker)
./mvnw verifyThe integration test (GalleryRepositoryUsingFirestoreIT) pulls gcr.io/google.com/cloudsdktool/cloud-sdk:emulators on first run; subsequent runs use the cached image.
macOS + Docker Desktop: before running
./mvnw verify, go to
Docker Desktop → Settings → Advanced → enable "Allow the default Docker socket to be used".
The backend ships a Makefile that wraps the everyday commands. Run it from backend/:
cd backend
make help # list every target with a descriptionLocal development
| Target | What it does |
|---|---|
make run |
Run the backend locally (profile local, port 8081, seeds test galleries) |
make test |
Unit tests (fast, no Docker) — same as ./mvnw test |
make verify |
Unit + Firestore-emulator integration tests (needs Docker) — same as ./mvnw verify |
make build |
Clean build the runnable jar |
make clean |
Remove build artifacts |
make docker-up / make docker-down |
Start / stop the backend via docker-compose |
Import — local (in-process /import endpoint, local profile only)
| Target | What it does |
|---|---|
make import-local |
Trigger an import and follow its progress until COMPLETED/FAILED |
make import-status-local JOB=<id> |
One-shot status of a specific job |
make import-localtalks to a running backend — start it withmake runin another terminal first.
Import — prod (the mappics-import-job Cloud Run Job; the /import endpoints 404 in prod)
| Target | What it does |
|---|---|
make import-prod |
Trigger the prod import job (returns immediately; follow it with make import-logs-prod) |
make import-logs-prod |
Tail recent import-job logs (last hour) |
make import-executions-prod |
List executions — check for a Running one before triggering |
Prod targets need
gcloudauth and a configured region/project. Either set them ingcloud config(the Makefile readsrun/regionandproject) or pass them inline:make import-prod REGION=europe-west1 PROJECT=my-project.Don't start overlapping prod imports — two at once would exceed OSM Nominatim's 1 req/s limit (the import itself is idempotent). See
.claude/plans/import-cloud-run-job.md.
mappics-v3/
├── backend/ ← Spring Boot backend
│ ├── src/
│ │ ├── main/java/com/antodippo/mappics/
│ │ │ ├── domain/ ← aggregates, value objects, port interfaces
│ │ │ ├── application/ ← import use case, job tracking
│ │ │ └── infrastructure/← adapters (GCS, Firestore, OSM, Open-Meteo…)
│ │ └── test/
│ ├── Dockerfile
│ └── pom.xml
│
├── frontend/ ← React + Vite frontend
│ ├── src/
│ │ ├── api/client.js ← fetchGalleries(), fetchGallery()
│ │ ├── components/ ← PictureOverlay
│ │ └── pages/ ← MapPage, GalleryPage
│ ├── firebase.json
│ └── vite.config.js ← /api/* proxy → localhost:8081
│
├── infrastructure/
│ ├── gcp/ ← Terraform: APIs, GCS, Firestore, Cloud Run,
│ │ │ Artifact Registry, Firebase Hosting, WIF
│ │ └── *.tf
│ └── README.md ← full infra setup guide (Terraform + manual)
│
└── .github/
└── workflows/
├── backend.yml ← test → build → push image → deploy Cloud Run
├── frontend.yml ← build (with Cloud Run URL) → Firebase deploy
└── terraform.yml ← plan on PRs, apply on merge to main
See infrastructure/README.md for the full guide, including:
- Terraform setup and first apply
- GCS buckets, Firestore, Artifact Registry, Cloud Run, Firebase Hosting
- Workload Identity Federation (no long-lived service account keys in CI)
- GitHub Secrets and Variables needed for each pipeline
- Firebase CLI first-time deploy
After terraform apply, the values you need are printed as outputs:
cd infrastructure/gcp
terraform output # prints all values| GitHub Secret | Source |
|---|---|
WIF_PROVIDER |
terraform output -raw workload_identity_provider |
CICD_SERVICE_ACCOUNT |
terraform output -raw cicd_service_account_email |
TERRAFORM_SERVICE_ACCOUNT |
terraform output -raw terraform_service_account_email (after first apply) |
| GitHub Variable | Source |
|---|---|
GCP_PROJECT_ID |
your GCP project ID |
GCP_REGION |
e.g. europe-west1 |
ARTIFACT_REGISTRY_URL |
terraform output -raw artifact_registry_url |
TF_STATE_BUCKET |
name of the GCS bucket for Terraform state |
TF_SOURCE_BUCKET |
terraform output -raw source_bucket_name |
TF_PROCESSED_BUCKET |
terraform output -raw processed_bucket_name |