MastertheMesh
Solo · Gateway API
Field guide

HTTPRoutes and tenant/team delegation routing

TO
Tom O'Rourke
EMEA Field CTO · Solo.io

One shared gateway, many teams. kgateway route delegation lets a small parent HTTPRoute hand a path prefix to a child route that each vertical team owns in its own namespace. This page walks the routing decision end to end: why you split instead of writing one big route, how delegation by label works, what it takes to add a new vertical, and where the traffic actually lands.

HTTPRoute kgateway Gateway API Delegation Multi-tenant

The question that starts this. "We run one gateway for the whole platform. Banking, insurance and payments are separate teams in separate namespaces. How do we let each team own its own routes without a central file that everyone has to fight over, and without one team's typo breaking another team's traffic?" kgateway's answer is route delegation: a parent route owned by the platform team that delegates each path prefix, by label, to a child route the owning team controls.

Two kinds of HTTPRoute

The pattern uses two roles. Both are the same HTTPRoute kind from the Gateway API, but they do very different jobs and they are owned by different teams.

Parent route

owned by the platform / gateway team · namespace kgateway-system

Attached to the Gateway. It names no application. Each rule says "anything under /banking is handled over there, anything under /insurance over there." It picks which team's route handles a prefix, nothing more.

Child route

owned by each vertical team · in that team's own namespace

Lives in the team's namespace and defines the real routes for that vertical: /banking/accounts, /banking/cards, and the backends they point at. The team merges changes here through Git with no access to anyone else's routes.

The parent delegates a path prefix to a child, and the child resolves it to a real backend. That is the whole mechanism.

The routing decision, end to end

request api.example.com/... PARENT HTTPRoute — picks which team's route owned by the platform / gateway team · namespace: kgateway-system /banking delegate → label banking-bff /insurance delegate → label insurance-bff /payments delegate → label payments-bff delegation by label (namespace: all — matches any team) CHILD HTTPRoute namespace: banking label: banking-bff /banking/accounts /banking/cards owned by the banking team CHILD HTTPRoute namespace: insurance label: insurance-bff /insurance/quotes /insurance/claims owned by the insurance team CHILD HTTPRoute namespace: payments label: payments-bff /payments/charge /payments/refund owned by the payments team backendRefs banking-bff Service Pods in this cluster insurance-bff Service Pods in this cluster payments-external Backend out-of-cluster (DNS / other region) What the parent decides vs. what the child decides Parent picks which team's route handles the path prefix. It never names a backend. Child picks the destination: an in-cluster Service, or a Backend pointing out of cluster.

How to read it. A request to api.example.com hits the parent route on the gateway. The parent matches the path prefix and delegates by label to the child route in the owning team's namespace. The child matches the full path and forwards through its own backendRefs. Banking and insurance land on an in-cluster Service; payments shows the same delegation resolving to an out-of-cluster Backend.

Why split instead of one big route

You could write a single HTTPRoute with every route for every vertical in it. It works. The reason not to is ownership and blast radius.

What one shared route costs you

Splitting it gives the banking team the banking route in the banking namespace, the payments team theirs, and so on. Each team merges its own changes in its own namespace with no access to anyone else's routes. The platform team owns only the small parent route.

Delegation by label

Look at how the parent points at a child. The backendRefs is not pointing at an application. It points at a label.

The parent says delegate /banking to whichever HTTPRoute carries the label delegation.kgateway.dev/label: banking-bff, in any namespace (namespace: all). The child opts in simply by wearing that label. Because the parent matches by label and not by a specific route name, a team can deploy a new child with the right label and it attaches automatically. The parent never has to be edited.

parent backendRefs · points at a label, not an app group + kind + name select the child route
backendRefs:
- group: delegation.kgateway.dev
  kind: label
  name: banking-bff
  namespace: all          # match the labelled child in any namespace
child metadata.labels · how a route opts in wear the label the parent is delegating to
metadata:
  name: banking-routes
  namespace: banking
  labels:
    delegation.kgateway.dev/label: banking-bff

Adding insurance and payments

There are two cases when a new vertical shows up, and only one of them touches the parent.

Case A · the prefix already exists in the parent

Say the parent already delegates /insurance to label insurance-bff with namespace: all. A new insurance team just deploys their child route carrying delegation.kgateway.dev/label: insurance-bff. No parent edit needed. It attaches on its own.

Case B · a brand-new top-level prefix

For a prefix the parent has never seen, you add one rule to the parent (a five-line block) for the new prefix, then the owning team deploys their labelled child. So a whole new vertical is one small parent rule plus the team's own child route.

The YAML below is the parent with all three verticals, plus the payments child added.

HTTPRoute · parent with three verticals + payments child platform owns the parent, payments owns the child
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: parent
  namespace: kgateway-system
spec:
  parentRefs:
  - name: http
    namespace: kgateway-system
  hostnames:
  - "api.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /banking
    backendRefs:
    - group: delegation.kgateway.dev
      kind: label
      name: banking-bff
      namespace: all
  - matches:
    - path:
        type: PathPrefix
        value: /insurance
    backendRefs:
    - group: delegation.kgateway.dev
      kind: label
      name: insurance-bff
      namespace: all
  - matches:
    - path:
        type: PathPrefix
        value: /payments
    backendRefs:
    - group: delegation.kgateway.dev
      kind: label
      name: payments-bff
      namespace: all
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: payments-routes
  namespace: payments
  labels:
    delegation.kgateway.dev/label: payments-bff
spec:
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /payments/charge
    backendRefs:
    - name: payments-bff
      port: 8080
  - matches:
    - path:
        type: PathPrefix
        value: /payments/refund
    backendRefs:
    - name: payments-bff
      port: 8080

Where does the traffic actually land

Not another gateway. Delegation routes to another HTTPRoute, and that child resolves to a real backend through its own backendRefs. The destination is whatever the child points at:

In-cluster app

the default · the normal BFF case

With no group/kind, a backendRef defaults to a Kubernetes Service in the same namespace. The BFF is a deployment, so the traffic goes to its Pods in this cluster.

Out-of-cluster service

a Backend with static hosts / DNS

Point the child at a kgateway Backend (gateway.kgateway.dev) on port: 443. The vertical's BFF can live in another cluster or region and you delegate to it exactly the same way.

child backendRefs · in-cluster Service vs out-of-cluster Backend same delegation, different destination
# in-cluster: defaults to a Service named payments-bff
backendRefs:
- name: payments-bff
  port: 8080

# out-of-cluster: a Backend CRD with static hosts / DNS
backendRefs:
- group: gateway.kgateway.dev
  kind: Backend
  name: payments-external
  port: 443

So the two halves stay clean. Delegation handles the routing decision, which path goes to which team's route. The child's backendRefs handles the destination, an app in this cluster or an external target.

References

Adjacent demos on this site