A composable, read-only OGC API - Features server in Go.
Every collection declares its storage in a catalog file. The API speaks one public
contract (OGC API - Features) to clients and one internal contract (FeatureStore)
to storage — so a backend can be added or swapped without touching anything above it.
What this is: the code behind a talk at FOSS4G 2026 (Hiroshima). The MongoDB adapter is based on the read path of Vallaris Maps, a geospatial platform in production; the architecture around it was written for this repository. Read-only, OGC API - Features Part 1, two conformance classes, two storage adapters.
Needs Go 1.22 or newer.
go run ./cmd/meros -config examples/catalog-quickstart.ymlcurl localhost:8080/ # landing page
curl localhost:8080/api # the API definition
curl localhost:8080/conformance # what this server actually conforms to
curl localhost:8080/collections # collections from the catalog
curl "localhost:8080/collections/landmarks/items?bbox=132.45,34.39,132.46,34.40&limit=5"
curl "localhost:8080/collections/hiroden/items?limit=3"Or open QGIS → Layer → Add Layer → Add WFS / OGC API - Features → http://localhost:8080.
Set Version to OGC API - Features; on auto-detect QGIS tries classic WFS first and gives up.
docker compose up -d # local MongoDB, seeds itself with the demo landmarks
go run ./cmd/meros -config examples/catalog.ymlThat catalog declares three collections, and the backend: line is the only thing
that differs between where they live:
| Collection | Storage | Geometry |
|---|---|---|
landmarks-mongodb |
MongoDB | points |
landmarks-file |
a GeoJSON file | points — the same ones |
hiroden |
a GeoJSON file | tram lines |
Ask the first two the same question and you get the same features back; the only
difference is the collection name in the links. One server, one catalog, two
databases, and nothing above the backend: line knows which is which.
examples/meros.postman_collection.json
follows the same path in Postman, with assertions on each step — the landing
page carries the links the standard requires, /conformance claims only two
classes, the two landmark collections return identical features, paging offers
next and prev in the right places, and the error cases name what went wrong.
Import it, set baseUrl, and run the folders in order.
It also runs headless:
newman run examples/meros.postman_collection.jsonClients (QGIS · browsers · scripts)
│
OGC API layer ← public contract (stable for clients)
│
FeatureStore ← internal contract (stable for storage)
│
Catalog / Router ← "which storage owns this collection?" (catalog.yml)
│
Storage adapters ← MongoDB · GeoJSON file (read off disk)
internal/core— the domain model and theFeatureStorecontract. Imports no driver, no framework, no config format.internal/api— the HTTP adapter. Importscoreand nothing else: not the storage adapters, not the catalog, not the config format.internal/catalog— catalog loading + a router that itself implementsFeatureStore, so the API never learns that routing exists.internal/store/file— GeoJSON files, read from disk at startup.internal/store/mongodb— MongoDB. All MongoDB vocabulary lives here and nowhere else.cmd/meros— the composition root; the only place that knows every concrete type.
The catalog registers every collection and how to reach it. Credentials never live
in the catalog — the MongoDB URI comes from MEROS_MONGODB_URI.
collections:
- id: landmarks
title: Hiroshima Landmarks
backend: mongodb # or: file
source:
database: meros
collection: landmarks
# geometry_field: geometry (default)Implement one interface with two methods:
type FeatureStore interface {
GetItems(ctx context.Context, collection string, q Query) (*FeatureCollection, error)
GetItem(ctx context.Context, collection, id string) (*Feature, error)
}Neutral types only — no driver vocabulary in the contract. Register the backend in
cmd/meros and declare it in the catalog. Nothing in the OGC layer or the core changes.
go test ./... # unit tests, no database
MEROS_TEST_MONGODB_URI=mongodb://localhost:27017 \
go test ./internal/store/mongodb/ # adapter integration testWrites and transactions (Part 4) · CQL2 filtering (Part 3) · coordinate reference
systems beyond CRS84 (Part 2) · tiles, maps, records. /conformance lists only
what the server actually does, which is why it declares two classes and not more.
Apache-2.0 for the code.
Everything under examples/data/ takes its geometry from OpenStreetMap and carries
OSM's licence instead — © OpenStreetMap contributors, ODbL 1.0. Provenance and
the Overpass queries that produced it are in
examples/data/README.md.