Skip to main content
Version: 3.10.x

Develop Custom Lua Plugins

Custom Lua plugins allow you to extend the functionality of API7 Gateway by executing custom logic during different phases of the request/response lifecycle. This guide walks you through creating a simple Lua plugin, registering it in API7 Gateway, and applying it to a route.

Prerequisites

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

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

For the product workflow, see Custom Plugins.

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.

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.

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.

note

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 "https://localhost:7443/apisix/admin/services/custom-header-service?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_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 "https://localhost:7443/apisix/admin/routes/custom-header-route?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_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.

Debugging Tips

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

Next Steps