Examples: TLS, replacement, shared quotas, gRPC, cache, bodies, external call
ExampleOperationsSince version 1.0.0
On this page
Operations guide: configure, deploy and observe the gateway. What the capability does, product side: product guide; calling it: developer guide.
mTLS toward an upstream
Outside a mesh only: inside one, the sidecar already does it. The files must exist at load - which is why this case has no example file validated in CI.
upstreams:
payments:
url: https://payments.partner.example:8443 # TLS requires https://
tls:
ca_file: /etc/flexway/tls/partner-ca.pem
cert_file: /etc/flexway/tls/client.crt # reloaded when it rotates, no restart
key_file: /etc/flexway/tls/client.key
server_name: payments.partner.exampleThe gateway's TLS listener
No edge terminating TLS: the gateway verifies the client certificate itself, on
FLEXWAY_TLS_ADDR (:8443 by default). Clients are declared as in 12.15; the files must exist
at load, hence no example file validated in CI.
server:
tls:
cert_file: /etc/flexway/tls/tls.crt # reloaded when it rotates, no restart
key_file: /etc/flexway/tls/tls.key
client_ca_file: /etc/flexway/tls/clients-ca.pem # the client certificates' authority, reloaded too
auth:
mtls:
clients:
- { id: bank-settler, uri: "spiffe://bank.example/settler", tenant: bank, operations: [settle] }Replacing a gateway in place
Coming from an existing gateway: importing its configuration writes the starting point - upstreams, issuers, rates, breakers, contract extensions - and the report of what does not carry over (Take over an existing gateway). What follows is what is added on day one.
# What a team replacing its gateway usually needs on day one (1.0.0): anonymous
# users behind a BFF counted apart, a bucket per operation as most gateways count, errors typed under
# its own portal with field codes in its own words, the body left to the service, tokens of an old
# and a new audience accepted while the audience is renamed.
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://shop.internal:8080
hosts:
api.example.com: { default_upstream: default, validation: params } # the services validate their bodies
server:
trusted_proxies: [local] # the mesh proxy: the pod's own addresses (not the loopback)
peer_identity_header: l5d-client-id # the mTLS identity that proxy verified
errors:
type_base: https://docs.example.com/errors/ # type = https://docs.example.com/errors/<code>
field_codes: { minLength: too_short, maxLength: too_long, minimum: out_of_range, maximum: out_of_range }
auth:
issuers:
- name: customers
discovery: https://login.example.com/.well-known/openid-configuration
audiences: [shop-api, api] # the old name and the new, during the rename
policies:
definitions:
per-user:
type: rate-limit
config:
key: header # anonymous: X-Client-Key, if the BFF set it
header: X-Client-Key
trusted: [web-bff.web.serviceaccount.identity.linkerd.cluster.local]
scope: operation # a bucket per operation and user, not one per class
classes:
read: { rps: 20, burst: 40 }
write: { rps: 5, burst: 10 }
chains:
global: [per-user]# excerpt of examples/replace-gateway/openapi.yaml
/orders:
post:
operationId: createOrder
x-flexway-validation: full # this one is checked here too, body includedShared rate, plans, shedding
# Several replicas sharing one rate, a plan per tenant, and load shed when the gateway saturates
# (1.0.0).
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://reports.internal:8080
server:
counters: { store: "${COUNTERS_URL}", sync: 1s } # Redis, Dragonfly or Valkey, shared by the replicas
shedding: { target: 20ms } # p99 scheduling latency above which load is shed
auth:
issuers:
- name: customers
discovery: https://login.example.com/.well-known/openid-configuration
audience: api
tenant_claim: org # what a quota counts by
policies:
definitions:
per-caller:
type: rate-limit
config:
classes:
read: { rps: 50, burst: 100 }
write: { rps: 10, burst: 20 }
plans:
type: quota
config: { limits: { day: 100000, month: 2000000 } }
chains:
global: [per-caller, plans]# excerpt of examples/shared-quotas/openapi.yaml
/reports:
post:
operationId: buildReport
x-flexway-priority: sheddable # shed first when the gateway saturates
/payments/{id}:
get:
operationId: getPayment
x-flexway-priority: critical # never shedgRPC upstream in gRPC, gRPC-Web and REST
# A gRPC upstream served three ways from its own .proto (1.1.0): to gRPC clients (h2c on the
# traffic listener), to browsers in gRPC-Web, and in REST/JSON at the routes its google.api.http
# annotations declare. One operation per method - shop.v1.Orders/GetOrder - whatever the protocol:
# the same admission, rate limits, quotas and audit.
#
# orders.binpb is the descriptor set built from orders.proto (`buf build -o orders.binpb`): Flexway
# reads it, never the .proto, and needs no generated code. Rebuild it when the .proto changes.
version: 1
contracts:
protobuf: [./orders.binpb]
upstreams:
# Named by the methods' option (upstream: "orders"), as x-flexway-upstream names one.
orders:
# Inside a mesh the sidecar does mTLS, so the gateway speaks HTTP/2 in clear (h2c).
url: http://orders.internal:9000
protocol: grpc
auth:
issuers:
- { name: staff, discovery: "https://id.example.com/realms/staff/.well-known/openid-configuration", audience: orders-api }// excerpt of examples/grpc-upstream/orders.proto
service Orders {
rpc GetOrder(GetOrderRequest) returns (Order) {
option (google.api.http) = { get: "/v1/shop/orders/{id}" }; // the REST route
option (flexway.v1.operation) = { // what x-flexway-* says in OpenAPI
upstream: "orders"
security: [ { scopes: ["orders:read"] } ]
timeout: "2s"
};
}
rpc CreateOrder(CreateOrderRequest) returns (Order) {
option (google.api.http) = { post: "/v1/shop/orders" body: "order" };
option (flexway.v1.operation) = { upstream: "orders" security: [ { anonymous: true } ] };
}
rpc WatchOrder(GetOrderRequest) returns (stream Order) { // an event stream in REST
option (google.api.http) = { get: "/v1/shop/orders/{id}/watch" };
option (flexway.v1.operation) = { upstream: "orders" security: [ { anonymous: true } ] };
}
}Response cache
# Responses kept by the gateway (1.2.0): opt-in, per operation in the contract
# (x-flexway-cache) or by a cache policy over routes - the operation's own wins. Public by default: an
# identified caller is never served from, nor stored into, a public entry; per tenant and subject only
# when the operation declares it. The upstream's Cache-Control only shortens the ttl; no-store,
# private, Set-Cookie and streams are never kept.
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://catalog.internal:8080
server:
cache:
max_bytes: 64MiB # per replica; past it, the least recently used goes
max_entry_bytes: 512KiB # a larger response is relayed, never kept
auth:
issuers:
- name: customers
discovery: https://login.example.com/.well-known/openid-configuration
audience: api
tenant_claim: org # a cache per subject keys by tenant and subject
policies:
definitions:
catalog-cache:
type: cache
config: { ttl: 5m } # public, always: a policy never caches per subject
chains:
routes:
"/products/**": [catalog-cache] # the reads under it; the writes are left alone# excerpt of examples/response-cache/openapi.yaml
/products:
get:
operationId: listProducts
x-flexway-cache: { ttl: 60s } # its own, over the route's policy
/me/cart:
get:
operationId: getCart
security: [ { customers: [] } ]
x-flexway-cache: { ttl: 30s, scope: subject } # each subject of each tenant, its own entryResponse body and status
# Response bodies shaped by the gateway (1.2.0): a closed list of fields served, personal
# data masked, a legacy upstream's statuses turned into semantic ones in RFC 9457. Only the operations
# that declare one of these policies are read whole - every other keeps the direct relay. A stream, a
# response that is not JSON, or a projection that drops a field the contract requires refuses the
# start.
version: 1
contracts:
openapi: ./openapi.yaml
upstreams:
default:
url: http://orders.internal:8080
server:
body:
max_bytes: 2MiB # past it: 502 response_too_large, never a cut body
validate_sample: 0.05 # one served body in twenty checked against its schema
policies:
definitions:
public-order:
type: transform-body
config: { project: [id, status, total, items, customer], on_missing: fail }
no-pii:
type: redact
config: { fields: [body.customer.email, body.customer.phone], with: "***" }
legacy-errors:
type: map-status
config:
- { from: 200, when: "has(body.error)", to: 422, code: legacy_rejected }
- { from: 520, to: 502 } # a CDN's private status: the upstream failing
chains:
routes:
"/orders/**": [legacy-errors] # every order read; /health keeps the direct relay# excerpt of examples/body-transforms/openapi.yaml
/orders/{id}:
get:
operationId: getOrder
x-flexway-policies: [public-order, no-pii] # what is not listed is not served; the PII masked
/health:
get:
operationId: health # no body policy: relayed without a bufferExternal call
# A decision that belongs to the client's own system (1.2.0): a risk service asked
# before an order reaches its upstream, a loyalty service asked once a profile is answered. The
# service is an upstream declared like any other - its pool, bulkhead and breaker are its own -,
# never a URL in a policy. The timeout and what a failed call does are declared, never defaulted;
# the call consumes the operation's budget.
version: 1
contracts:
openapi: ./openapi.yaml
auth:
issuers:
- name: customers
discovery: https://id.example.com/.well-known/openid-configuration
audience: api
tenant_claim: org
tenant_header: X-Tenant # sent to the risk service when named, never set by it
upstreams:
default:
url: http://orders.internal:8080
risk:
url: http://risk.internal:8080 # the external service: an upstream like any other
max_in_flight: 200 # its bulkhead
circuit_breaker: { min_requests: 20, failure_ratio: 0.5, cooldown: 10s }
loyalty:
url: http://loyalty.internal:8080
policies:
definitions:
risk-check:
type: external-call
phase: request
when: "request.method == 'POST'"
config:
upstream: risk
path: /score
timeout: 150ms # required, within the operation's budget
on_failure: fail-closed # an authorization: no answer, no order
send: { headers: [X-Tenant], body: true }
deny: { when: "response.status == 403", status: 403, code: risk_refused }
set_headers: { X-Risk-Score: "response.body.score" }
loyalty-tier:
type: external-call
phase: response
config:
upstream: loyalty
path: /tier
timeout: 80ms
on_failure: fail-open # an enrichment: no answer, no header
send: { headers: [X-Tenant] }
set_headers: { X-Loyalty-Tier: "response.body.tier" }# excerpt of examples/external-call/openapi.yaml
/orders:
post:
operationId: createOrder
x-flexway-timeout: 2s # the budget the risk call consumes
x-flexway-policies: [risk-check]
/health:
get:
operationId: health # no external call: nothing asked of anyoneNot built yet
Written here so the target shape is known. Until its version, a new flexway.yaml key is
refused at load, an upcoming policy type is accepted with a warning and never applied, and an
upcoming contract extension is refused at load with the version that brings it - the detail,
checked with flexway validate, and one complete, commented example per use case are in
Target configurations for upcoming versions. The syntax is the planned one, not a promise
of detail.
| Case | Target shape | Version |
|---|---|---|
| Devices and industry: an MQTT listener for devices (X.509, a token or a key as the password), a topic bound to the tenant and the certificate, Sparkplug B, OPC UA PubSub JSON | listeners.mqtt, x-flexway-device, x-flexway-sparkplug, x-flexway-bind: "cert:<field>" |
1.7.0 |
| Plans and budgets: limits per plan (verified claim or key attribute, default plan), inference budgets per user and per use case | plans, issuers[].plan_claim, ai.budget.per_subject, ai.use_cases, ai.aliases, x-flexway-use-case |
1.8.0 |
| Signed incoming webhooks, IP lists: generic HMAC, Standard Webhooks, provider presets; allow and deny lists, PROXY protocol | x-flexway-webhook, webhooks.secrets, server.ip_deny, server.proxy_protocol, ip-filter policy |
1.8.0 |
| Negotiated compression, coalescing of in-flight reads | compress policy (gzip, brotli, zstd), x-flexway-coalesce |
1.8.0 |
| Upstream A2A cards verified, published card re-signed | agents.a2a.signers, agents.a2a.signing, agents.a2a.cards, x-flexway-a2a.signer |
1.9.0 |
| Agent-to-agent delegation, registry | Bounds tightened per agent over two A2A hops, verdicts in /config |
1.9.0 |
Licence: without a valid licence, Flexway refuses to start; free Developer key outside production (90 days, Flexway-License: developer header, 100 requests per second per replica), Business and Enterprise with 30 days of grace; state in the log, the flexway.license.state metric, /config and the console; a running process is never stopped |
Licence file mounted as a secret, its path in flexway.yaml (key name still to be set); flexway validate stays usable without a licence |
At public opening, to be dated |