Table of Contents

We’ve started a project to centralize authorization logic using Open Policy Agent (OPA) compiled to WebAssembly (WASM) and enforced locally inside our Resource Servers via a small HTTP middleware.

Disclaimer: This blog post is automatically generated from project documentation and technical proposals using AI assistance. The content represents our development journey and architectural decisions. Code examples are simplified illustrations and may not reflect the exact production implementation.

Introduction

When Claude, Caroline, and I looked across the codebase we saw repeated authorization checks implemented differently in each service. That made policy reviews time-consuming and audits error-prone. We wanted a single, testable source of truth for authorization, but also the low latency required by our real-time APIs.

OPA compiled to WASM gave us both: Rego for policy authoring and WASM for shipping a compact, efficient runtime inside each service process. Below we summarize what we chose, why, and how we built the initial prototype.

Why this matters

Centralizing policies in Rego improves reviewability and testing, but remote enforcement adds network latency and operational complexity. Compiling policies to WASM and evaluating them locally provides near-zero enforcement latency and removes a runtime dependency on a remote PDP. It also simplifies auditing: each artifact is versioned and produced by CI so we can trace decisions back to policy commits.

Architecture

At a high level the architecture looks like this:

graph LR
	Client --> API[Resource Server]
	API -->|evaluate| WASM[OPA WASM runtime]
	API --> Metrics[Observability]
	CI -->|build| Artifacts[Policy Bundles]
	Artifacts -->|publish| Registry[Artifact Store]
	API -->|fetch| Registry

The Resource Server loads a policy bundle at startup or via hot-reload, evaluates a compact input (http, user claims, resource), and receives a structured decision (allow, status, context). Denies are logged and metricized.

Implementation highlights

  • Policy CI: opa fmt, opa test, opa build -t wasm are run in CI. The resulting policy.bundle.tar.gz contains bundle.wasm and metadata and is published to our artifact registry.
  • Docker: The final Node.js image includes the selected policy bundle under /app/policies/{name}/{version}. We prefer building policies in CI and copying artifacts into the runtime image to keep images lean.
  • Middleware: A small opa-wasm-loader exposes evaluate(input) returning the policy result. We wire this into preHandler (Fastify) or a Go http.Handler wrapper.

Example flow

  1. A client requests GET /matches/123.
  2. Middleware constructs input from request path, method, headers and request.user claims.
  3. The WASM evaluator runs the policy and returns { allow: true } or { allow: false, status: { code: 403, message: 'forbidden' } }.
  4. Middleware enforces the result and records metrics.

Running and testing policies locally

To author and validate policies locally:

# format and run unit tests
opa fmt -w ops/policies
opa test ./ops/policies -v

# build the WASM bundle
opa build -t wasm -e policy/main ./ops/policies -o ./ops/policies/bundles/policy.bundle.tar.gz

# quick smoke eval (optional)
echo '{"input":{"http":{"method":"GET"}}}' | opa eval --format=json -d ./ops/policies/bundles/policy.bundle.tar.gz 'data.policy.main.allow'

CI should run the same steps and refuse to publish artifacts unless tests pass and a smoke evaluation succeeds.

Key learnings so far

  • Keep policy inputs small and deterministic — large inputs hurt performance and complicate testing.
  • Run opa test frequently; policy regressions are often subtle and tests catch them early.
  • Enforce resource limits (time/memory) in the WASM runtime to protect services from expensive decisions.

Next steps

  • Build the opa-wasm-loader helper and publish a small example service in the repo.
  • Add a GitHub Actions workflow that runs opa test and publishes the bundle to the artifact registry.
  • Pilot in staging in advisory mode and iterate on policies and input shapes.

If you want to try this locally, run the opa commands above and then start your service with a policy bundle in /app/policies.

Why this matters

Our codebase had authorization checks scattered across services. That made policies hard to review, test and evolve. By authoring policies in Rego, building them into WASM artifacts, and loading them directly inside the service process, we get:

  • Reusable, testable policy logic (Rego) with unit tests.
  • Low-latency, offline enforcement without a remote PDP call.
  • Clear separation of policy from business code and standardized deployment.

High-level approach

  1. Author policies in ops/policies/.
  2. Run opa test in CI; build the WASM bundle when tests pass.
  3. Publish policy bundles as artifacts and copy them into service Docker images (multi-stage build) or mount them at runtime.
  4. Middleware in Node.js and Go evaluates policies per-request using a structured input and enforces allow/deny.

What we built (proposal)

The proposal covers:

  • A lightweight opa-wasm-loader helper for Node.js and a Go evaluator.
  • Middleware examples (Fastify and net/http).
  • CI pipeline to format, test, build and publish WASM bundles.
  • Docker integration: copy policy.bundle.tar.gz into final images in the multi-stage Node.js build.
  • Observability, hot-reload, and rollout guidance (advisory -> canary -> enforce).

How to run policy tests locally

Install OPA (https://openpolicyagent.org). Then from the repo root:

opa fmt -w ops/policies
opa test ./ops/policies -v

Build the WASM bundle:

opa build -t wasm -e policy/main ./ops/policies -o ./ops/policies/bundles/policy.bundle.tar.gz

Add this to your CI to fail fast on policy errors and publish validated bundles for deployment.

Next steps

  • Implement the opa-wasm-loader helpers and a small example service.
  • Add GitHub Actions job to run opa test and build/publish bundles.
  • Start with an admin-policy pilot and run in advisory (log-only) mode in staging.

If you’re interested in helping with policy authoring or the Node.js loader, ping the Platform team — we’ll add contributor docs and examples soon.