Standort is a Go service that provides location-based information (country + continent) from:
It exposes two API versions (v1 and v2) over gRPC and HTTP. It is useful when an application needs a small service boundary for country/continent enrichment without calling an external provider at request time.
[!NOTE] Standort ships its lookup data as embedded assets, so runtime responses are determined by the GeoIP2 database and GeoJSON files committed under
assets/.
Standort returns:
country: ISO-3166 alpha-2 code (e.g. US, DE)continent: two-letter continent code (e.g. NA, EU)Lookups are performed using embedded assets:
assets/geoip2.mmdb: IP → country code (GeoIP2)assets/earth.geojson: lat/lng → country + continent name (GeoJSON polygons), indexed with an R-tree[!CAUTION] Freshness is only one accuracy limit. Standort provides coarse country/continent enrichment:
- GeoLite Country results are estimates and must not be used to identify a specific household, individual, or street address.
- Natural Earth 1:110m polygons are generalized for schematic world maps and are not suitable for precise coastline, border, or disputed-boundary decisions.
Update the assets and rebuild the service when lookup freshness matters. See
assets/SOURCES.mdfor dataset provenance and terms.
The repository’s MIT license covers repository-owned code. The embedded datasets retain their source terms and attribution requirements, recorded in assets/SOURCES.md.
When replacing embedded lookup data, keep the filenames and formats unchanged:
geoip2.mmdb must remain a GeoIP2 country database compatible with the github.com/IncSW/geoip2 reader.earth.geojson must remain a GeoJSON feature collection whose indexed features have Polygon or MultiPolygon geometry, a two-character iso_a2 or iso_a2_eh country code, and a supported continent value.Record the dataset source/provenance in the asset update change, then rebuild and run the feature harness so changed lookup results are visible in review.
To refresh both committed lookup assets:
MAXMIND_ACCOUNT_ID=... MAXMIND_LICENSE_KEY=... make update-lookup-assets
The refresh requires curl, jq, shasum, and tar in addition to common
POSIX tools. MAXMIND_URL and EARTH_GEOJSON_URL may point to alternate
locations of the same GeoLite2 Country and Natural Earth Admin-0 1:110m
datasets; the generated provenance and terms assume those dataset identities.
The size guards can be tuned with
LOOKUP_ASSET_MIN_SIZE_PERCENT (default 80, range 1–100) and
LOOKUP_ASSET_MAX_SIZE_PERCENT (default 400, minimum 100).
The command checks the downloaded files before replacing assets/geoip2.mmdb
and assets/earth.geojson: each replacement must be non-empty, match the
expected dataset shape, and stay within size bounds relative to the current
committed asset. It also writes the resolved source, checksum, and size metadata
to assets/SOURCES.md. The Natural Earth GeoJSON is minified, filtered to
features the service can index, and trimmed to the lookup properties expected by
the service. If the downloaded assets match the current committed files, the
command exits without rewriting the provenance timestamp.
v1 has separate RPCs for IP-based lookup and lat/lng-based lookup.
GetLocationByIPGetLocationByLatLngBoth methods map invalid or unresolved lookups to gRPC NotFound; the HTTP
mapping returns 404.
v2 combines both inputs into a single lookup RPC and also supports batch lookups:
GetLocationLookupLocationsGetLookupAssetsv2 supports passing inputs either directly in the request or via request metadata:
X-Forwarded-For).Geolocation header containing a geo: URI (RFC 5870).If a lookup fails, v2 keeps trying any other available input, and only returns “not found” when neither IP nor GEO yields a location.
v2 response fields are independent:
ip is populated when the IP lookup succeeds.geo is populated when the point lookup succeeds.Terminal lookup failures return gRPC NotFound; the HTTP RPC router exposes the same lookup miss as HTTP 404. Code-only diagnostics are delivered in gRPC trailing metadata or HTTP response headers for terminal GetLocation failures, and in google.rpc.ErrorInfo.metadata for failed batch entries.
| Diagnostic key | Meaning | Current codes |
|---|---|---|
location-ip-error |
IP parsing or lookup failed | invalid_ip, not_found |
location-lat-lng-error |
Point validation or lookup failed | invalid_point, not_found |
location-point-error |
Geolocation metadata could not be parsed |
invalid_geo_uri |
LookupLocations accepts up to 100 lookup entries and preserves request order
in the response. Each entry returns one outcome: successful entries populate
locations.ip, locations.geo, or both, while entries that do not resolve any
location populate a per-entry google.rpc.Status with code NOT_FOUND (5)
instead of failing the whole batch. Requests with more than 100 lookup entries
fail the whole call with gRPC InvalidArgument; the HTTP RPC mapping returns
400.
For a classified batch failure, status.details contains one
google.rpc.ErrorInfo with reason LOCATION_LOOKUP_FAILED and domain
standort.v2. Its metadata preserves the safe, code-only diagnostic keys used
by terminal lookup failures (such as location-ip-error: not_found). Clients
must ignore unknown diagnostic keys and codes; no raw request input or provider
error is included.
For the HTTP mapping of LookupLocations, set the Content-Type header to
application/pbjson to receive protobuf JSON where each lookup entry has either
locations or status.
GetLookupAssets returns read-only metadata for the embedded lookup assets,
including asset name, size in bytes, checksum algorithm, and checksum. It is
informational only; it does not enforce asset freshness.
[!WARNING] Treat forwarded IP metadata as trusted only after your proxy or gateway has normalized it. Standort reads the metadata supplied by the transport layer; it does not decide whether a forwarded client IP is trustworthy.
go.mod)bin/ submodule)test/)Optional tools used by specific targets include air, buf, gotestsum, govulncheck, trivy, and hadolint.
Initialize submodules and vendor dependencies:
git submodule sync
git submodule update --init
make dep
[!IMPORTANT] Most workflow commands include make fragments from the
bin/submodule, and many Go targets run with-mod vendor. Ifbin/is missing or Go reports “inconsistent vendoring,” run the bootstrap commands above again. Thebinsubmodule intentionally uses an SSH URL (git@github.com:alexfalkowski/bin.git), so fresh environments need GitHub SSH credentials beforegit submodule update --initcan succeed.
Build the server binary:
make build
The repository includes a dev config at test/.config/server.yaml with default addresses:
:11000:12000air)make dev
This runs air to rebuild and restart using the dev config.
If you’ve built the binary (via make build), run:
./standort server -config file:test/.config/server.yaml
standort server reads a go-service configuration file from -config (or -c). The dev sample at test/.config/server.yaml is the authoritative local example.
Standort-owned configuration currently adds the required health section:
health:
duration: 1s
timeout: 1s
duration is the interval between health check executions, while timeout is
the maximum time allowed for one check. Both values must be positive durations.
The rest of the file is the embedded go-service configuration, inlined at the
top level, including transport addresses/timeouts, limiter settings, logger,
metrics, tracer, and ID settings.
When running with the dev config, the health HTTP observer endpoints are:
GET http://localhost:11000/standort/healthzGET http://localhost:11000/standort/livezGET http://localhost:11000/standort/readyzGET http://localhost:11000/standort/metrics[!NOTE] go-service prefixes operational HTTP routes with the service name. The dev and test harness service name is
standort.
The healthz endpoint uses go-health’s online checker. The livez and readyz endpoints are local noop observers. The metrics endpoint is available with the dev config because it sets telemetry.metrics.kind: prometheus.
Use livez or readyz for local process probes that must not depend on public egress. The healthz endpoint is an online check and is expected to report unhealthy when the process cannot reach the public internet.
The gRPC health service registers these service names:
standort.v1.Servicestandort.v2.ServiceThe service is gRPC-first. Its HTTP transport uses go-service RPC routing to map generated gRPC full method names to transport-specific HTTP handlers; the HTTP and gRPC handlers share the same response-building locators.
[!TIP] Use
-plaintextwithgrpcurlfor the local dev config unless you have configured TLS.
You can use grpcurl against the dev gRPC address (localhost:12000).
grpcurl -plaintext \
-d '{"ip":"8.8.8.8"}' \
localhost:12000 \
standort.v1.Service/GetLocationByIP
grpcurl -plaintext \
-d '{"lat":52.5200,"lng":13.4050}' \
localhost:12000 \
standort.v1.Service/GetLocationByLatLng
grpcurl -plaintext \
-d '{"ip":"8.8.8.8"}' \
localhost:12000 \
standort.v2.Service/GetLocation
grpcurl -plaintext \
-d '{"point":{"lat":52.5200,"lng":13.4050}}' \
localhost:12000 \
standort.v2.Service/GetLocation
v2 can fall back to metadata when request fields are omitted:
X-Forwarded-For).Geolocation header using a geo: URI.Example with a geo URI:
grpcurl -plaintext \
-H 'Geolocation: geo:52.5200,13.4050' \
-d '{}' \
localhost:12000 \
standort.v2.Service/GetLocation
Example with a forwarded IP:
grpcurl -plaintext \
-H 'X-Forwarded-For: 8.8.8.8' \
-d '{}' \
localhost:12000 \
standort.v2.Service/GetLocation
grpcurl -plaintext \
-d '{"lookups":[{"ip":"8.8.8.8"},{"point":{"lat":52.5200,"lng":13.4050}},{"ip":"192.0.2.1"}]}' \
localhost:12000 \
standort.v2.Service/LookupLocations
grpcurl -plaintext \
-d '{}' \
localhost:12000 \
standort.v2.Service/GetLookupAssets
HTTP runs on localhost:11000 with the dev config. The RPC router exposes POST routes using the generated gRPC full method names.
curl -sS \
-X POST \
-H 'Content-Type: application/json' \
-d '{"ip":"8.8.8.8"}' \
http://localhost:11000/standort.v1.Service/GetLocationByIP
curl -sS \
-X POST \
-H 'Content-Type: application/json' \
-d '{"lat":52.5200,"lng":13.4050}' \
http://localhost:11000/standort.v1.Service/GetLocationByLatLng
curl -sS \
-X POST \
-H 'Content-Type: application/json' \
-d '{"ip":"8.8.8.8"}' \
http://localhost:11000/standort.v2.Service/GetLocation
curl -sS \
-X POST \
-H 'Content-Type: application/json' \
-d '{"point":{"lat":52.5200,"lng":13.4050}}' \
http://localhost:11000/standort.v2.Service/GetLocation
curl -sS \
-X POST \
-H 'Content-Type: application/json' \
-H 'Geolocation: geo:52.5200,13.4050' \
-d '{}' \
http://localhost:11000/standort.v2.Service/GetLocation
Example with a forwarded IP:
curl -sS \
-X POST \
-H 'Content-Type: application/json' \
-H 'X-Forwarded-For: 8.8.8.8' \
-d '{}' \
http://localhost:11000/standort.v2.Service/GetLocation
curl -sS \
-X POST \
-H 'Content-Type: application/pbjson' \
-d '{"lookups":[{"ip":"8.8.8.8"},{"point":{"lat":52.5200,"lng":13.4050}},{"ip":"192.0.2.1"}]}' \
http://localhost:11000/standort.v2.Service/LookupLocations
curl -sS \
-X POST \
-H 'Content-Type: application/json' \
-d '{}' \
http://localhost:11000/standort.v2.Service/GetLookupAssets
make format
make lint
To apply automatic fixes where available:
make fix-lint
make specs
make specs uses gotestsum directly. Install it if you don’t already have it.
make features
The Ruby harness is configured by test/nonnative.yml and writes logs, coverage, and report artifacts under test/reports/.
The harness starts and stops ../standort server itself with -config file:.config/server.yaml relative to test/, so leave ports 11000 and 12000 free before running it. To rerun a focused slice, pass paths relative to test/:
make features feature=features/v2/transport/grpc/api.feature tags=@grpc
make benchmarks
Benchmark runs use the same harness configuration and report directory as feature tests.
Focused benchmark runs also use paths relative to test/:
make benchmarks feature=features/v2/transport/http/benchmark.feature
make sec
make trivy-repo
make sec is the full local security wrapper: it runs govulncheck -show verbose -test ./... and the Trivy repository scan. Use make trivy-repo only when you want the narrower Trivy-only scan.
CircleCI initializes submodules, vendors dependencies, then runs linting, protobuf breaking and generated-output stale checks, security checks, feature tests, benchmarks, analysis, coverage, and Codecov upload. To mirror the main validation sequence locally, use:
make dep
make lint
make proto-breaking
make proto-stale
make sec
make features
make benchmarks
make analyse
make coverage
Use make specs as a focused Go test check; it is not a separate CircleCI
step in the main build job.
Protos live under api/standort/v1 and api/standort/v2.
To lint and generate code:
make proto-lint
make proto-format
make proto-generate
buf generate writes Go protobuf/gRPC files into this repository and Ruby protobuf/gRPC files under test/lib. It uses Buf remote plugins, so make proto-generate and make proto-stale may need network access on first run or when the plugin cache is empty. When a .proto file is renamed or deleted, remove any orphaned generated Go and Ruby outputs in the same change.
Breaking-change check:
make proto-breaking
make proto-breaking compares api/ with the master branch on GitHub and requires network access to that remote baseline.
Generated-output freshness check:
make proto-stale
[!CAUTION] Do not hand-edit generated protobuf or gRPC files. Change the
.protofiles and runmake proto-generate.
main.go: CLI entrypoint; registers the server command.internal/cmd: DI composition and command wiring.internal/config: service config composition.internal/health: health registrations/observers.internal/location: domain lookup logic (IP + point-in-polygon).internal/api/v1, internal/api/v2: API modules and transports (gRPC + HTTP wiring).assets: embedded runtime datasets (GeoJSON + GeoIP DB).api: protobuf definitions + buf config.test: Ruby feature/benchmark harness + example config.vendor: vendored Go dependencies.Use the GitHub issue tracker for bug reports, documentation fixes, and feature requests. Maintainers should keep README command examples aligned with Makefile, .circleci/config.yml, and the generated API definitions under api/.