How to Extend CubeEgress with Custom Lua Modules: 3 Methods Explained

You can extend CubeEgress by adding new Lua files to the OpenResty data plane, hooking code into the worker initialization phase, or registering custom policy action modules that integrate with the existing policy engine.

CubeEgress, the data-plane component of the CubeSandbox repository, runs on OpenResty (NGINX + LuaJIT) and stores its logic in standard Lua modules under CubeEgress/lua/. Because the architecture relies on Lua's require system rather than compiled plugins, you can inject custom business logic at three distinct lifecycle points without recompiling NGINX.

Three Methods for Extending CubeEgress with Custom Lua Modules

CubeEgress loads its functionality through ordinary Lua require statements executed during NGINX phases. This design yields three distinct extension strategies, each suited to different operational needs.

Option 1: Add Request-Level Logic via Phase Hooks

Drop a new .lua file alongside the existing egress modules and import it from one of the phase-specific entry points. The most common hooks are access_phase.lua (for request authorization), header_filter_phase.lua (for response manipulation), and log_phase.lua (for audit trails).

This method runs your code on every request entering the chosen phase. It is ideal for simple transformations like custom header injection, conditional redirects, or supplemental logging.

In CubeEgress/lua/access_phase.lua, the entry point loads the policy module with local policy = require "policy". You can add a similar line to load your custom module:

-- Add near the top of access_phase.lua
local my_custom = require "my_custom"

-- Invoke inside the request-handling loop
my_custom.handle()

Option 2: Worker-Level Initialization in init_worker_phase

Run code once per worker process by hooking into CubeEgress/lua/init_worker_phase.lua. This file triggers bootstrap.run() to load the policy bundle, making it the correct location for heavy-weight initialization that should not repeat per request.

Use this option to load static lookup tables, pre-compute data structures, start background timers, or register additional shared-dictionary keys.

-- Inside init_worker_phase.lua
local static_map = require "static_map"
static_map.load()

The init_worker_phase.lua file runs immediately after the worker spawns, before any traffic is processed, ensuring your data is ready before the first request arrives.

Option 3: Register Custom Policy Actions

Implement a module that conforms to the interface used by built-in actions (allow, inject, audit), then invoke it dynamically from access_phase.lua when a policy rule matches. This approach lets administrators toggle functionality via the JSON policy API without redeploying NGINX configuration files.

The policy-driven approach executes only when a specific rule matches, providing per-rule granularity. To implement, check for your custom module using pcall and pass the rule context:

-- Inside access_phase.lua rule execution block
local ok, custom_mod = pcall(require, "custom_action")
if ok then
    custom_mod.apply(rule, ctx)
end

Your policy JSON can then reference the custom behavior:

{
  "policy_id": "example",
  "rules": [
    {
      "id": "r1",
      "match": {},
      "action": {
        "allow": true,
        "custom": "add_cookie"
      }
    }
  ]
}

Core Architecture and Entry Points

Understanding the Lua loading sequence helps you choose the correct hook point. CubeEgress uses the following files as primary extension anchors:

File Role Key Loading Pattern
CubeEgress/lua/init_worker_phase.lua Worker initialization local bootstrap = require "bootstrap"
CubeEgress/lua/bootstrap.lua Policy bundle download Reads CUBE_EGRESS_BOOTSTRAP_URL environment variable
CubeEgress/lua/access_phase.lua Request processing local policy = require "policy"
CubeEgress/lua/policy.lua Policy storage & validation Interacts with ngx.shared.policy_store

All custom modules follow the same pattern: create a Lua table, define functions, and return the table.

Implementation Examples

Example: Simple Request Header Injection

Create CubeEgress/lua/my_custom.lua to inject client IP headers:

local _M = {}

function _M.handle()
    local ip = ngx.var.remote_addr
    ngx.req.set_header("X-My-Proxy-Client", ip)
end

return _M

Hook it into access_phase.lua as shown in Option 1 above.

Example: Worker-Level Static Data Pre-loading

For data that should load once per worker:

-- CubeEgress/lua/static_map.lua
local cjson = require "cjson.safe"
local _M = { map = {} }

function _M.load()
    local f = io.open("/etc/cube/lookup.json", "r")
    if f then
        local data = f:read("*a")
        _M.map = cjson.decode(data) or {}
        f:close()
    end
end

return _M

Load this from init_worker_phase.lua to populate static_map.map before traffic hits.

Example: Policy-Driven Custom Actions

Implement a conditional cookie injector:

-- CubeEgress/lua/custom_action.lua
local _M = {}

function _M.apply(rule, ctx)
    if rule.action.custom == "add_cookie" then
        ngx.header["Set-Cookie"] = "mycookie=1; Path=/"
    end
end

return _M

Register this in access_phase.lua using pcall(require, "custom_action") to safely load the module if present.

Shared Dictionary and State Management

CubeEgress stores runtime policies in ngx.shared.policy_store, defined in CubeEgress/lua/policy.lua. Custom modules can interact with this shared memory or declare their own dictionaries in the NGINX configuration (lua_shared_dict my_mod_store 10m).

The policy index is maintained manually through functions like read_index and with_index_lock in policy.lua. When extending CubeEgress, respect these locking patterns to avoid race conditions when updating shared state.

Summary

  • CubeEgress uses standard Lua modules on OpenResty, allowing extensions via require statements.
  • Option 1 (phase hooks) runs per-request in access_phase.lua or header_filter_phase.lua for header manipulation and logging.
  • Option 2 (init_worker_phase) runs once per worker for static data loading and timer initialization.
  • Option 3 (policy actions) enables dynamic rule-based execution controlled via the JSON policy API.
  • All extensions interact with shared dictionaries like ngx.shared.policy_store and follow the file paths in CubeEgress/lua/.

Frequently Asked Questions

Where do I place custom Lua modules in CubeEgress?

Place new .lua files in the CubeEgress/lua/ directory alongside the existing modules like bootstrap.lua and policy.lua. The OpenResty Lua package path includes this directory, so you can load them with require "my_custom" without additional path configuration.

Can I access the policy store from custom modules?

Yes. Custom modules can read and write to ngx.shared.policy_store, the same shared dictionary used by CubeEgress/lua/policy.lua. You can also define new shared dictionaries in the NGINX configuration for module-specific data isolation.

Do custom Lua modules require NGINX recompilation?

No. CubeEgress runs on LuaJIT through OpenResty, which interprets Lua modules at runtime. Simply drop your .lua files into the CubeEgress/lua/ directory and reload the NGINX configuration to pick up changes. No C compilation or binary linking is required.

How do I debug custom Lua code in CubeEgress?

Use ngx.log(ngx.ERR, "message") to write to the NGINX error log, or ngx.say() for development environments. Since the code runs in access_phase.lua or init_worker_phase.lua, standard OpenResty debugging techniques apply, including using pcall to catch module loading errors as implemented in the policy action extension pattern.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →