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-Authenticatechallenge, 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-bearergrant, 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_INTERSECTSis 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_SAFEopt-in, so providers are not rebuilt on every request. - [ ] SQL escaping in pygeofilter's
sqlbackend: literals andLIKEpatterns 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.