Skip to content

What fastgeoapi adds to pygeoapi

fastgeoapi is not a fork of pygeoapi. pygeoapi is the engine: the OGC API implementation, the conformance declarations, the providers and the HTML templates all come from upstream, and fastgeoapi tracks its releases. Anything you can serve with pygeoapi you can serve with fastgeoapi, and the responses are the same.

What differs is everything around that engine — how the server is built, where its configuration comes from, who is allowed to call it, which routes exist at all, and how fast it reads data that lives in a bucket.

Area pygeoapi fastgeoapi
Authentication not in scope OIDC/JWT with JWKS, API keys, OPA policies
AI agents MCP server over the same API, with its own OAuth authorization server
Configuration a local file named by PYGEOAPI_CONFIG any object store: S3, GCS, Azure, Tigris, local
Reconfiguration restart the process POST /admin/config/reload, atomic swap
Route table every route of every specification only the specifications the configuration exposes
GeoParquet s3:// via s3fs, no CQL2 any cloud, full CQL2 pushed into DuckDB
Provider instances rebuilt on every request reused, with an explicit thread-safety opt-in

The rest of this page explains each line, and links to the how-to guide that covers it in depth.

Security is the reason fastgeoapi exists

pygeoapi deliberately leaves authentication and authorization to the deployment. fastgeoapi fills that gap with a stack you configure rather than code:

  • OpenID Connect — OAuth2/JWT bearer tokens validated against the issuer's JWKS, with multiple identity providers supported side by side.
  • API keys — for programmatic clients that cannot run an OAuth flow.
  • Open Policy Agent — Rego policies decide per-request, so "this tenant may read these collections" is a policy change rather than a code change.

The authorization layer wraps the mounted pygeoapi application as ASGI middleware, which is why it applies uniformly to every route the engine exposes, including ones added by a future upstream release. Health probes (/healthz, /readyz) are mounted outside the protected surface so orchestrators can reach them without credentials.

Your OGC API, usable by AI agents

fastgeoapi ships a production Model Context Protocol server at /mcp. An assistant like Claude can list your collections, read their queryables, run CQL2 queries and execute processes — against the same data, through the same API, under the same identity a human client would use.

The tools come from your OpenAPI document. They are generated by parsing the OGC API specification the server already publishes, so a collection you add to the configuration becomes callable without writing a tool definition. There is no hand-maintained catalogue to drift out of sync.

There is only one API. The MCP server reaches pygeoapi in-process over an httpx.ASGITransport, not over the network: no second copy of the engine, no loopback hop, no internal API key to provision and rotate.

It is identity-secured, not open. This is where most "expose an API to an agent" setups stop, and it is the hard part. The MCP server plays two roles at once: the protected resource an agent talks to, and its own OAuth Authorization Server — an OIDC proxy fronting whatever IdP you already run (Keycloak, Logto, Ory Hydra, Rauthy, Entra…). Concretely it implements:

  • OAuth 2.0 Protected Resource Metadata (RFC 9728), advertised in the WWW-Authenticate challenge, with RFC 6750 error semantics;
  • authorization code with PKCE, Authorization Server Metadata (RFC 8414) and Dynamic Client Registration (RFC 7591) with redirect-URI validation;
  • refresh-token rotation and a client-facing token TTL decoupled from the upstream IdP's expires_in;
  • CIMD (Client ID Metadata Document) — not theoretical: Claude identifies itself this way against this server in production, including the case of a document that declares no scope, with an SSRF-hardened fetcher and enforcement of the keys and redirect URIs the document publishes;
  • mixed-key JWKS validation (RSA/EC/Ed25519), skipping key types it cannot use instead of rejecting an entire key set;
  • EMA / ID-JAG enterprise-managed authorization through the jwt-bearer grant, currently under end-to-end verification.

It survives being restarted. The transport runs in stateless Streamable HTTP mode, so an auto-suspending machine, a redeploy or a serverless cold start is transparent to a connected client instead of stranding it on a dead session.

The full standards matrix, with what is verified on a live deployment and what is still in progress, is in Supported specifications.

Configuration from a bucket, not from a path

Upstream builds its application at import time: starlette_app.py reads PYGEOAPI_CONFIG and PYGEOAPI_OPENAPI, opens those local files, and constructs the API as a module-level side effect.

fastgeoapi constructs the API programmatically instead — API(config, openapi) from dictionaries it holds in memory. The configuration never has to touch a local disk, which is what makes the rest possible:

  • read the configuration from s3://, gs://, az://, a Tigris bucket, or a local directory, through one storage abstraction with a single code path;
  • run on read-only filesystems (AWS Lambda, distroless containers) where writing a YAML file next to the process is not an option;
  • give each tenant its own configuration object without re-templating an environment variable.

See Config from cloud storage.

Reload without a restart

Because the API is an object rather than an import side effect, fastgeoapi can build a second one and swap it in atomically. POST /admin/config/reload returns 202 immediately, rebuilds in the background, and GET on the same route reports the outcome of the last attempt. It is protected by the same authentication as the rest of the API — security follows the configuration, so there is no second credential to manage. Reloads are idempotent on the configuration object's ETag, so a webhook that fires twice does the work once.

The MCP tools follow too: they are regenerated from the new OpenAPI document, so a collection added to the configuration becomes callable by an agent without restarting anything. A client that is already connected keeps its cached list until it asks again, normally on reconnect.

One limit is worth knowing up front: in a multi-instance deployment the reload reaches only the instance that received the call.

Only the routes your configuration actually needs

pygeoapi registers the full route table of every specification it implements — Features, Tiles, EDR, Processes, STAC, Records — whether or not your configuration has a resource behind them. fastgeoapi groups the route table by specification and mounts a group only when the configuration exposes a provider for it, recomputing the set on every reload.

The result is an OpenAPI document and a route table that describe what the server can really do. /conformance is filtered the same way, from the configured providers rather than from a static list. A parity test keeps the complete table aligned with upstream's, so a route added in a new pygeoapi release turns the suite red instead of silently disappearing.

GeoParquet from any cloud, with CQL2 in the engine

Upstream ships a Parquet provider built on pyarrow and geopandas. It reads s3:// through s3fs — other cloud schemes fall back to pyarrow's auto-detection, with nowhere to pass a region, an endpoint or anonymous credentials — and it filters on bbox, datetime and property equality. There is no CQL2.

fastgeoapi's GeoParquet provider runs on DuckDB with the spatial extension:

  • Any object store, with credentials, region, custom endpoint and public-bucket access as provider options.
  • Full CQL2, text and JSON, translated to SQL and pushed into the engine — spatial predicates included. S_INTERSECTS is evaluated by DuckDB, not post-filtered in Python.
  • GeoParquet 1.1 covering bbox columns used as a pre-filter before the exact geometry test, plus hive-partition and row-group pruning.
  • DuckDB's native cloud reader, which caches the blocks it fetches. On Overture's division-areas (4.47 GB across eight files, read from Europe) a bbox query over Lazio went from 44 s to 0.9 s once warm.
  • No geopandas, shapely or pyarrow on the serving path.

The provider is synchronous — DuckDB has no async API and pygeoapi's provider contract is synchronous — but it does not block the event loop: the factory's shim dispatches provider calls to an executor.

See GeoParquet provider.

Provider instances are reused

pygeoapi.plugin.load_plugin constructs a fresh object on every call, so every request pays for whatever the provider does in __init__. For a DuckDB-backed provider that is 76 ms of connection and extension setup against 2.9 ms of actual query.

fastgeoapi adds a cache in front of load_plugin, keyed on the provider definition. The opt-in is explicit and provider-agnostic: a class that declares THREAD_SAFE = True is shared process-wide, anything else is cached per thread, because upstream hands every request a fresh instance and a provider is entitled to rely on that. A generation counter invalidates the cache whenever the API is rebuilt. Measured end to end, HTTP latency dropped from 73 ms to 24 ms.

An OpenAPI document a client can consume

fastgeoapi resolves the external $refs in the generated document deterministically, so the schema a client downloads today is the schema it downloads tomorrow, and augments it with the security schemes that match the configured authentication. Server URLs follow the reverse proxy the deployment sits behind rather than the port the process happens to bind. The document is validated in CI with Spectral, exercised with contract tests, and scanned with OWASP ZAP.

The same document can be generated offline with the fastgeoapi CLI and written back to the object store, so a deployment can serve a pre-built artifact instead of computing it at boot.

What we aim to send upstream

Several of the items above started as bugs or gaps found while building fastgeoapi, and would serve pygeoapi users beyond this project. They are candidates for upstream contribution, not commitments — this list is updated as each one is actually proposed:

  • [ ] Plugin instance cache with an explicit THREAD_SAFE opt-in, so providers are not rebuilt on every request.
  • [ ] SQL escaping in pygeofilter's sql backend: literals and LIKE patterns are interpolated unescaped today.
  • [ ] Cache invalidation for the localized configuration, so a rebuilt API does not serve stale HTML.
  • [ ] The HTML templates read server.limits, which the configuration schema does not require — either the schema or the template should give way.
  • [ ] An application-factory RFC: building the API without import-time environment variables.
Back to top