OpenResty based API gateway that aggregates multiple upstream OpenAPI 3.x
services under a single endpoint. Each service is exposed under a /<name>
prefix; a merged /openapi.json schema covers all of them.
Proxying runs through native nginx proxy_pass — keepalive pools, TLS, body
streaming, and retries at C speed with no Lua on the hot path.
Built on OpenResty + Alpine: the Docker image is ~62 MB and idle memory sits in the low single-digit MB range.
- Proxy
- Multi-upstream load balancing
- Keepalive pools
- WebSocket
- Server TLS/mTLS
- Upstream mTLS
- Policy
- Per-service JWT auth (HS*/RS*/ES*, JWKS)
- Rate limiting
- Adaptive concurrency limiting
- CORS
- Header injection/stripping
- Fine-grained route filtering (path, method, tag — with wildcard and negation patterns)
- Observability
- Prometheus metrics
- JSON access log
- OpenTelemetry OTLP/HTTP
- W3C trace propagation
docker pull ghcr.io/mpenet/uplink:latest
docker run -p 8080:8080 -v ./config.json:/uplink/config.json ghcr.io/mpenet/uplink:latestMinimal config.json:
{
"services": [
{
"name": "petstore",
"upstream": "https://petstore3.swagger.io",
"schema_url": "https://petstore3.swagger.io/api/v3/openapi.json",
"ttl": 300,
"rules": [
{
"paths": ["/api/v3/*", "!/api/v3/admin/*"],
"methods": ["GET", "POST", "PUT", "DELETE"],
"tags": ["pet", "store"]
}
]
},
{
"name": "apisguru",
"upstream": "https://api.apis.guru",
"schema_url": "https://api.apis.guru/v2/openapi.yaml",
"ttl": 300,
"rules": [
{
"paths": ["!/v2/private/*"],
"methods": ["GET"]
}
]
}
]
}GET /petstore/api/v3/pet/1 → strips /petstore → proxied to https://petstore3.swagger.io/api/v3/pet/1.
GET /openapi.json → merged schema for all services.
rules is an array — each element is a rule object whose fields are AND'd; rules are OR'd. Rule patterns support * as a trailing wildcard and ! for negation:
"rules": [{"paths": ["!/internal/*", "!/admin/*"]}]Everything except /internal/* and /admin/* — multiple negations are ORed.
"rules": [{"paths": ["/api/*", "!/api/internal/*", "!/api/debug/*"]}]Only /api/*, but not /api/internal/* or /api/debug/*.
"rules": [
{"paths": ["/v1/*", "!/v1/admin/*"], "methods": ["GET", "POST"], "tags": ["public"]},
{"methods": ["GET"], "tags": ["internal"]}
]POST on public /v1/* paths OR any GET tagged internal. Within each rule fields are AND'd; rules are OR'd. See Routing for full semantics.
See config.json.sample for a full annotated example.
| Page | Description |
|---|---|
| How it works | Request lifecycle, routing, schema aggregation, trace propagation |
| Configuration | Config structure and service field reference |
| Routing | Rules, load balancing, WebSocket |
| Authentication | JWT — HMAC, PEM, JWKS |
| TLS | Server TLS/mTLS, upstream mTLS |
| Traffic control | Rate limiting, adaptive concurrency, keepalive pools |
| Headers & CORS | Header injection/stripping, CORS |
| Observability | Prometheus metrics, JSON access log, OpenTelemetry |
| Deployment | Docker, Kubernetes, shared dict sizing, troubleshooting |
Copyright © 2026 Max Penet
Distributed under the Mozilla Public License 2.0