Immich Places is an add-on web UI for existing Immich instances that helps you assign GPS coordinates to photos that are missing location data.
The app has two parts:
- Frontend: Next.js app (served on port
3032) for map review, bulk selection, and location assignment. - Backend: Go service that syncs metadata from Immich, stores local state in SQLite, and performs geocode/sync jobs.
- Fix geolocation for older photos with missing GPS.
- Keep existing Immich users and albums in sync while you enrich missing coordinates.
- Run the workflow from a fast map + list interface instead of manual per-item workflows.
Map & Location
- Interactive map with clustered markers (Leaflet)
- Drag-and-drop a photo onto the map to assign coordinates
- Geocoding search with autocomplete and history (Nominatim by default, optional HERE and Google Maps providers)
- Favorite places: star locations from search results for quick access
- Smart suggestions: locations from nearby-in-time photos, same-day, nearby-day, weekly patterns, frequent spots, and album context
- GPX import: upload one or multiple GPX tracks to batch-assign coordinates to photos by timestamp
Photo Browsing
- Photo grid with configurable columns and page size
- Filter by GPS status (missing / present / all) and by album
- Go to location: jump to a photo's position on the map from the grid
- Fullscreen lightbox with keyboard navigation
- Multi-select with shift+click for bulk operations
Sync & Data
- Background sync keeps assets up to date with Immich (configurable interval)
- Manual sync trigger from the UI
- Local SQLite database — no changes to Immich until you explicitly save
- Confirmation bar to review and save/cancel pending coordinate changes
- An existing Immich instance already running (local or remote).
- Docker and
docker composeon the target host. - A valid Immich API key for the user who will configure Immich Places.
- Access to a shell on the Immich Places host.
From this repo root, create .env with the values below:
cp .env.example .envThen edit .env:
IMMICH_URL: Base URL used by the backend to call your Immich API.ENCRYPTION_KEY: Random key used to encrypt stored Immich API keys in the local DB.TRUST_PROXY_TLS: Set totruewhen behind HTTPS reverse proxy (default behavior).IMMICH_EXTERNAL_URL: Optional, only when browser-facing Immich links differ from backend-to-Immich calls.CARTO_API_KEY: Optional. Free CARTO basemap key. Without it the map works but every tile is stamped "API KEY REQUIRED" — see CARTO basemap.
Example:
IMMICH_URL=http://immich-server:2283
ENCRYPTION_KEY=$(openssl rand -hex 32)
TRUST_PROXY_TLS=true
SYNC_INTERVAL_MS=300000From the repo root:
docker compose up -d --builddocker network create immich-places-net
docker run -d --name immich-places-backend --network immich-places-net --env-file .env -v immich-places-data:/data ghcr.io/majorfi/immich-places-backend
docker run -d --name immich-places --network immich-places-net -p 3032:3032 -e PORT=3032 -e BACKEND_URL=http://immich-places-backend:8082 ghcr.io/majorfi/immich-placesThe frontend command takes its configuration from -e flags rather than .env. See CARTO basemap to add a basemap key.
Then open:
http://localhost:3032
- Register or log in to Immich Places.
- On first login, provide your Immich API key when prompted.
- Once the key is accepted, the map view opens automatically.
- Wait for sync completion, then start correcting assets without GPS.
Immich Places validates the key with GET /api/users/me and then calls:
GET /api/search/metadata(read assets for sync/search)GET /api/albumsandGET /api/albums/{id}(album listing and album contents)GET /api/stacks(stack synchronization)PUT /api/assets(bulk write updates when saving coordinates)GET /api/assets/{id}/thumbnail(asset preview thumbnails)
The following Immich permissions are required:
user.readasset.readasset.updateasset.viewalbum.readlibrary.read(if External Library is used)stack.read
IMMICH_URL(required): Immich base URL visible from the backend container.ENCRYPTION_KEY(required): Must be stable across restarts; changing it will make stored keys unreadable.TRUST_PROXY_TLS(required unless insecure mode): Must match your deployment TLS posture.ALLOW_INSECURE(optional): Set totrueonly for local non-HTTPS testing.
IMMICH_EXTERNAL_URL(optional): Public Immich URL for link generation/fallback behavior.FRONTEND_PORT(default3032): Frontend port exposed to the host.REGISTRATION_ENABLED(defaulttrue): Set tofalseto disable new users.SYNC_INTERVAL_MS(default300000): Background sync frequency in milliseconds.SUGGESTIONS_NEIGHBOR_WINDOW_HOURS(default6): For "Nearby in time" suggestions, how many hours before/after a photo to search for geolocated neighbors. Widen it if your library is geotagged sparsely; narrow it to avoid suggesting a location from a different outing on the same day.DATA_DIR(default/data): Backend DB path inside container.PORT(default8082): Backend listen port inside container.BACKEND_URL(frontend): Backend service URL used by the Next.js rewrite, default ishttp://backend:8082.NEXT_PUBLIC_BACKEND_BASE(frontend): Client API base path, default/api/backend.CARTO_API_KEY(frontend): CARTO basemap API key. Without it the map tiles carry an "API KEY REQUIRED" watermark. See CARTO basemap.DEFAULT_TIMEZONE: IANA timezone fallback for GPX import when auto-detection fails (e.g.Europe/Vienna).DAWARICH_URL: URL for Dawarich location history import integration.DAWARICH_SYNC_INTERVAL_MS(default86400000): Dawarich sync frequency in milliseconds (default: 24 hours).DEBUG(defaultfalse): Set totrueto enable verbose HTTP request logging.
Nominatim (OpenStreetMap) is used by default and requires no API key. You can optionally add HERE Maps and/or Google Maps as additional providers to improve geocoding quality.
The provider chain always starts with Nominatim, then tries each configured provider in order. The first provider that returns a strong result wins. If a provider returns a weak result (just coordinates or "Unknown"), the next provider in the chain is tried.
Configure with GEOCODE_PROVIDER as a comma-separated list:
| Value | Chain |
|---|---|
nominatim (default) |
Nominatim only |
here |
Nominatim -> HERE |
google |
Nominatim -> Google |
here,google |
Nominatim -> HERE -> Google |
Provider env vars:
| Variable | Purpose |
|---|---|
GEOCODE_PROVIDER |
Comma-separated provider chain (default: nominatim) |
HERE_API_KEY |
API key for HERE Maps geocoding |
GOOGLE_API_KEY |
API key for Google Maps geocoding |
GEOCODE_API_KEY |
Legacy fallback key (used if provider-specific key is not set) |
GEOCODE_TIMEOUT |
Upstream geocode request timeout in seconds (default: 10) |
Getting API keys:
- HERE Maps: Sign up at developer.here.com. Free tier includes 250,000 requests/month.
- Google Maps: Enable the Geocoding API in Google Cloud Console. Free tier includes roughly 10,000 requests/month.
The map tiles come from CARTO. Since August 2026 CARTO requires an API key for its raster basemaps and stamps an "API KEY REQUIRED" watermark across tiles served without one. The app works without a key — the watermark is the only difference — but a key is free and removes it.
Getting a key:
Request one at carto.com/basemaps/apikey. The form asks for your email, the domain the maps will be served from, and a one-line description of the project. No CARTO account is needed and there is no approval queue — the key is emailed back immediately.
The free allowance is 5 million tile requests per calendar month, counted across raster and vector. Past that CARTO gets in touch rather than cutting access off.
Setting it:
| Deployment | How |
|---|---|
| Option A (Docker Compose) | Add CARTO_API_KEY=<your-key> to .env, then docker compose up -d |
| Option B (pre-built images) | Add -e CARTO_API_KEY=<your-key> to the frontend docker run command |
The key belongs to the frontend container, not the backend. Unlike HERE_API_KEY and GOOGLE_API_KEY, which the backend uses for geocoding, this one is used by the browser when it requests tiles. It is read at startup, so recreate the container after changing it.
Notes:
- The key is visible in the browser's tile requests. That is inherent to CARTO's raster basemaps, which are keyed client-side. Use a key issued for this instance and do not reuse it across unrelated projects.
- A wrong key looks exactly like no key: CARTO returns the watermarked tile with HTTP 200 and no error. If the watermark persists after setting the variable, check the value before suspecting anything else.
- Keep the CARTO and OpenStreetMap attribution visible on the map. It is on by default and is a condition of the free tier.
- For a containerized Immich stack, point
IMMICH_URLat the Immich service name. - For remote Immich behind HTTPS, use
https://...directly inIMMICH_URL. - You can disable registration (
REGISTRATION_ENABLED=false) once your admin account exists. - First sync may take longer on large libraries. Check service logs if anything stalls.
- Frontend logs:
docker compose logs -f frontend - Backend logs:
docker compose logs -f backend - If startup fails on
ENCRYPTION_KEY, confirm.envis in the project root and contains the key. - If the map shows an "API KEY REQUIRED" watermark, set
CARTO_API_KEYand recreate the frontend container — see CARTO basemap.
As with any software, there may still be bugs, edge-case errors, or incomplete hardening details.
We aim to keep behavior safe, stable, and security-aware, but no software is perfect.
We used AI models as a drafting and review aid during implementation.
It was not vibe-coded. Design decisions major part of the implementation were still done by a human.
Use this project at your own risk.
