# Target configurations for upcoming versions

> This page shows, for each planned use case, the target shape of a complete flexway.yaml and the contract excerpt that goes with it, and the version meant to…

Guide - Since version 1.6.0 - Flexway v1.6.3 - https://flexway.io/en/docs/operations-planned-configurations/

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

This page shows, for each **planned** use case, the target shape of a complete `flexway.yaml` and the
contract excerpt that goes with it, and the version meant to deliver it: `1.7.0` (devices and industry)
and `1.8.0` (enterprise needs). **Nothing below is built**: a detail may still move before the version
ships, and this page will follow. Where the shape of a key is not fixed yet, the example says so rather
than invent it. Each example will become a file of `examples/`, validated like the others, with its
version.

> **Already delivered in `1.6.0`**, and so described elsewhere: the native Kafka proxy
> ([Native Kafka clients: a virtual cluster per tenant](https://flexway.io/en/docs/operations-kafka-clients/)), field encryption
> ([Field encryption](https://flexway.io/en/docs/operations-kafka-encryption/)), Protobuf, Avro and CBOR payloads under schema
> and the schema registry
> ([Avro payloads under schema, schema registry](https://flexway.io/en/docs/operations-examples-brokers/#avro-payloads-under-schema-schema-registry)),
> NATS and AMQP 1.0 ([Examples: Kafka, RabbitMQ, MQTT, Avro, NATS, AMQP 1.0](https://flexway.io/en/docs/operations-examples-brokers/#nats-jetstream-publish-from-http-or-an-mcp-tool)).

**What today's gateway does** with these shapes (checked with `flexway validate`):

| The example declares | Today |
| --- | --- |
| A new `flexway.yaml` key: `listeners.mqtt`, `plans`, `webhooks`, `server.ip_deny`, `server.proxy_protocol`, `ai.budget.per_subject`, `ai.use_cases`, `ai.aliases`, `plan_claim`, `use_case_claim`, `trusted_clients`, `auth.mtls.clients[].plan` | **Refused at load** (`field ... not found`): the whole configuration is refused |
| An `x-flexway-bind: "cert:<field>"` binding, a webhook scheme read from a header other than `Authorization` | **Refused at load** |
| An upcoming policy type: `compress`, `ip-filter` | **Accepted, never applied**: `flexway validate` and the start warn about it (`a later version - NOT applied`) |
| An upcoming contract extension: `x-flexway-device`, `x-flexway-sparkplug`, `x-flexway-use-case`, `x-flexway-webhook`, `x-flexway-coalesce` | **Refused at load** (`not served yet - planned for <version>`): the whole contract is refused, like any unknown `x-flexway-*` extension (`unknown extension`, with the closest one when it is obvious) or one written where it is not read (`not read here`) - [Not built yet](https://flexway.io/en/docs/operations-examples-gateway/#not-built-yet) |

## MQTT devices: X.509 or token, topic bound to the tenant and the certificate

> [!NOTE]
> Planned in 1.7

> **Target of version 1.7.0 - not built yet: refused at load today** (`listeners.mqtt`,
> `x-flexway-bind: "cert:cn"`, `x-flexway-device`).

The gateway is an MQTT proxy in front of the client's broker: one upstream connection per device,
each packet judged by the same decision as an HTTP request. Sessions, retained messages, wills and
QoS 2 stay with the broker.

```yaml
# flexway.yaml - the listener; no topic here, they come from the contract
version: 1
contracts:
  asyncapi: api/asyncapi.yaml
listeners:
  mqtt:
    address: ":8883"
    tls: { cert_file: /tls/tls.crt, key_file: /tls/tls.key, client_ca_file: /tls/ca.crt }   # the devices' X.509
    websocket: { path: /mqtt }
    max_packet_bytes: 262144
    connect_timeout: 5s                # a slow CONNECT is closed
upstreams:
  plant-broker:
    protocol: mqtt
    brokers: ["mqtts://broker.plant.internal:8883"]
    protocol_version: 5
auth:
  issuers:                             # for a device without a certificate: a token as the password
    - name: devices
      discovery: https://id.example.com/.well-known/openid-configuration
      audience: plant-devices
      tenant_claim: site
```

```yaml
# api/asyncapi.yaml - the listeners' convention, the native Kafka proxy's (1.6.0):
# the extension on the operation, send = the device publishes, receive = it subscribes
channels:
  telemetry:
    address: "plant/{site}/line/{line}/{device}/telemetry"
    parameters:
      site:   { x-flexway-bind: tenant }      # the verified tenant, never the topic's
      device: { x-flexway-bind: "cert:cn" }   # the common name of the device's certificate
    messages:
      reading:
        contentType: application/x-protobuf
        payload: { schemaFormat: "application/vnd.google.protobuf;version=3", schema: { $ref: "./reading.binpb#Reading" } }
operations:
  publishTelemetry:
    action: send                       # the device publishes: writes to the channel
    channel: { $ref: "#/channels/telemetry" }
    x-flexway-upstream: plant-broker
    x-flexway-device: { qos_max: 1, validate: full }
    security: [{ deviceCert: [] }]
```

| Packet | Decision |
| --- | --- |
| `CONNECT` | A verified certificate, else the password (a verified token or a key), else refused `0x87`; the client id is **rewritten, bound to the identity** - a device never evicts another; the will is judged as a `PUBLISH` |
| `PUBLISH` | Topic -> declared channel, bound parameters **equal** to the verified values (`site` = tenant, `device` = CN), scope, quota, schema when `validate: full`; refused with `PUBACK` `0x87`, `0x97` (quota), `0x99` (format) in MQTT 5, closed in 3.1.1 |
| `SUBSCRIBE` | Each filter, wildcards included, must fit inside a declared channel once the bound parameters are set; else `SUBACK 0x87` |
| Token `exp` | `DISCONNECT 0xA0` (MQTT 5); the device comes back with a fresh token |

## Sparkplug B

> [!NOTE]
> Planned in 1.7

> **Target of version 1.7.0 - not built yet: refused at load today** (`listeners.mqtt`,
> `x-flexway-bind: "cert:cn"`, `x-flexway-sparkplug`).

`flexway.yaml` is that of 12.29.6. A Sparkplug B 3.0 channel takes one word: the `spBv1.0/...`
namespace and the `sparkplug_b.proto` descriptor are built in.

```yaml
# api/asyncapi.yaml - excerpt
channels:
  sparkplug:
    address: "spBv1.0/{group}/{type}/{edge_node}"
    x-flexway-sparkplug: true
    parameters:
      group:     { x-flexway-bind: tenant }     # the Sparkplug group is the tenant
      edge_node: { x-flexway-bind: "cert:cn" }  # a node publishes under its own name only
```

| Element | Handling |
| --- | --- |
| Birth and death | `NBIRTH`/`DBIRTH` -> online, `NDEATH` (will)/`DDEATH` -> offline, per tenant; `bdSeq` checked; seen in the console |
| Sequences | `seq` 0-255 checked per node, a gap counted (`flexway.sparkplug.sequence.gaps`), never blocking |
| Commands | `NCMD`/`DCMD` published by an application (an HTTP `x-flexway-publish` gate or MQTT) under a declared write scope |

## OPC UA PubSub JSON over MQTT

> [!NOTE]
> Planned in 1.7

> **Target of version 1.7.0 - not built yet: refused at load today** (`listeners.mqtt`,
> `x-flexway-device`).

No extension of its own: an OPC UA PubSub publisher sending JSON over MQTT is a device of [MQTT devices](#mqtt-devices-x509-or-token-topic-bound-to-the-tenant-and-the-certificate),
under an **ordinary contract** - a channel, its JSON schema, its bindings. The `plant/{site}/` prefix
(configured in the publisher) binds the tenant; the rest follows the topic structure OPC UA
recommends (part 14).

```yaml
# api/asyncapi.yaml - excerpt
channels:
  opcua-data:
    address: "plant/{site}/opcua/json/data/{publisher}/{writer_group}"
    parameters:
      site:      { x-flexway-bind: tenant }
      publisher: { x-flexway-bind: "cert:cn" }   # the publisher sends under its own name
    messages:
      network-message:
        contentType: application/json
        payload: { $ref: "./opcua-network-message.json" }   # the JSON NetworkMessage, validated
operations:
  publishOpcuaData:
    action: send
    channel: { $ref: "#/channels/opcua-data" }
    x-flexway-upstream: plant-broker
    x-flexway-device: { qos_max: 1, validate: full }
    security: [{ deviceCert: [] }]
```

## Plans read from a claim, a default plan

> [!NOTE]
> Planned in 1.8

> **Target of version 1.8.0 - not built yet: refused at load today** (`plans`, `plan_claim`).

Selling an API by plans: a caller's plan comes from a **verified claim** of its token, or from the
`plan` attribute of its API key or its certificate; its limits are declared here. A caller without a
plan, or whose plan is not declared, gets the **default** plan - never "unlimited".

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://orders.internal:8080
server:
  counters: { store: "${COUNTERS_URL}" }   # required: refused at load without a shared store
auth:
  issuers:                             # the existing key is auth.issuers
    - name: corporate
      discovery: https://id.example.com/.well-known/openid-configuration
      audience: orders-api
      tenant_claim: org
      plan_claim: subscription.plan    # a dotted path to a claim of the verified token
plans:
  default: free                        # required as soon as plans: exists
  count_per: tenant                    # tenant (default) | subject
  definitions:
    free:
      requests: { day: 1000, month: 20000 }
      tokens:   { day: 50000, month: 1000000 }
      eur:      { month: 5 }
    pro:
      requests: { day: 100000, month: 2000000 }
      tokens:   { month: 50000000 }
      eur:      { month: 500 }
```

```yaml
# api/openapi.yaml - excerpt: nothing specific to plans, the contract only says who gets in
paths:
  /orders:
    get:
      operationId: listOrders
      security: [ { corporate: [orders:read] } ]
```

| Rule | Behaviour |
| --- | --- |
| Unknown plan | The `default` plan, a `plan_unknown` log line and the `flexway.plans.unknown` counter - the caller is authenticated, the gap comes from its provider |
| With the tenant's quotas ([Setting quotas](https://flexway.io/en/docs/operations-quotas/)) | Both apply, the first reached refuses: `429` `quota_exceeded`, member `limit: plan` |
| API key, certificate | The `plan` attribute of the store entry (written by `flexway api-key`) or of `auth.mtls.clients[]` |

## Inference budgets per user and per use case

> [!NOTE]
> Planned in 1.8

> **Target of version 1.8.0 - not built yet: refused at load today** (`ai.budget.per_subject`,
> `ai.use_cases`, `ai.aliases`, `use_case_claim`, `x-flexway-use-case`).

On top of the tenant's budget ([Serving inference](https://flexway.io/en/docs/operations-inference/)), a budget per **user** (the verified subject) and per **use
case**. The use case comes from the contract (`x-flexway-use-case`), from a **model alias** of
`/v1/*`, or from the claim of a trusted client - **never** from a caller's header. The most
restrictive budget wins.

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://tickets.internal:8080
auth:
  issuers:
    - name: corporate
      discovery: https://id.example.com/.well-known/openid-configuration
      audience: ai
      tenant_claim: org
      use_case_claim: ai_use_case      # believed only for the clients of trusted_clients
      trusted_clients: [batch-reviewer]
ai:
  security: [ { oidc: { scopes: [inference] } } ]
  default_backend: hosted
  backends:
    hosted:
      url: https://api.provider.example/v1
      api_key: "file:/etc/flexway/secrets/provider-key"
      default_model: gpt-4.1-mini
      external: true
      price: { input_per_mtok: 0.40, output_per_mtok: 1.60 }
  budget:
    store: "${COUNTERS_URL}"
    monthly_eur: 2000                  # tenant, unchanged
    per_subject: { monthly_eur: 20, daily_tokens: 200000 }   # each verified subject
  use_cases:
    support-bot: { monthly_eur: 300 }
    code-review: { monthly_eur: 800, daily_tokens: 5000000 }
  aliases:                             # a /v1/* model name that carries a use case
    support-assistant: { backend: hosted, model: gpt-4.1-mini, use_case: support-bot }
```

```yaml
# api/openapi.yaml - excerpt: an operation (or a metered gRPC method) carries its use case
paths:
  /tickets/{id}/summary:
    post:
      operationId: summarizeTicket
      x-flexway-use-case: support-bot  # a use case missing from ai.use_cases: refused at load
```

| Rule | Behaviour |
| --- | --- |
| A call without a subject | No user budget; the tenant's applies |
| Contract and claim | The contract wins: a trusted client does not move a marked operation to another budget |
| Refusal | `429` `budget_exceeded`, member `limit`: `tenant`, `plan`, `subject` or `use_case` (OpenAI envelope on `/v1/*`) |
| Soft cap | The tenant's only; a user or use case budget is a hard cap |
| Consumption per user | `GET /usage/subjects` (administration), subject hashed; never a metric dimension |

## Incoming Stripe webhook and secret rotation

> [!NOTE]
> Planned in 1.8

> **Target of version 1.8.0 - not built yet: refused at load today** (`webhooks`, `x-flexway-webhook`,
> a scheme read from `Stripe-Signature`).

Receiving a provider's webhooks **signed**: the signature is the caller's identity, an alternative of
`security` in the contract. A preset is enough for Stripe, GitHub, Slack, Shopify and SendGrid.

```yaml
# flexway.yaml - secrets by reference, never in clear
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://billing.internal:8080
webhooks:
  secrets:
    stripe-endpoint:
      current: file:/secrets/stripe/current
      previous: file:/secrets/stripe/previous   # rotation: both verify
      tenant: acme                     # bound like an API key
  replay: { max_ids: 100000 }          # ids seen, per replica, bounded
```

```yaml
# api/openapi.yaml - excerpt
components:
  securitySchemes:
    stripe:
      type: apiKey
      in: header
      name: Stripe-Signature
      x-flexway-webhook: { preset: stripe, secret: stripe-endpoint }
paths:
  /hooks/stripe:
    post:
      operationId: stripeEvents
      security: [{ stripe: [] }]
```

**Rotating the secret** without losing an event:

| Step | Action |
| --- | --- |
| 1 | The old secret moves to `previous`, the new one to `current` (two keys of the mounted `Secret`); hot reload ([Changing the configuration without a restart](https://flexway.io/en/docs/operations-live-config/)) - both verify |
| 2 | The provider switches to the new secret; a signature against either one is enough |
| 3 | Once the switch is done, `previous` is removed |

| Refusal | Response |
| --- | --- |
| Header missing or unreadable, wrong signature, timestamp out of tolerance, id already seen | `401` `webhook_signature_invalid`, without saying which; the cause goes to the log and to `flexway.webhook.verifications{outcome}` |
| Body past the bound | `413`, before any verification |

The body is read whole and bounded **before** the verification, then relayed **byte for byte**; the
signature headers stay (the upstream may verify again), the secret never leaves.

## Generic HMAC webhook

> [!NOTE]
> Planned in 1.8

> **Target of version 1.8.0 - not built yet: refused at load today** (`webhooks`, `x-flexway-webhook`,
> a scheme read from `X-Signature`).

A partner without a preset: the HMAC scheme is declared in full, as a **closed template**.

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://partners.internal:8080
webhooks:
  secrets:
    partner-hooks:
      current: file:/secrets/partner/current
      tenant: partner-a
  replay: { max_ids: 100000 }
```

```yaml
# api/openapi.yaml - excerpt
components:
  securitySchemes:
    partner:
      type: apiKey
      in: header
      name: X-Signature
      x-flexway-webhook:
        secret: partner-hooks
        algorithm: sha256              # sha256 | sha512; sha1 refused
        signature: { header: X-Signature, prefix: "sha256=", encoding: hex }
        timestamp: { header: X-Timestamp, format: unix }
        id: { header: X-Delivery-Id }
        signed: "{timestamp}.{body}"   # closed template: {id}, {timestamp}, {body}
        tolerance: 5m
paths:
  /hooks/partner:
    post:
      operationId: partnerEvents
      security: [{ partner: [] }]
```

## Standard Webhooks, a shared replay memory

> [!NOTE]
> Planned in 1.8

> **Target of version 1.8.0 - not built yet: refused at load today** (`webhooks`, `x-flexway-webhook`,
> a scheme read from `webhook-signature`).

A sender that follows [Standard Webhooks](https://www.standardwebhooks.com/), the format of Flexway's
**outgoing** webhooks ([Publishing and consuming on a broker](https://flexway.io/en/docs/operations-async/)): HMAC-SHA256 (`v1`) or Ed25519 (`v1a`), id and timestamp signed. With
several replicas, the memory of the ids seen can be **shared** (a declared option): without it, a
replay on another replica within the tolerance window gets through.

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://events.internal:8080
server:
  counters: { store: "${COUNTERS_URL}" }   # the shared memory lives in the counters' store
webhooks:
  secrets:
    platform-events:
      current: file:/secrets/platform/current   # whsec_<base64>
      previous: file:/secrets/platform/previous
      tenant: acme
  replay: { max_ids: 100000, shared: true }    # a timeout, and a refusal if the store does not answer
```

```yaml
# api/openapi.yaml - excerpt
components:
  securitySchemes:
    platform:
      type: apiKey
      in: header
      name: webhook-signature
      x-flexway-webhook: { preset: standard-webhooks, secret: platform-events }
paths:
  /hooks/platform:
    post:
      operationId: platformEvents
      security: [{ platform: [] }]
```

| Preset | Replay |
| --- | --- |
| `standard-webhooks`, `stripe`, `slack`, `sendgrid` | Signed timestamp: tolerance and ids seen |
| `github`, `shopify` | **Unsigned** id: partial protection, said by `/config` |

## Address lists: a global deny list and `ip-filter`

> [!NOTE]
> Planned in 1.8

> **Target of version 1.8.0 - not built yet: refused at load today** (`server.ip_deny`,
> `server.proxy_protocol`). On its own, an `ip-filter` policy is accepted with a warning and **is not
> applied**.

Two gates: `server.ip_deny`, a **global deny list** judged before routing (a threat feed costs
neither routing nor cryptography); the `ip-filter` policy, attached to a host, a route or an
operation, judged after routing and before CORS and identity.

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://shop.internal:8080
server:
  trusted_proxies: [10.0.0.0/8]        # X-Forwarded-For believed from these peers only (already exists)
  proxy_protocol: { from: [10.0.5.0/24] }   # v1 and v2, from these peers only; read at start
  ip_deny:
    inline: [203.0.113.0/24]
    files: [/lists/threat-feed.txt]    # one range per line, # comment; read again at reload
policies:
  definitions:
    office-only:   { type: ip-filter, config: { allow: [198.51.100.0/24, "2001:db8::/32"] } }
    stripe-ranges: { type: ip-filter, config: { allow_files: [/lists/stripe-ips.txt] } }
  chains:
    hosts: { admin.example.com: [office-only] }
```

```yaml
# api/openapi.yaml - excerpt: the operation level of the chain, in the contract like any policy
paths:
  /hooks/stripe:
    post:
      operationId: stripeEvents
      x-flexway-policies: [stripe-ranges]   # the ranges the provider publishes
```

| Rule | Behaviour |
| --- | --- |
| Client address | The TCP peer; behind a peer of `trusted_proxies`, `X-Forwarded-For` read right to left up to the first untrusted hop; PROXY protocol from another peer: the connection is closed |
| `ip-filter` | `allow`: the address must be in it; `deny`: refused if it is; `deny` wins; the most precise chain wins, `!office-only` removes it |
| Unknown route | The host's chain is judged before `404` or `405`: a refused caller gets `403` and does not map the routes |
| Refusal | `403` `address_denied`, before any cryptography; `flexway.ip.refused{level}` |
| File unreadable at reload | The previous configuration stays - never an empty list put in its place |

## Negotiated compression

> [!NOTE]
> Planned in 1.8

> **Target of version 1.8.0 - not built yet: today, the `compress` policy is accepted with a warning
> (`a later version - NOT applied`) and is not applied.**

gzip, brotli and zstd, negotiated by `Accept-Encoding`; opt-in through a policy - without it, nothing
changes.

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://catalog.internal:8080
policies:
  definitions:
    compress-json:
      type: compress
      config:
        encodings: [zstd, br, gzip]    # the server's preference at equal quality
        min_bytes: 1024                # default fixed by bench M1
        types: [application/json, "application/*+json", "text/*"]   # an allow list
  chains:
    global: [compress-json]
```

```yaml
# api/openapi.yaml - excerpt: nothing to declare; a stream is never compressed
paths:
  /products:
    get:
      operationId: listProducts
```

| Rule | Behaviour |
| --- | --- |
| Eligible | Known length of at least `min_bytes`, a type of the allow list, no `Content-Encoding` from the upstream, a method other than `HEAD`; a stream (SSE, unknown length, `101`) never |
| Headers | `Content-Encoding`, `Content-Length` recomputed, `Vary: Accept-Encoding` on every eligible response, a strong `ETag` made weak |
| Levels | Fixed by bench, one constant per algorithm: no key |
| Cache ([Caching a read](https://flexway.io/en/docs/operations-cache/)) | One entry per chosen encoding, never per raw `Accept-Encoding` value |
| BREACH | A response that mixes a secret and a reflected input: exclude it with a `when` condition |

## Coalescing in-flight reads

> [!NOTE]
> Planned in 1.8

> **Target of version 1.8.0 - not built yet: refused at load today** (`x-flexway-coalesce`).

A burst of identical reads while a first one is in flight: one upstream call, the response copied to
each waiter. Opt-in **per operation**, in the contract; the key is the cache's - tenant **and**
subject for an operation that requires an identity. Each waiter was admitted and **counted**:
coalescing spares the upstream, never the quota. `flexway.yaml` has nothing new.

```yaml
# flexway.yaml - nothing specific to coalescing
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://catalog.internal:8080
```

```yaml
# api/openapi.yaml - excerpt
paths:
  /catalog/products:
    get:
      operationId: listProducts
      x-flexway-cache: { ttl: 30s }    # optional: the cache first, coalescing on its misses
      x-flexway-coalesce: { max_waiters: 256, max_bytes: 1MiB }
```

| Rule | Behaviour |
| --- | --- |
| Refused at load | A method other than `GET` or `HEAD`, a stream, WebSocket, a gRPC method, inference |
| Not shareable | `Set-Cookie`, unknown length, past `max_bytes`: each waiter calls the upstream |
| Past `max_waiters` | The call goes its own way (`flexway.coalesce.requests{role="overflow"}`) |
| Upstream error | Shared like a response; a timeout of the leader is not |
