Skip to content

Accessing your data from other tools

QGIS · GeoLibre · DuckDB · Python · R · anything that speaks HTTP.

GeoDeploy is cloud-native: rather than running heavy XML-era OGC services, it shares data through formats and APIs that clients read directly over HTTP — Cloud-Optimized GeoTIFF, XYZ tiles, and GeoParquet — discovered through a built-in STAC catalog, plus a standards-based OGC API - Features service (the WFS successor) so any GIS can read a layer with no GeoDeploy-specific knowledge.

Four surfaces, four jobs:

Surface For Start at
Instance index seeing what an instance publishes, from nothing but its URL /api/public
OGC API - Features reading features in any GIS (QGIS, ArcGIS, FME, GDAL) /api/ogc
STAC discovering what an instance holds, and where each asset lives /api/stac
Tiles (WMTS · XYZ · TileJSON · PMTiles · COG) drawing big layers fast per-layer, see below

In the app, the Share links panel (link icon on any ready layer in My Data) hands you the right URL for each of these, labelled with the exact menu path in each tool.

Start from the URL alone

GET /api/public answers "what does this instance offer?" with no credentials: the published public portals, and the public layers grouped by kindraster, postgis, geoparquet — each with the access URLs that suit it.

curl https://geodeploy.example.org/api/public | jq '.layers.postgis[].name'

Or, with the command line, which formats it and needs no account either:

geodeploy browse https://geodeploy.example.org

Each portal entry carries a style_url: the published bundle's own style.json, listing every source and layer in that portal. One anonymous fetch describes the whole map.

Only what has been deliberately shared appears — a published public portal, a layer whose visibility is public.

An admin can turn the listing off in Settings → Infrastructure → Public listing (or with geodeploy admin public-index --off), which makes the endpoint answer 404. That changes whether your portals can be found, not who may open them: a published public portal stays reachable by its link either way.

Downloading a whole layer

A raster and a GeoParquet layer are files, so they download directly (/cog, /parquet/…). A PostGIS layer is a table, so the instance builds the file for you:

# queue it, poll it, fetch it — or let the CLI do all three
geodeploy layers download roads --format gpkg
POST /api/data/vector/{id}/export        {"format": "gpkg"}      → {"job_id": …}
GET  /api/data/vector/{id}/export-status/{job_id}                → queued|processing|ready|error
GET  /api/data/vector/{id}/export-download/{job_id}              → a zip

Formats: gpkg, csv, geojson, and geoparquet for file-backed layers. Add a bbox to clip to an area, and "target_crs": "native" to keep the layer's own CRS where the format can carry it. The zip includes a MANIFEST.txt recording the extent, the CRS and the row count of every file it contains.

Public layers export without a token, on the same terms as their other artifacts.

Layers bigger than a million features

A built export is capped — 1,000,000 features per layer for a whole layer, 50,000 for a bbox clip — because the archive is assembled in worker memory. When the cap bites, the export says so in three places: MANIFEST.txt, the truncated list on the status response, and the CLI's exit code. It is never silent.

The cap applies only to files the instance builds. Every complete path is either paged or already a file, and none of them is capped:

Paged, and GDAL follows the paging itself — this is the general answer for a large PostGIS layer:

ogr2ogr -f GPKG roads.gpkg "OAPIF:https://geodeploy.example.org/api/ogc" roads
# or page it yourself: follow rel="next" until it stops appearing
url = "https://geodeploy.example.org/api/ogc/collections/vector-a7f3.../items?limit=10000"

Requires OGC sharing enabled on the layer (geodeploy layers share roads --ogc).

The stored files, byte for byte — no server-side work at all:

geodeploy layers download parcels          # manifest + every partition, into a folder
-- or straight from DuckDB, no download step
SELECT count(*) FROM read_parquet(
  'https://geodeploy.example.org/api/data/vector/<uid>/parquet/*.parquet');

Already one file:

curl -O https://geodeploy.example.org/api/data/raster/<uid>/cog

An administrator can raise the cap with EXPORT_FEATURE_CAP on the API and worker containers, but raising it far trades a truncated download for a worker that runs out of memory. Prefer the paths above.

About the identifiers in these URLs. A layer is addressed by a short opaque id such as vector-a7f3c91b04e2 — deliberately not a row number. A row number is unique only within one layer kind and one database: vector and raster are numbered separately, and a restore or a move to another instance renumbers everything, which would make old links quietly return someone else's data. The opaque id is stable for the life of the layer and 404s honestly once it is gone. Links that used numbers still work, but nothing hands one out. Store the opaque id, not the number.

Sharing a layer (admin)

Nothing is shared by default. In My Data, click the globe icon on a ready layer to list it in the public catalog (a "Public data" badge appears). Optional catalog metadata — abstract, keywords, license, attribution — can be set via the API:

curl -X PUT https://YOUR-HOST/api/data/vector/5/sharing \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"is_public": true, "abstract": "Land cover 2018", "license": "CC-BY-4.0",
       "keywords": "landcover, france", "attribution": "© IGN"}'

What the flag controls: discovery (the layer's entry in the STAC catalog) and, for rasters, the raw COG endpoint. Portal display endpoints (tiles, viewport features) are always addressable by id — publishing a portal already exposes its layers' rendering.

The STAC catalog

Entry point: https://YOUR-HOST/api/stac

  • GET /api/stac — catalog root (STAC 1.0.0, API core + collections + item-search)
  • GET /api/stac/collections — two collections: vectors, rasters
  • GET /api/stac/collections/{id}/items — one STAC Item per shared layer, with ready-to-use asset URLs
  • GET /api/stac/search?bbox=minx,miny,maxx,maxy&collections=rasters&limit=50 — item search

Works with QGIS (native STAC support in 3.40+, or the STAC API plugin: add https://YOUR-HOST/api/stac as a connection), stac-browser, and pystac-client:

from pystac_client import Client
cat = Client.open("https://YOUR-HOST/api/stac")
for item in cat.search(bbox=[-5, 42, 9, 51]).items():
    print(item.id, list(item.assets))

OGC API - Features — the one that works everywhere

Entry point: https://YOUR-HOST/api/ogc

If you only read one section, read this one. OGC API - Features is the standard every modern GIS speaks natively, so it is the shortest path from a GeoDeploy layer to somebody else's tool. Every layer you share as Public becomes a collection, whatever it is stored as (PostGIS or GeoParquet) — same URLs, same GeoJSON, EPSG:4326.

  • GET /api/ogc — landing page · GET /api/ogc/conformance — conformance classes
  • GET /api/ogc/collections — one collection per shared vector layer (vector-{uid})
  • GET /api/ogc/collections/{cid}/items?bbox=minx,miny,maxx,maxy&limit=1000&offset=0
  • GET /api/ogc/collections/{cid}/items/{featureId}

Responses carry numberReturned, numberMatched (when it is known), and next/prev links — follow next to walk a whole layer.

QGISLayer ▸ Add Layer ▸ Add OGC API - Features Layer ▸ New, URL https://YOUR-HOST/api/ogc, then pick the layer from the list. Attributes, the attribute table, and identify all work; QGIS requests only your current extent.

GDAL / ogr2ogr — the OAPIF driver takes the collection URL directly:

ogr2ogr -f GPKG roads.gpkg \
  "OAPIF:https://YOUR-HOST/api/ogc/collections/vector-a7f3c91b04e2"

Anything else (Python, R, a browser) — …/items?bbox=… is plain GeoJSON over HTTP.

Also works in: ArcGIS Pro (OGC API server connection) and FME.

What it deliberately does not do: no CRS negotiation (everything is CRS84/EPSG:4326), no CQL2 filtering, no transactions, no OGC API - Tiles or Records — and /api/ogc/conformance claims only Core + GeoJSON, so a spec-driven client is never misled.

Rendering vs. reading. Use OGC API - Features to get the data. For drawing a very large layer quickly, use the tile links below (TileJSON / PMTiles) — they are generalized per zoom and have no attribute queries. GeoLibre reads TileJSON under Add data ▸ OGC API - Tiles (vector). The Share links panel in My Data (link icon on any ready layer) gives you every one of these URLs for a layer, already labelled with the menu path for each tool.

PMTiles: paste the plain URL. …/api/data/vector/{uid}/pmtiles is the archive itself — that is what GeoLibre's PMTiles field, a download, and GDAL's /vsicurl/ all expect. The pmtiles:// prefix you may have seen is a protocol handler the MapLibre GL JS library registers in code; it belongs inside a style source (GeoDeploy's own portals emit it), never in a UI field or an address bar. To load a layer into QGIS, use OGC API - Features — PMTiles is a rendering format.

In QGIS, use Add Vector Layer — not Add Vector Tile Layer. Paste the plain URL:

https://YOUR-HOST/api/data/vector/{uid}/pmtiles

GDAL 3.8+ (QGIS 3.34 and later) opens it with its PMTiles driver — verified against a live instance: driver=PMTiles, one layer, features readable. No pmtiles:// prefix and no /vsicurl/ needed, though /vsicurl/<url> works too. Add Vector Tile Layer cannot open it at all: that dialog builds an XYZ template (type=xyz&url=…{z}/{x}/{y}) and an archive is a single file, so it answers "Invalid Data Source". Older GDAL cannot read PMTiles at all.

For most QGIS work OGC API - Features is still the better route: it carries full attributes and is not generalized per zoom.

Consuming the assets

Rasters (Cloud-Optimized GeoTIFF)

  • QGIS / GDAL — full pixel access, no download: add a raster layer with the URL /vsicurl/https://YOUR-HOST/api/data/raster/{id}/cog. Range requests fetch only the tiles and overviews you look at (this is the modern replacement for WCS).
  • Download: the same URL fetched normally returns the whole GeoTIFF.
  • WMTS — the one to use in QGIS: …/api/data/raster/{id}/wmts, added under Layer ▸ Add Layer ▸ Add WMS/WMTS Layer ▸ New. It carries the layer's extent, so Zoom to Layer goes to the data. QGIS does not read TileJSON for a raster, which is why an XYZ connection leaves it guessing at the whole world.
  • TileJSON — the one to use in MapLibre, deck.gl or GeoLibre: …/api/data/raster/{id}/tilejson wraps the tile template with the layer's bounds and saved styling.
  • XYZ tiles (display only, no extent): the item's tiles asset is a TiTiler tile template — paste it into a QGIS XYZ Tiles connection or any web map. Prefer WMTS or TileJSON above: a bare {z}/{x}/{y} template has nowhere to carry an extent, so nothing that reads it can zoom to the layer. Tiles outside the raster come back transparent rather than as errors.

Vector layers served from PostGIS

  • XYZ vector tiles (display only): the vector-tiles asset (https://YOUR-HOST/tiles/{schema}.{table}/{z}/{x}/{y}) pastes into a QGIS Vector Tiles connection. Tiles are generalized per zoom — for full-fidelity data use the portal's select-and-download tool, or store the layer as GeoParquet.

Vector layers stored as GeoParquet

A prepared layer is a spatially partitioned GeoParquet dataset: a prefix of __cell=N/*.parquet files plus a manifest.json describing the partition grid and per-cell files. All of it is served with HTTP Range support.

  • Viewport queries (simplest):
  • …/api/data/vector/{id}/features.geojson?bbox=minx,miny,maxx,maxy&limit=50000 → GeoJSON
  • …/api/data/vector/{id}/features.arrow?bbox=…&limit=… → GeoArrow (Arrow IPC stream)
  • DuckDB — query the dataset in place:
-- discover the files
-- curl https://YOUR-HOST/api/data/vector/5/parquet/manifest.json
SELECT count(*)
FROM read_parquet([
  'https://YOUR-HOST/api/data/vector/5/parquet/__cell=137/data_0.parquet',
  'https://YOUR-HOST/api/data/vector/5/parquet/__cell=138/data_0.parquet'
]);
-- every partition file carries a GeoParquet 1.1 bbox covering column: filter on
-- struct_extract("bbox", 'xmin') etc. for row-group pruning, exactly like GeoDeploy does.

A small script can read the manifest, pick the cells overlapping an area of interest (cell = ix*grid + iy on the manifest's grid), and hand DuckDB just those files. - QGIS/GDAL: single .parquet files open via /vsicurl/https://YOUR-HOST/api/data/vector/{id}/parquet/__cell=N/data_0.parquet (GDAL ≥ 3.5 with the Parquet driver).

GeoLibre

GeoLibre reads GeoDeploy layers directly — it is a lightweight, cloud-native GIS that speaks the same modern formats, so nothing has to be exported or converted.

In GeoLibre, choose Paste
Add data ▸ OGC API - Features the layer's OGC API - Features URL
Add data ▸ OGC API - Tiles (vector) the layer's TileJSON URL — fastest for drawing a big layer
PMTiles the layer's PMTiles URL, as a plain https:// address with no prefix
COG a raster's Cloud-Optimized GeoTIFF URL

Every one of those URLs is on the layer's Share panel in GeoDeploy, ready to copy.

Round-tripping is on the roadmap

Today the flow is one-way: publish here, open there. Planned next is two-way — import a GeoLibre project's layers and styling into a portal, and push a portal's layers and symbology back out. Both sides already speak the MapLibre style specification, which is the shared ground that makes it tractable. See the roadmap.

Which standard for which job

Every access path here is a modern, HTTP-native standard. There is one for each kind of job:

You want to… Use
Read features with their attributes OGC API - Features
Draw a large vector layer quickly Vector tiles (TileJSON) or PMTiles
Read raster pixels, or a subset of them Cloud-Optimized GeoTIFF over HTTP Range
Draw a raster quickly XYZ raster tiles
Analyse a large table columnar-style GeoParquet
Discover what exists and its metadata STAC

The XML-era OGC services — WMS, WFS, WCS, CSW — are deliberately not provided. Each has a modern replacement in the table above that clients read directly over HTTP, without a heavyweight service in front of it: OGC API - Features instead of WFS, COG over Range instead of WCS, XYZ/TileJSON instead of WMS, STAC instead of CSW. Keeping to those is what lets GeoDeploy run on a small server. Clients that specifically require the older protocols are out of scope.

In QGIS, without any of this

There is a GeoDeploy plugin for QGIS: paste an instance URL and browse what it publishes — no account needed for public data — add a layer using the fastest source it offers, open a whole portal as a styled group, and upload back. It carries symbology in both directions, so a layer opens looking like the portal and what you restyle there goes home. The protocols below are what it uses underneath, and remain the right choice for anything scripted.

Not yet implemented

  • Private catalog access via API token (shared layers are public; unshared layers are simply not listed).
  • Single-file GeoParquet download of a partitioned dataset (merge-on-demand).
  • OGC API - Features extensions: CRS negotiation (Part 2), CQL2 filtering (Part 3), transactions, and property filters/queryables. Core + GeoJSON only, as /api/ogc/conformance states.
  • OGC API - Tiles and OGC API - Records (tiles are TileJSON/PMTiles; the catalog is STAC).
  • Single-feature access (/items/{featureId}) for a GeoParquet layer whose dataset has no id-like column (id/fid/gid/objectid/…) — the collection still pages normally.