Stream Kong Gateway plugins

Deployment Platform
Related Resources
Minimum Version
Kong Gateway - 3.15
TL;DR

Use the custom_plugins key in your decK configuration to embed the plugin schema and handler directly in Kong Gateway entity configuration. If you’re running in hybrid mode, the control plane streams the plugin to all connected data planes automatically.

Prerequisites

This is a Konnect tutorial and requires a Konnect personal access token.

  1. Create a new personal access token by opening the Konnect PAT page and selecting Generate Token.

  2. Export your token to an environment variable:

    export KONNECT_TOKEN='YOUR_KONNECT_PAT'
  3. Run the quickstart script to automatically provision a Control Plane and Data Plane, and configure your environment:

    curl -Ls https://get.konghq.com/quickstart | bash -s -- -k $KONNECT_TOKEN \
         -e KONG_CUSTOM_PLUGIN_STREAMING_ENABLED=on
         --deck-output

    This sets up a Konnect Control Plane named quickstart, provisions a local Data Plane, and prints out the following environment variable exports:

    export DECK_KONNECT_TOKEN=$KONNECT_TOKEN
    export DECK_KONNECT_CONTROL_PLANE_NAME=quickstart
    export KONNECT_CONTROL_PLANE_URL=https://us.api.konghq.com
    export KONNECT_PROXY_URL='http://localhost:8000'

    Copy and paste these into your terminal to configure your session.

This tutorial requires Kong Gateway Enterprise. If you don’t have Kong Gateway set up yet, you can use the quickstart script with an enterprise license to get an instance of Kong Gateway running almost instantly.

  1. Export your license to an environment variable:

     export KONG_LICENSE_DATA='LICENSE-CONTENTS-GO-HERE'
  2. Run the quickstart script:

    curl -Ls https://get.konghq.com/quickstart | bash -s -- -e KONG_LICENSE_DATA \
         -e KONG_CUSTOM_PLUGIN_STREAMING_ENABLED=on

    Once Kong Gateway is ready, you will see the following message:

     Kong Gateway Ready

To complete this tutorial, install decK. We recommend keeping decK up to date with the latest version (1.64.0).

decK is a CLI tool for managing Kong Gateway declaratively with state files. This guide uses deck gateway apply, which directly applies entity configuration to your Gateway instance.

You can check your current decK version with deck version.

For this tutorial, you’ll need Kong Gateway entities, like Gateway Services and Routes, pre-configured. These entities are essential for Kong Gateway to function but installing them isn’t the focus of this guide. Follow these steps to pre-configure them:

  1. Run the following command:

    echo '
    _format_version: "3.0"
    services:
      - name: example-service
        url: http://httpbin.konghq.com/anything
    routes:
      - name: example-route
        paths:
        - "/anything"
        service:
          name: example-service
        protocols:
        - http
        - https
    ' | deck gateway apply -

To learn more about entities, you can read our entities documentation.

If you’re not using the Gateway quickstart, make sure your Gateway user has super-admin or admin permissions (not workspace-super-admin or workspace-admin.) The quickstart demo user has these permissions by default.

Ensure your Konnect account has one of the following control plane roles: ServiceAdmin, RouteAdmin, PluginAdmin, CPAdmin, or Deployer.

Normally, deploying a custom plugin requires uploading Lua files to every data plane and restarting Kong Gateway. With streaming plugins, you define the plugin schema and handler directly in your Kong Gateway entity configuration. The control plane becomes the single source of truth and distributes the plugin to all connected data planes automatically, with no file management or restarts needed.

In this guide, you’ll define two plugins inline to demonstrate how streaming works:

  • replaceme: Substitutes a target word in the request body with a replacement word before forwarding to the upstream.
  • reflector: Returns the request body directly to the caller, bypassing the upstream. This lets you inspect the modified body without needing an external service.

You’ll apply replaceme globally with a condition so it only runs when the request path does not contain the word skip, then validate both cases.

Create the first plugin

The replaceme plugin reads the raw request body, performs a global text substitution, and writes the modified body back before the request is proxied upstream.

cat <<'EOF' | deck gateway apply --include-plugin-definitions -
_format_version: "3.0"
_transform: true

custom_plugins:
  - name: replaceme
    schema: |
      return {
        name = "replaceme",
        fields = {
          {
            config = {
              type = "record",
              fields = {
                { target_word = { type = "string", required = true } },
                { replacement_word = { type = "string", required = true } },
              },
            },
          },
        },
      }
    handler: |
      local WordReplacerHandler = {
        PRIORITY = 800,
        VERSION = "1.0.0",
      }
      function WordReplacerHandler:access(config)
        local raw_body, err = kong.request.get_raw_body()
        if err then
          kong.log.err("Failed to read request body: ", err)
          return
        end
        if raw_body and raw_body ~= "" then
          local escaped_target = config.target_word:gsub("([^%w])", "%%%1")
          local modified_body, count = string.gsub(raw_body, escaped_target, config.replacement_word)
          if count > 0 then
            kong.service.request.set_raw_body(modified_body)
          end
        end
      end
      return WordReplacerHandler
EOF

Where:

  • custom_plugins.name: A unique name for the plugin.
  • custom_plugins.schema: The Lua schema definition, which declares the plugin’s configuration fields.
  • custom_plugins.handler: The Lua handler that contains the plugin logic.

Create the second plugin

The reflector plugin returns the request body directly to the caller with a 200 response, bypassing the upstream entirely. This makes it useful for testing what the request body looks like after earlier plugins have modified it.

The reflector plugin has an empty schema because it takes no configuration. Its PRIORITY is set to -10 so it runs after replaceme (priority 800), ensuring replaceme modifies the body first.

cat <<'EOF' | deck gateway apply --include-plugin-definitions -
_format_version: "3.0"
_transform: true

custom_plugins:
  - name: reflector
    schema: 'return { name = "reflector", fields = { { config = { type = "record", fields = {} } } } }'
    handler: |
      local ReflectorHandler = {
        PRIORITY = -10,
        VERSION = "1.0.0",
      }
      function ReflectorHandler:access(config)
        local body = kong.request.get_raw_body()
        local headers = kong.request.get_headers()
        local content_type = headers["content-type"] or "text/plain"
        return kong.response.exit(200, body, {
          ["Content-Type"] = content_type
        })
      end
      return ReflectorHandler
EOF

Configure the plugins

Now that both plugins are defined, apply them globally. Apply replaceme with a condition so it only runs when the request path doesn’t contain skip:

echo '
_format_version: "3.0"
plugins:
  - name: replaceme
    condition: '!http.path.contains("skip")'
    config:
      target_word: sea
      replacement_word: pelican
  - name: reflector
' | deck gateway apply --include-plugin-definitions -

Validate

Send a request with the word sea in the body. The replaceme plugin substitutes every occurrence of sea with pelican, and reflector returns the modified body directly:

curl http://localhost:8000/anything -d 'She sells sea shells by the sea shore.'

You should see the following response:

She sells pelican shells by the pelican shore.

Now send the same request, but include skip somewhere in the path. The condition !http.path.contains("skip") prevents replaceme from running, so the body passes through unchanged:

curl http://localhost:8000/anything/skip -d 'She sells sea shells by the sea shore.'

You should see the following response:

She sells sea shells by the sea shore.

Cleanup

If you created a new control plane and want to conserve your free trial credits or avoid unnecessary charges, delete the new control plane used in this tutorial.

curl -Ls https://get.konghq.com/quickstart | bash -s -- -d

FAQs

No, there are some limitations. The plugin must have only one handler and one schema, can’t run in the init_worker phase or create timers, and must be written in Lua. See the custom plugin streaming reference for more detail.

Help us make these docs great!

Kong Developer docs are open source. If you find these useful and want to make them better, contribute today!