Tiny, zero-allocation authorization engine for proxy-wasm and the edge. ~50 KB. No GC. No deps.
zopa runs as a wasm32-freestanding module. Hosts hand it a request
input and a compiled policy AST, both as JSON; zopa returns an
allow/deny decision. There's no embedded language compiler, no GC,
and no scratch memory that survives a request -- a per-request arena
is reset at the end of each evaluation.
The intended deployment is as a proxy-wasm filter in Envoy or
any other proxy-wasm 0.2.1 host. The same binary also works as a
plain WebAssembly.Module for hosts that just want to call
evaluate(input, ast) directly.
Alpha. The AST covers a useful subset of Rego, the proxy-wasm shim boots in Envoy, and the integration tests pass under three different hosts. Public surface (export names, AST schema, callback semantics) will change before 1.0.
Size. A release build is around 50 KB. OPA's WASM build is two orders of magnitude larger; Cedar and Casbin don't ship as wasm modules at all.
Allocation profile. Every evaluation runs against a single
std.heap.ArenaAllocator that is reset with .retain_capacity after
each call. After a brief warm-up, memory.grow doesn't fire again --
the wasm linear memory footprint stays flat regardless of throughput.
proxy-wasm native. proxy_on_request_headers and the rest of
the lifecycle exports are first-class. The repo ships an Envoy
bootstrap (examples/envoy/) that's exercised in CI.
No DSL to learn. zopa accepts a Rego-flavored AST as JSON. Use OPA's compiler to produce it; zopa runs it. The wasm module is the runtime, not the language.
No external dependencies. Just Zig 0.16+ stdlib. The whole code
fits in src/ and reads top-to-bottom.
import { readFileSync } from 'node:fs';
const { instance } = await WebAssembly.instantiate(
readFileSync('zig-out/bin/zopa.wasm'),
{ env: {
proxy_log: () => 0,
proxy_get_buffer_bytes: () => 1,
proxy_get_header_map_pairs: () => 1,
proxy_get_header_map_value: () => 1,
proxy_send_local_response: () => 0,
}},
);
const { malloc, free, evaluate, memory } = instance.exports;
const enc = new TextEncoder();
function write(obj) {
const bytes = enc.encode(JSON.stringify(obj));
const ptr = malloc(bytes.length);
new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes);
return [ptr, bytes.length];
}
const [ip, il] = write({ user: { role: 'admin' } });
const [ap, al] = write({
type: 'compare', op: 'eq',
left: { type: 'ref', path: ['input', 'user', 'role'] },
right: { type: 'value', value: 'admin' },
});
console.log(evaluate(ip, il, ap, al)); // 1 = allow
free(ip); free(ap);http_filters:
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
config:
configuration:
"@type": type.googleapis.com/google.protobuf.StringValue
value: |
{"type":"module","rules":[
{"type":"rule","name":"allow","default":true,
"value":{"type":"value","value":false}},
{"type":"rule","name":"allow","body":[
{"type":"eq",
"left":{"type":"ref","path":["input","method"]},
"right":{"type":"value","value":"GET"}}]}
]}
vm_config:
runtime: envoy.wasm.runtime.v8 # or .wamr / .wasmtime
code:
local:
filename: /etc/zopa/zopa.wasmA complete bootstrap with end-to-end test runner is in
examples/envoy/.
The AST is Rego-shaped JSON. Full reference: docs/ast.md.
{ "type": "module", "rules": [
{ "type": "rule", "name": "allow", "default": true,
"value": { "type": "value", "value": false } },
{ "type": "rule", "name": "allow", "body": [
{ "type": "eq",
"left": { "type": "ref", "path": ["input", "user", "role"] },
"right": { "type": "value", "value": "admin" } }
]},
{ "type": "rule", "name": "allow", "body": [
{ "type": "every", "var": "p",
"source": { "type": "ref", "path": ["input", "required_perms"] },
"body": {
"type": "some", "var": "g",
"source": { "type": "ref", "path": ["input", "user", "perms"] },
"body": { "type": "eq",
"left": { "type": "ref", "path": ["g"] },
"right": { "type": "ref", "path": ["p"] } } } }
]}
]}Supported nodes: value, ref, compare (eq/neq/lt/lte/gt/gte),
not, set, some, every, module, rule. The type field
accepts shorthand for compare ops ({"type": "eq", ...} is the same
as {"type": "compare", "op": "eq", ...}).
host wasm (zopa)
+----------+ malloc(n) +-----------------+
| Envoy / | -----------------> | host_allocator |
| any | <----------------- | (length-prefix)|
| runtime | ptr +-----------------+
| |
| | evaluate(in,ast) +-----------------+
| | -----------------> | request arena |
| | | json.parse |
| | | ast.buildModule|
| | <----------------- | evalModule |
| | 1 / 0 / -1 | arena.reset |
+----------+ +-----------------+
host_allocator (std.heap.wasm_allocator) lives for the module's
lifetime and backs every host-visible buffer. The request arena is
allocated on top of it and reset at the end of every evaluate(),
including the proxy-wasm callback path.
More detail in docs/architecture.md.
You need Zig 0.16.0:
brew install zig # or download from ziglang.org
zig build # debug build
zig build --release=small # ~50 KB optimized .wasmThe artifact is zig-out/bin/zopa.wasm.
zopa runs the same suite under three hosts. None of them are required; pick what's installed.
zig build test # Node.js (must have node 18+)
zig build test-wasmtime # wasmtime via Python (see test/requirements.txt)
zig build test-envoy # real Envoy (brew install envoy)
zig build test-all # everything availableSetup for the Python suite:
python3 -m venv .venv-test
.venv-test/bin/pip install -r test/requirements.txt| OPA | Cedar | Casbin | zopa | |
|---|---|---|---|---|
| Language | Go | Rust | Go (+ ports) | Zig |
| Released as wasm | Yes (~30 MB) | No | No | Yes (~50 KB) |
| Allocation model | GC | RC + arenas | GC | per-request arena |
| proxy-wasm | Side project | No | No | First-class |
| Policy input | Rego source | Cedar source | CSV / source | Compiled AST (Rego-shaped) |
| Maturity | CNCF Graduated | Stable | Mature | Alpha |
zopa is not a replacement for OPA when you need the full Rego language, the management plane, or bundles. It's a drop-in for the narrow case where you've already compiled the policy and want to evaluate it inside a proxy-wasm filter without a 30 MB sidecar.
See ROADMAP.md. Body-aware and response-side policies, plus an OPA conformance harness, are the next big items.
CONTRIBUTING.md covers local setup, code style, DCO, and PR expectations.
SECURITY.md. Use GitHub's private vulnerability reporting; don't open a public issue for security bugs.
zopa would not exist without:
- Open Policy Agent for the Rego language and reference implementation.
- Cedar for the example of a small, focused authorization language.
- proxy-wasm/spec and the Envoy team for the ABI.