Docs
API7 GatewayExtend and integratePluginsDevelop Custom Lua Plugins

Develop Custom Lua Plugins

Learn how to develop, register, and test custom Lua plugins for API7 Gateway.

Applies to:API7 Gateway 3.10.x

Roles:Platform engineerTime:45 minutesImpact:Changes live traffic

Goal

A custom Lua plugin, written and uploaded to a gateway group, running on a route.

Custom Lua plugins extend API7 Gateway by executing your own logic during the phases of the request and response lifecycle.

Before you begin

  • Basic knowledge of the Lua programming language.

  • Familiarity with OpenResty and the Nginx request lifecycle.

  • Access to an API7 Gateway instance and its Dashboard.

  • A token from the Dashboard.

  • For the ADC path, ADC installed, and the variables the examples use:

    export API7_ADMIN_URL="https://localhost:7443"
    export API7_GATEWAY_GROUP_ID="default"   # the group the quickstart creates; otherwise its UUID from Gateway Groups in the Dashboard
    read -rp "Gateway group name for ADC: " API7_GATEWAY_GROUP_NAME && export API7_GATEWAY_GROUP_NAME
    read -rsp "Dashboard token: " API7_ADMIN_KEY && export API7_ADMIN_KEY && echo

    API7_GATEWAY_GROUP_ID is used by the Admin API examples and API7_GATEWAY_GROUP_NAME by the ADC ones — they identify the same gateway group in two different ways.

Plugin Structure

A Lua plugin is a module that exports a table containing its configuration schema, priority, and phase handlers.

Standard Fields

Every plugin should define the following fields:

  • name: The unique name of the plugin.
  • version: The version of the plugin.
  • priority: Determines the execution order. Plugins with higher priority run earlier.
  • schema: A JSON schema that defines the configuration parameters for the plugin.

Execution Phases

API7 Gateway executes plugins in the following order:

  1. rewrite: Modify the request before routing (e.g., changing the URI).
  2. access: Main processing phase (e.g., authentication, rate limiting).
  3. before_proxy: After the access phase, just before the request is proxied to the upstream.
  4. header_filter: Modify response headers.
  5. body_filter: Modify the response body.
  6. log: Record logs after the response has been sent to the client.

Step-by-Step Development

1. Create the Plugin File

Create a new Lua file for your plugin. This example uses a plugin named custom-header.

2. Define the Schema and Priority

Use apisix.core to validate the plugin configuration against a JSON schema.

local core = require("apisix.core")

local schema = {
    type = "object",
    properties = {
        header_name = {
            type = "string",
            default = "X-Custom-Lua"
        },
        header_value = {
            type = "string",
            default = "loaded"
        }
    }
}

local plugin_name = "custom-header"

local _M = {
    version = 0.1,
    priority = 1500,
    name = plugin_name,
    schema = schema,
}

function _M.check_schema(conf)
    return core.schema.check(schema, conf)
end

3. Implement Phase Handlers

Implement the logic in the appropriate phase. To add a header to the request before it reaches the upstream, use the access phase.

function _M.access(conf, ctx)
    core.request.set_header(ctx, conf.header_name, conf.header_value)
end

4. Complete Plugin Example

Here is the full code for the custom-header plugin:

local core = require("apisix.core")

local schema = {
    type = "object",
    properties = {
        header_name = {
            type = "string",
            default = "X-Custom-Lua"
        },
        header_value = {
            type = "string",
            default = "loaded"
        }
    }
}

local plugin_name = "custom-header"

local _M = {
    version = 0.1,
    priority = 1500,
    name = plugin_name,
    schema = schema,
}

function _M.check_schema(conf)
    return core.schema.check(schema, conf)
end

function _M.access(conf, ctx)
    core.request.set_header(ctx, conf.header_name, conf.header_value)
end

return _M

Register the Plugin in API7 Gateway

In API7 Gateway, custom plugins are managed from the control plane. After you prepare the Lua source file, upload and deploy it from API7 Dashboard so it becomes available in the plugin picker for routes, services, consumers, or global rules.

1. Add the Plugin in the Dashboard

A custom plugin belongs to the gateway group you upload it to. In the Dashboard, open the gateway group that should run the plugin, go to Custom Plugins, then click Add Custom Plugin.

Upload the Lua source file and provide the required plugin metadata:

  • Plugin Source Code File: upload custom-header.lua
  • Plugin Description: briefly describe what the plugin does
  • Plugin Documentation Link: optionally link to your plugin documentation
  • Plugin Author: identify the owner of the plugin

When the file is uploaded successfully, API7 Gateway parses the plugin and displays its detected name and version in the dialog before you add it.

After the plugin is added, it can be selected from the Add Plugin dialog on routes and services in that gateway group. To run it in another gateway group, upload it there as well — see Roll Out Across Gateway Groups.

Before version 3.10.6, custom plugins were managed under Gateway Settings for the whole control plane and a Deployed Gateway Group field selected which gateway groups received them. Plugins that existed before the upgrade are carried over into every gateway group they were deployed to.

2. Confirm the Plugin is Available

Open a route or service in that gateway group and confirm that custom-header appears in the Add Plugin dialog.

Register via the Admin API

To upload a custom plugin without the Dashboard — for example, from a GitOps pipeline — register it directly on the gateway group:

jq -n --rawfile src custom-header.lua \
  '{source_code: $src, catalog: "Traffic", description: "Adds a custom response header", documentation_link: "https://example.com/docs/custom-header", author: "platform-team"}' \
  > custom-header-payload.json

curl -k "https://localhost:7443/api/gateway_groups/${GATEWAY_GROUP_ID}/custom_plugins/custom-header" -X PUT \
  -H "X-API-KEY: ${API_KEY}" \
  -H "Content-Type: application/json" \
  -d @custom-header-payload.json

The plugin name is the last path segment (custom-header above), and source_code must be the plugin's Lua source as a JSON string. catalog, description, documentation_link, and author are all required and rejected if empty; catalog must be one of the Dashboard's built-in catalog names (for example Traffic, Security, or General) — an arbitrary value such as other is rejected with invalid plugin catalog: <value>.

The pre-3.10.6, control-plane-wide endpoint (/api/custom_plugins) no longer exists — it returns 410 Gone with a message pointing at the gateway-group-scoped path above. Every custom plugin request needs the gateway group ID as part of the path, as shown above.

Add Dependency Libraries

A custom plugin you upload from the Dashboard is registered as a single, self-contained plugin module. The control plane distributes its source code to the gateways and loads it directly into the runtime. The file is not written to the gateway's Lua module search path. Because of this, an uploaded custom plugin is not reliably resolvable through require() from another plugin. You should not upload a shared library as a custom plugin for other plugins to require. That may appear to work at first, but it breaks after a gateway restart: the dependency is not guaranteed to load before the plugins that require it.

From 3.10.5 a plugin can instead be uploaded as a package: an archive holding $NAME.lua at the top level, its dependency modules under $NAME/, and a metadata.json. Entry file and dependencies are distributed together as one unit, and no gateway restart is required. That is the supported way to ship a plugin with its own private dependencies, and it avoids everything below.

The filesystem approach in the rest of this section is still the right answer for a module genuinely shared between several plugins, and for native (C) libraries, which a package cannot carry.

Some plugins share helper code, or depend on a third-party Lua module or a native (C) library. Install that dependency on the gateway's package path, and require it from your plugin as usual.

Place the Dependency on the Lua or C Path

The gateway resolves modules through two search paths:

  • Lua modules are resolved through lua_package_path. A file at <dir>/myorg/utils.lua is loaded with require("myorg.utils").
  • Native Lua/C modules are resolved through lua_package_cpath. A file at <dir>/myorg/native.so is loaded with require("myorg.native").

You can use either the gateway's built-in paths or your own directories:

  • Built-in path — place the file under the gateway's built-in search path (for example, /usr/local/apisix/), matching the module name you require.

  • Extra path (recommended) — keep your code separate from the runtime and add your own directory in the data plane config.yaml:

    apisix:
      extra_lua_path: "/data/lua/?.lua"     # extends lua_package_path
      extra_lua_cpath: "/data/clib/?.so"    # extends lua_package_cpath

    For details, see Custom Plugin Paths and the configuration reference.

A native library loaded through LuaJIT FFI with ffi.load() is resolved by the system dynamic linker, not by lua_package_cpath. Place such libraries on the system library path (for example, /usr/local/lib) or set LD_LIBRARY_PATH on the gateway.

Make the Files Available to the Gateway

The dependency files must exist on every gateway node before the gateway starts:

  • Kubernetes / Helm — mount the files into the gateway pod with a volume (such as a ConfigMap or a persistent volume) and point extra_lua_path / extra_lua_cpath at the mount path.
  • Docker / Docker Compose — bind-mount the directory into the gateway container.

Unlike uploaded custom plugins, which are distributed dynamically by the control plane, libraries on the package path are loaded when the gateway worker starts. After you add or change them, reload or restart the gateway for the change to take effect.

Example

Put shared helpers in a plain Lua module on the gateway's path — for example, /data/lua/myorg/base.lua:

local _M = {}

function _M.verify(token)
    -- shared logic used by multiple plugins
    return token ~= nil
end

return _M

Add the directory to the data plane configuration:

apisix:
  extra_lua_path: "/data/lua/?.lua"

Then require it from any custom plugin:

local core = require("apisix.core")
local base = require("myorg.base")

-- ...

function _M.access(conf, ctx)
    if not base.verify(core.request.header(ctx, "Authorization")) then
        return 401
    end
end

Because the module is on the package path, require("myorg.base") resolves identically on every worker and survives gateway restarts.

Test the Plugin

Once the plugin source code is available on the gateway node and the plugin is registered in API7 Gateway, you can enable it on a specific route using the Admin API or ADC.

  1. Create a route with the plugin enabled:
curl -k "${API7_ADMIN_URL}/apisix/admin/services/custom-header-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
  -H "X-API-KEY: ${API7_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "custom-header-service",
    "upstream": {
      "type": "roundrobin",
      "scheme": "http",
      "nodes": [
        {
          "host": "httpbin.org",
          "port": 80,
          "weight": 100
        }
      ]
    }
  }'

curl -k "${API7_ADMIN_URL}/apisix/admin/routes/custom-header-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
  -H "X-API-KEY: ${API7_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "custom-header-route",
    "methods": ["GET"],
    "paths": ["/get"],
    "service_id": "custom-header-service",
    "plugins": {
      "custom-header": {
        "header_name": "X-Trace-ID",
        "header_value": "docs-test"
      }
    }
  }'
  1. Send a request and verify the header on the upstream response body:
curl -i "http://localhost:9080/get"

The httpbin response should include X-Trace-ID: docs-test under headers, which confirms the plugin modified the request before proxying.

If the proxy returns 404 Route Not Found immediately after the route is created, wait a few seconds and retry the request.

Roll Out Across Gateway Groups

A custom plugin belongs to one gateway group. Each gateway group is an isolated configuration scope. The plugin you upload to one group is not available to routes or services in another, and each group runs the code you uploaded to it. That isolation is what lets you validate a plugin, or a change to one, in a non-production group before it can affect production traffic.

The recommended rollout is:

  1. Upload to a test gateway group. Open the test or staging gateway group and add the plugin there. It becomes available to that group alone.
  2. Validate. Enable the plugin on a route in the test group and confirm it behaves as expected, following Test the Plugin. Production groups do not have the plugin yet, so they are unaffected while you iterate.
  3. Promote to production. Once validated, upload the same file to the production gateway group. The control plane distributes it to that group's gateways immediately, and no other gateway group is touched.

The same steps apply to a plugin that is already in production. Upload the new code to the test group and validate it there, while production keeps running the code it already has, then upload it to production. The two builds do not need different names, so routes, services, and plugin_metadata keep referring to the plugin by the same name throughout.

Keep these points in mind:

  • An upload replaces the code in that gateway group only. Uploading to the test group never changes what production runs, and the reverse holds too.
  • Deleting removes the plugin from that gateway group. If routes in that group still reference the plugin, the deletion is blocked until they stop. Other gateway groups keep their copy.
  • The plugin name is unique within a gateway group, not across the control plane. Two gateway groups can hold unrelated plugins under the same name. Keep the name stable for a plugin you are rolling out, and treat a name as one plugin across your environments.

Available in API7 Enterprise from version 3.10.6. Before that, a custom plugin was a single control-plane wide object with a list of deployed gateway groups. Uploading new code updated every one of those groups at once.

Before 3.10.6 the control plane also looked custom plugins up by name alone. A route could reference a plugin its own gateway group did not have, and the gateway never ran that plugin. From 3.10.6, saving such a resource is rejected. Upload the plugin to that gateway group, or remove it from the configuration.

Roll back / Clean up

Undo this guide in the reverse order it was applied. Removing the plugin file while a route still references the plugin leaves that route referring to something the gateway does not have.

  1. Remove the plugin from the route, by deleting the custom-header block from adc.yaml and re-syncing, or by sending the route again without it through the Admin API.

  2. Delete the route and service if you created them only for this guide:

    curl -k "${API7_ADMIN_URL}/apisix/admin/routes/custom-header-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X DELETE \
      -H "X-API-KEY: ${API7_ADMIN_KEY}"
    curl -k "${API7_ADMIN_URL}/apisix/admin/services/custom-header-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X DELETE \
      -H "X-API-KEY: ${API7_ADMIN_KEY}"
  3. Delete the plugin from the gateway group in the Dashboard. The control plane refuses the deletion while any route or service in that group still references the plugin, which is the check that makes step 1 matter.

  4. Remove any dependency files you placed on the gateway's Lua or C path, and restart the gateway instances if you added them by rebuilding the image or mounting a volume.

Deleting the plugin from one gateway group leaves every other group's copy in place.

Troubleshoot

  • Check Error Logs: By default, logs are located at /usr/local/apisix/logs/error.log.
  • Syntax Errors: Use luac -p custom-header.lua to check for Lua syntax errors before uploading.
  • Plugin Visibility: If the plugin does not appear in the Dashboard plugin list, verify it was uploaded to the gateway group you are configuring.
  • Admin API Payload Shape: In API7 Gateway, route creation uses paths and service_id rather than APISIX-style top-level uri and inline upstream.
  • Name Conflicts: Ensure the plugin name is unique and does not collide with a built-in plugin.