Read-only access to a self-hosted GeoLens spatial catalog: datasets, features, maps, sandboxed SQL.
English | Español | Français | Deutsch | 简体中文
Turn scattered GIS files into a searchable catalog and shareable maps—on your own infrastructure.
GeoLens brings files, database tables, service snapshots, and remote assets into one spatial catalog you control. Search metadata, preview data, and keep every dataset’s origin visible. Build maps in the browser, publish links or embeds, and keep using QGIS and open standards alongside your team’s existing tools.
git clone https://github.com/geolens-io/geolens.git && cd geolens
bash scripts/install.sh # read it first: it writes .env, generates secrets, runs docker compose up -d
# Open http://localhost:8080, then log in with the credentials you chose
Or the one-line form, which runs the same script and pulls the prebuilt images:
curl -fsSL https://getgeolens.com/install.sh | sh
Images are published for linux/amd64 and linux/arm64. A fresh install runs six containers at about 1.3 GB resident.
GeoLens has no telemetry and phones home to nothing, except default basemap tiles from tiles.openfreemap.org until an administrator configures another provider. Features you opt into can make outbound calls: AI assist to your chosen OpenAI-compatible endpoint or Anthropic key, OAuth/OIDC sign-in, SMTP, remote or S3 data sources, and off-site backups.
Upload files, create datasets in the browser, or register tables already in GeoLens’s own PostGIS database without copying them. WFS, ArcGIS FeatureServer, and OGC API Features imports create one-shot copies; remote STAC assets remain live references. GeoLens records each dataset’s origin, indexes catalog metadata with pg_trgm for fuzzy search, and can add pgvector semantic ranking after you configure an embedding provider and enable semantic search.
[!NOTE] API stability. The standards surfaces (OGC API Features/Records, STAC, and the tile endpoints) track their specifications and are safe to build against. GeoLens's own REST API can still change between minor releases: contract changes are listed in the CHANGELOG, and breaking ones keep the old form working for at least one more minor release. Hit a rough edge? Open an issue.
Full user, admin, and API documentation lives at docs.getgeolens.com. The Reference table below links each guide.
GeoLens is published through the standard package registries:
pip install geolens # Python SDK
pip install geolens-cli # CLI; installs the `geolens` command
pip install geolens-mcp # MCP server for coding agents (read-only)
npm install @geolens/sdk # TypeScript/JavaScript SDK
Prebuilt public API and frontend images are published to GitHub Container Registry:
docker pull ghcr.io/geolens-io/geolens-api:latest
docker pull ghcr.io/geolens-io/geolens-frontend:latest
The latest tag tracks the newest published stable release.
Spatial data ends up scattered: shapefiles on shared drives, tables in database schemas, rasters in cloud buckets, metadata in spreadsheets. Finding the right dataset means asking Slack or grepping file servers. Sharing it means exporting, emailing, and hoping the CRS matches.
GeoLens replaces that workflow:
The examples below use a JWT bearer token. Mint one against the local stack (the login endpoint accepts an OAuth2 password form, so use -d with form fields, not JSON). Substitute your admin username and the password from .env (grep '^GEOLENS_ADMIN_PASSWORD=' .env):
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login/ \
-d 'username=admin&password=<your-admin-password>' | jq -r '.access_token')
Semantic search takes a one-time admin setup: an embedding provider and the AI + Semantic Search toggles in the admin AI settings, plus an embedding backfill for data ingested before setup (the search guide walks through it). Once that's on, search datasets by meaning instead of exact keyword matches:
# Semantic search ranks by meaning: "hydrology" surfaces the lake and river
# network datasets whose titles never mention the word
curl "http://localhost:8080/api/search/datasets/?q=hydrology&limit=3" \
-H "Authorization: Bearer $TOKEN" | jq '.features[].properties.title'
One search-endpoint behavior to know when consuming it programmatically: the
first page augments the dataset results with up to five matching collections,
so numberReturned can exceed limit on page 0 only. That is deliberate, not
a bug — limit still bounds the number of datasets per page.
Every dataset is also a standard OGC API Features endpoint:
# Grab a public collection id from the catalog. Search anonymously (no token) so
# the id is one anyone can read, matching the unauthenticated items request below.
CID=$(curl -s "http://localhost:8080/api/search/datasets/?q=countries&limit=1" \
| jq -r '.features[0].id')
# GeoJSON features with a bbox filter, works in QGIS, ArcGIS, any OGC client
curl "http://localhost:8080/api/collections/$CID/items?bbox=-10,35,30,60&limit=5"
PostGIS and pgvector share one database, so with semantic search enabled you can rank datasets by meaning inside a spatial window in a single query. See the search guide for how semantic and spatial search work together.
Connect directly from QGIS: Layer > Add WFS / OGC API Features and point at http://localhost:8080/api/.
The same endpoints from the tools you already use: geolens-examples holds single-file MapLibre, Leaflet, OpenLayers and ArcGIS JS pages, QGIS and DuckDB walkthroughs, both GeoLens SDKs, a semantic catalog search, a STAC browser, a saved-map embed, a Python/GeoPandas analysis, a catalog-as-code manifest for the CLI, and an MCP setup. The read-only ones run against the live demo, and CI replays them there on every push and once a week, so what you copy is code that worked this week. Browse the gallery.
Each example above has a full guide in the docs. What GeoLens reads, writes, and exposes:
area_sqm and length_m columns, and intersect writes the pairwise overlay with attributes from both sides/queryables) and OGC API - Records; STAC API 1.0 catalog endpoint; JSON-LD catalogs for DCAT 3, DCAT-US 3.0, and GeoDCAT-APcols=<column>,<column> query parameter to a tile URL to opt specific columns in at every zoom (names are validated against the dataset's columns, unknown names are dropped)Prerequisites: Docker Engine 24+ and Docker Compose v2. The bundled stack
ships PostgreSQL 18. If you point GeoLens at an externally managed database, it
must be PostgreSQL 15+ (for NULLS NOT DISTINCT unique indexes) with
pgvector 0.5+ (for HNSW semantic-search indexes), plus PostGIS, pg_trgm, and
unaccent. The API and worker run in containers (Python 3.14 bundled, no host Python needed). The
optional CLI runs on your host and requires Python 3.11+; the Python SDK and
seed scripts require Python 3.10+.
Clone the repo and run the installer from the checkout. You can read the script before running it; from a clone it builds the images locally:
git clone https://github.com/geolens-io/geolens.git
cd geolens
bash scripts/install.sh
The one-line form runs the same script and pulls the prebuilt, version-pinned images instead of building them:
curl -fsSL https://getgeolens.com/install.sh | sh
Either way, scripts/install.sh copies .env.example to .env, generates a JWT signing
secret, sets up admin credentials, and runs docker compose up -d. The admin username
defaults to admin; the admin password is auto-generated as a strong random value
(written to .env, never printed to your terminal) unless you supply your own.
For unattended installs, set GEOLENS_ADMIN_USERNAME and GEOLENS_ADMIN_PASSWORD in the
environment before running and the prompts are skipped. Re-running the script is idempotent:
existing values in .env are preserved.
Wait about 60 seconds for services to start, then open http://localhost:8080.
Log in with your admin username and the generated password (retrieve it with
grep '^GEOLENS_ADMIN_PASSWORD=' geolens/.env — the one-line installer clones
into geolens/ under the directory you ran it from; inside a source checkout
it's just .env).
Verify all services are healthy:
docker compose ps
First-run notes: the one-line install pulls prebuilt images and is up in about
a minute (only the small PostGIS + pgvector database layer builds locally). Cloning
and running bash scripts/install.sh instead builds every image from source:
5-10 minutes on the first run (GDAL + Postgres extensions + the frontend bundle);
subsequent starts settle in ~60 seconds either way. If ports 5434/8001/8080 are
already taken, change DB_PORT, API_PORT,
or FRONTEND_PORT in .env. For port conflicts, stuck startups, out-of-memory,
and migration warnings, see the Troubleshooting guide.
For production deployment, see the Install Guide. A Kubernetes Helm chart lives in the separate geolens-deployments repo.
Each GitHub Release attaches a SHA256SUMS
file generated by CI alongside install.sh. To confirm a downloaded installer was not tampered
with before running it, download both assets from the same release and place them in the same
directory, then run:
# Linux / Windows WSL
sha256sum -c SHA256SUMS
# macOS
shasum -a 256 -c SHA256SUMS
A passing check prints install.sh: OK.
To upgrade a prebuilt install, run ./scripts/upgrade.sh from your install
directory. It backs up the database, pulls the new images, runs migrations
behind a health gate, and prints a rollback recipe if anything fails. See
UPGRADING.md for the prebuilt and source-build flows plus
rollback, or the online Upgrade Guide.
The repo ships a small city-parks.geojson. Upload and publish it in one command with the GeoLens CLI:
pip install geolens-cli # installs the `geolens` command
geolens login http://localhost:8080/api # use your admin username + password
geolens publish examples/manifests/first-catalog/city-parks.geojson --name "City Parks"
geolens publish runs the upload → preview → commit ingest flow and prints the new dataset's URL. One command takes a local file to a published, mappable dataset.
For repeatable, multi-dataset catalogs, describe your sources in a manifest (geolens.yaml) and apply it with geolens apply. Manifest sources are referenced by HTTP(S) URL, S3 URI, or a path already staged on the server; the examples in examples/manifests/ are templates to adapt. Scaffold a fresh one with geolens init and edit it for your sources:
geolens init # writes geolens.yaml in the current directory
geolens validate geolens.yaml # local schema check, no API call
geolens apply geolens.yaml # validates + applies via /ingest/manifest/apply
See the CLI guide for the full manifest schema, source kinds, and CI integration patterns.
scripts/seed-showcase.py builds seven showcase maps from public open data: a global
tectonics story over real ocean-floor relief, the Manhattan 3D skyline colored by
construction era (the hero above), Atlantic hurricane tracks since 1950, clustered
meteorite falls, the Matterhorn in 2 m lidar 3D terrain, by-reference Sentinel-2
imagery of New York, and a hurricane-exposure map computed in place from the storm
tracks with buffer, intersect and dissolve:
pip install httpx
python scripts/seed-showcase.py --username admin --password "$(grep '^GEOLENS_ADMIN_PASSWORD=' .env | cut -d= -f2-)"
Requires internet access to the upstream open-data sources. See
scripts/README.md for flags (--no-terrain, --prune, …).
GeoLens is a small set of services around a single PostgreSQL/PostGIS database: the API serves the catalog, search, and OGC/STAC endpoints; a worker handles ingestion; and Titiler serves raster tiles from object storage.
flowchart TB
B["Browser: React + MapLibre app"]
OGC["QGIS · ArcGIS · OGC/STAC clients"]
NG["Nginx reverse proxy<br/>serves the React build, routes /api and tiles"]
subgraph Application
API["FastAPI<br/>catalog · semantic search · OGC/STAC · vector tiles"]
W["Worker<br/>GDAL/ogr2ogr ingestion"]
TT["Titiler<br/>COG raster tiles"]
end
subgraph store [Data and storage]
PG[("PostgreSQL 18<br/>PostGIS · pgvector · pg_trgm<br/>+ Procrastinate queue")]
OBJ[("Object storage<br/>local files or S3/MinIO")]
CACHE[("Valkey cache")]
end
B --> NG
OGC --> NG
NG --> API
NG --> TT
API <--> PG
API --> OBJ
API -. tile/query cache .-> CACHE
PG == job ==> W
W --> PG
W --> OBJ
TT --> OBJ
| Component | Technology |
|---|---|
| Frontend | React 19, Vite, MapLibre GL v6, TanStack Query, Tailwind CSS |
| Backend API | FastAPI (Python), GDAL/ogr2ogr, Procrastinate (task queue) |
| Raster Tiles | Titiler (COG tile server) |
| Object Storage | MinIO (S3-compatible, local dev) or any S3 provider |
| Cache | Valkey (tile and query cache) |
| Database | PostgreSQL 18 + PostGIS 3.6 + pgvector + pg_trgm (minimum: PostgreSQL 15, pgvector 0.5) |
| Reverse Proxy | Nginx (production) / Vite dev proxy (development) |
All configuration is managed through environment variables in .env. See the Configuration Reference for the full list of options with defaults and descriptions.
GeoLens ships tuned for a single PostgreSQL instance: the API, worker, and admin
pools fit within 70 of 80 max_connections out of the box (Postgres
max_connections is set to 80), sized by DB_POOL_SIZE (pool_size) and
DB_MAX_OVERFLOW (max_overflow, default 3). See
Connection Pool Tuning
for the per-process budget and how to raise the ceiling.
Automated, scheduled backups run by default. You do not need a --profile backup flag.
The backup service starts alongside api, worker, and db on every
docker compose up and runs pg_dump on a daily/weekly schedule alongside an
archive of the object-storage staging volume, so a restore reproduces a working
instance (DB + uploaded files).
Off-site (S3) upload is additionally gated on BACKUP_S3_ENABLED=true. The
built-in uploader signs requests with AWS Signature V4 (awscli), compatible
with Cloudflare R2, modern AWS S3, and MinIO. A failed upload surfaces a visible
ERROR in container logs (not a swallowed warning), so silent offsite backup
loss is detectable immediately.
For day-2 operations, restore procedures, and incident response, see RUNBOOK.md. For provider-specific configuration options, see Backups & Restore.
The API and worker export Prometheus metrics out of the box (HTTP rate/latency/
errors, job-queue depth, DB pool, tile-cache). Reference scrape config, alert
rules, and a Grafana dashboard ship in infra/monitoring/;
see RUNBOOK.md §4 for the setup steps.
| Guide | Description |
|---|---|
| Install Guide | Step-by-step deployment with Docker Compose |
| Upgrade Guide | Upgrading between versions with rollback procedures |
| Configuration Reference | All environment variables and their defaults |
| Admin Guide | User management, datasets, system health |
| Self-host on managed cloud services | Managed database, object storage, and cache deployment guides |
| CLI & Manifests | Publish files and manage catalogs with the geolens CLI |
| API Reference | Auto-generated reference at docs.getgeolens.com; development-mode stacks also serve Swagger UI at /api/docs (disabled in production) |
| Manifest examples | Template geolens.yaml manifests to adapt: public-cog (remote COG), url-source, s3-source, publication-states |
| Client examples | Runnable browser, QGIS, DuckDB, SDK, CLI, embed, Python, and MCP examples; the read-only ones are verified against the live demo in CI (gallery) |
GeoLens is licensed under the Apache License 2.0. The GeoLens name, logo, and brand assets are not covered by this license. See TRADEMARKS.md. Third-party sample-data attribution is in THIRD_PARTY_DATA.md.
Project policies: governance · maintainers · contributing · security · release process · egress & air-gap.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
uvx geolens-mcpMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-geolens-io-geolens": {
"command": "uvx",
"args": [
"geolens-mcp"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referencegeolens-mcppypiGeoLens works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.