Designing a ReBAC System with AuthZEN and OPA

May 6, 2026
Walter Manger
3 minute read

      Most authorization systems answer “what role does this user have?” The problem is roles don’t capture relationships. The question isn’t “is this user an admin?” — it’s “does this user have a relationship to the thing they’re trying to access?”

      That’s ReBAC: authorization through relationships.

      I’m building a system that answers the question: does user X have can_write on resource Y? Here’s how it’s designed.

      The Tuple Store

      The entire relationship graph lives in a single table:

      tuples(subject_type, subject_id, relation, object_type, object_id)
      

      A user being an owner of a business is a tuple. A business owning a resource is a tuple. Everything is a relationship.

      To answer “can user X write resource Y?”, the system walks the graph:

      1. Find which business owns resource Y → business:M owns resource:Y
      2. Check if user X has a role in business M that permits can_writeuser:X is owner of business:M
      3. If yes → allow. If no → deny with a reason.

      The Denial Reason

      The denial reason is what makes this system genuinely useful. Instead of a flat 403, the system explains:

      User X does not have can_write on resource Y — user X is not an owner of business M, and business M owns resource Y.

      The denial traverses the same graph as the allow path and reports where the chain breaks. Invaluable for debugging access issues in production.

      AuthZEN as the PDP Contract

      The system exposes an AuthZEN-compliant API. A request looks like:

      {
        "subject": { "type": "user", "id": "user-x" },
        "action": { "name": "can_write" },
        "resource": { "type": "document", "id": "resource-y" }
      }
      

      And the response on denial:

      {
        "decision": false,
        "context": {
          "reason": "user-x is not an owner of business-m, which owns resource-y"
        }
      }
      

      AuthZEN gives the PDP a standard contract — the enforcement layer doesn’t need to know anything about how decisions are made.

      OPA as the PEP — with a Custom Go Builtin

      OPA sits in the request path as the enforcement layer. Rather than calling the AuthZEN PDP over HTTP with http.send, OPA uses a custom builtin written in Go that queries the tuple store directly and returns a structured decision:

      allow if {
        input.request.method == "PUT"
        ["v1", "todos", todo_id] = input.request.path
        decision := rebac.check(input.user, "can_write", "todos", todo_id)
        decision.allow == true
      }
      
      deny_reason := reason {
        input.request.method == "PUT"
        ["v1", "todos", todo_id] = input.request.path
        decision := rebac.check(input.user, "can_write", "todos", todo_id)
        decision.allow == false
        reason := decision.reason
      }
      

      The builtin handles the tuple traversal in Go, which means we skip OPA’s built-in caching (not available for custom builtins) and manage our own with Redis. Tuple lookups are cached with short TTLs — fast enough to not matter in the hot path, and invalidated when tuples change.

      The PAP — and Why It’s MCP-Accessible

      The Policy Administration Point is where tuples are created and managed. Engineers use it to grant access, audit relationships, and debug permission issues.

      Both the PAP and the PDP are exposed as internal MCP servers. That means any MCP-capable agent — including Claude — can interact with them directly. An engineer can ask: “why can’t user X access resource Y?” and get the traversal result back in plain English. Or: “grant user X access to business M” and have the tuple written without touching a UI.

      It makes the authorization layer a first-class participant in agentic workflows.

      Why This Way

      One table keeps the schema simple and the query logic in one place. AuthZEN gives the API a standard shape that any PEP can call. The custom Go builtin keeps OPA’s enforcement tight — no HTTP round trips in the policy path. And making both the PAP and PDP available over MCP turns what is usually a black box into something engineers can actually reason about, with or without a UI.

      More on the implementation soon.