# Examples: TLS, replacement, shared quotas, gRPC, cache, bodies, external call

> 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.

Example - Since version 1.0.0 - Flexway v1.6.3 - https://flexway.io/en/docs/operations-examples-gateway/

> Operations guide: configure, deploy and observe the gateway. What the capability does, product side: [product guide](https://flexway.io/en/docs/product-overview/); calling it: [developer guide](https://flexway.io/en/docs/developer-overview/).

## 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.

```yaml
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.example
```

## The 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.

```yaml
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](https://flexway.io/en/docs/operations-import/)). What follows is what is added on day one.

<!-- example: examples/replace-gateway/flexway.yaml -->

```yaml
# 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]
```

```yaml
# excerpt of examples/replace-gateway/openapi.yaml
  /orders:
    post:
      operationId: createOrder
      x-flexway-validation: full                 # this one is checked here too, body included
```

## Shared rate, plans, shedding

<!-- example: examples/shared-quotas/flexway.yaml -->

```yaml
# 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]
```

```yaml
# 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 shed
```

## gRPC upstream in gRPC, gRPC-Web and REST

<!-- example: examples/grpc-upstream/flexway.yaml -->

```yaml
# 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 }
```

```proto
// 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

<!-- example: examples/response-cache/flexway.yaml -->

```yaml
# 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
```

```yaml
# 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 entry
```

## Response body and status

<!-- example: examples/body-transforms/flexway.yaml -->

```yaml
# 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
```

```yaml
# 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 buffer
```

## External call

<!-- example: examples/external-call/flexway.yaml -->

```yaml
# 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" }
```

```yaml
# 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 anyone
```

## Not 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](https://flexway.io/en/docs/operations-planned-configurations/). 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 |
