k3s + Traefik
Two namespaces (dev and prod), default-deny network policies, and a single ingress layer.
Architecture
STAC defines the storage model. OGC API — Features is the serving surface. One gateway and one permission service decide every request. No service holds a second copy of the rules.
System shape
Clients reach the gateway. The gateway authenticates the caller, asks the permission service for a decision, enforces the request quota in Redis, strips any client-supplied tenant header, and injects the trusted context upstream. featureflow serves features from PostGIS; admin-api runs onboarding and loads. Neither decides access.
The gateway is the only place where identity and tenant context are established.
PostGIS holds tenants, collections, items, licenses, API-key hashes, and the usage outbox.
QGIS, a CLI, or a custom app use the same open HTTP surface.
Request path
Public catalogue listings are the only anonymous branch. They return metadata and never serve feature data.
Services
The mixed legacy binary is gone. Each service can be deployed and scaled independently.
| Service | Role | Owns |
|---|---|---|
gateway | Policy enforcement point (PEP) | Authentication, PDP call, quota enforcement, context stripping/injection, routing. |
permission-service | Policy decision point (PDP) | Ownership, visibility, licenses, terms, capabilities, quotas, suspension, discovery scope. |
featureflow | Serving | OGC API — Features Core + Part 3/CQL2, bbox/datetime, keyset pagination. No authorization state. |
admin-api | Administration + loader | Onboarding, uploads, load jobs, publishing, licenses, API keys, usage reads. |
frontend | Vue SPA | Admin UI, public portal, tenant discovery views, sign-in and registration. |
Decision flow
The sequence is deliberately boring: authenticate, decide, enforce, inject, serve, audit.
The gateway accepts an OIDC user token, a tenant API key, or an OAuth2 client-credentials token. Public listing paths are the only exception.
The gateway calls the permission service. The PDP resolves the first matching rule: owner → host → public-open → licensed → trial → deny.
If the license carries a request quota, the gateway increments an atomic Redis counter for the fixed UTC window. Redis outage is fail-open with a warning.
The gateway strips client-supplied tenant context and injects the authenticated tenant, roles, granted subset, and restriction terms.
featureflow applies the injected context as data filters and returns whole features from PostGIS. It never validates or decides access.
The gateway appends one usage/audit event per reached decision to the append-only log. Denials and quota outcomes are recorded too.
Trust boundary
featureflow has no rules engine, no tenant lookup, and no client context. It receives a decision and applies it as filters. That keeps the authorization path auditable and prevents a second, divergent implementation of access control.
Deployment
The reference deployment is a k3s cluster with Traefik, Cloudflare Tunnel, an external PostGIS VM, Redis, and Keycloak. Configuration is environment variables only.
Two namespaces (dev and prod), default-deny network policies, and a single ingress layer.
External VM, migrations embedded in the images, forward-only and applied once.
Public hostnames terminate TLS at the edge; Traefik receives plain HTTP inside the cluster.
One IdP for users and machine clients, with separate prod and dev realms.
gateway, featureflow, admin-api, permission-service, and the Vue frontend.
Argo CD manifests exist; the deploy repository remains owner-only and is applied explicitly.