Skip to main content

Cerbos vs Permit.io vs OPA: Authorization as a Service (2026)

·PkgPulse Team

TL;DR

Cerbos is the open-source authorization engine — policy-as-code in YAML, self-hosted, decoupled from your app, supports RBAC/ABAC/PBAC, fast local evaluation. Permit.io is the authorization-as-a-service platform — visual policy editor, SDKs, RBAC/ABAC/ReBAC, audit logs, no infrastructure to manage. OPA (Open Policy Agent) is the general-purpose policy engine — Rego policy language, works for API authorization, Kubernetes, infrastructure, the CNCF standard. In 2026: Cerbos for self-hosted policy-as-code authorization, Permit.io for managed authorization with a UI, OPA for general-purpose policy enforcement.

Key Takeaways

  • Cerbos: Open-source — YAML policies, self-hosted, RBAC/ABAC, sidecar pattern
  • Permit.io: SaaS — visual editor, SDKs, RBAC/ABAC/ReBAC, audit logs
  • OPA: Open-source — Rego language, general-purpose, CNCF graduated, Kubernetes-native
  • Cerbos is purpose-built for application authorization
  • Permit.io requires no infrastructure management
  • OPA is the most versatile (API, K8s, Terraform, data filtering)

Cerbos

Cerbos — policy-as-code authorization:

Define policies

# policies/resource_package.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  resource: "package"
  version: "default"
  rules:
    # Anyone can read published packages:
    - actions: ["read", "list"]
      effect: EFFECT_ALLOW
      roles: ["*"]
      condition:
        match:
          expr: request.resource.attr.status == "published"

    # Authors can update their own packages:
    - actions: ["update"]
      effect: EFFECT_ALLOW
      roles: ["author"]
      condition:
        match:
          expr: request.resource.attr.ownerId == request.principal.id

    # Admins can do everything:
    - actions: ["*"]
      effect: EFFECT_ALLOW
      roles: ["admin"]

    # Authors can delete their own draft packages:
    - actions: ["delete"]
      effect: EFFECT_ALLOW
      roles: ["author"]
      condition:
        match:
          all:
            of:
              - expr: request.resource.attr.ownerId == request.principal.id
              - expr: request.resource.attr.status == "draft"

Node.js SDK

import { GRPC as Cerbos } from "@cerbos/grpc"

const cerbos = new Cerbos("localhost:3593")

// Check permission:
const decision = await cerbos.checkResource({
  principal: {
    id: "user-123",
    roles: ["author"],
    attr: { department: "engineering" },
  },
  resource: {
    kind: "package",
    id: "pkg-456",
    attr: {
      ownerId: "user-123",
      status: "published",
    },
  },
  actions: ["read", "update", "delete"],
})

console.log(decision.isAllowed("read"))    // true
console.log(decision.isAllowed("update"))  // true (owner)
console.log(decision.isAllowed("delete"))  // false (published)

Express middleware

import { GRPC as Cerbos } from "@cerbos/grpc"
import express from "express"

const cerbos = new Cerbos("localhost:3593")
const app = express()

// Authorization middleware:
function authorize(resourceKind: string, action: string) {
  return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
    const decision = await cerbos.checkResource({
      principal: {
        id: req.user!.id,
        roles: req.user!.roles,
        attr: { department: req.user!.department },
      },
      resource: {
        kind: resourceKind,
        id: req.params.id || "new",
        attr: req.resourceAttrs || {},
      },
      actions: [action],
    })

    if (!decision.isAllowed(action)) {
      return res.status(403).json({ error: "Forbidden" })
    }

    next()
  }
}

// Usage:
app.get("/packages/:id", authorize("package", "read"), getPackage)
app.put("/packages/:id", authorize("package", "update"), updatePackage)
app.delete("/packages/:id", authorize("package", "delete"), deletePackage)

Permit.io

Permit.io — authorization as a service:

SDK setup

import { Permit } from "permitio"

const permit = new Permit({
  token: process.env.PERMIT_API_KEY!,
  pdp: "https://cloudpdp.api.permit.io",  // Or self-hosted PDP
})

Permission checks

import { Permit } from "permitio"

const permit = new Permit({ token: process.env.PERMIT_API_KEY! })

// Simple check:
const allowed = await permit.check("user-123", "update", "package")
console.log(allowed)  // true or false

// With resource instance:
const canEdit = await permit.check(
  "user-123",           // Who
  "update",             // Action
  {                     // Resource
    type: "package",
    key: "pkg-456",
    tenant: "org-789",
  }
)

// With context:
const canDelete = await permit.check("user-123", "delete", {
  type: "package",
  key: "pkg-456",
  attributes: {
    status: "draft",
    ownerId: "user-123",
  },
})

Express middleware

import { Permit } from "permitio"
import express from "express"

const permit = new Permit({ token: process.env.PERMIT_API_KEY! })
const app = express()

// Middleware:
function authorize(action: string, resourceType: string) {
  return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
    const allowed = await permit.check(
      req.user!.id,
      action,
      {
        type: resourceType,
        key: req.params.id,
        tenant: req.user!.orgId,
      }
    )

    if (!allowed) {
      return res.status(403).json({ error: "Forbidden" })
    }

    next()
  }
}

app.get("/packages/:id", authorize("read", "package"), getPackage)
app.put("/packages/:id", authorize("update", "package"), updatePackage)

Sync users and roles

import { Permit } from "permitio"

const permit = new Permit({ token: process.env.PERMIT_API_KEY! })

// Sync user:
await permit.api.syncUser({
  key: "user-123",
  email: "royce@example.com",
  first_name: "Royce",
  attributes: { department: "engineering" },
})

// Assign role:
await permit.api.assignRole({
  user: "user-123",
  role: "admin",
  tenant: "org-789",
})

// Create resource relationship:
await permit.api.createRelationshipTuple({
  subject: "user:user-123",
  relation: "owner",
  object: "package:pkg-456",
})

OPA (Open Policy Agent)

OPA — general-purpose policy engine:

Define policies (Rego)

# policies/authz.rego
package authz

import rego.v1

default allow := false

# Anyone can read published packages:
allow if {
  input.action == "read"
  input.resource.status == "published"
}

# Authors can update their own packages:
allow if {
  input.action == "update"
  input.user.roles[_] == "author"
  input.resource.ownerId == input.user.id
}

# Admins can do anything:
allow if {
  input.user.roles[_] == "admin"
}

# Authors can delete their own drafts:
allow if {
  input.action == "delete"
  input.user.roles[_] == "author"
  input.resource.ownerId == input.user.id
  input.resource.status == "draft"
}

Query OPA from Node.js

// Query OPA server:
async function checkPermission(
  user: { id: string; roles: string[] },
  action: string,
  resource: { id: string; ownerId: string; status: string }
): Promise<boolean> {
  const response = await fetch("http://localhost:8181/v1/data/authz/allow", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      input: { user, action, resource },
    }),
  })

  const data = await response.json()
  return data.result === true
}

// Usage:
const allowed = await checkPermission(
  { id: "user-123", roles: ["author"] },
  "update",
  { id: "pkg-456", ownerId: "user-123", status: "published" }
)

WASM (embedded in Node.js)

import { loadPolicy } from "@open-policy-agent/opa-wasm"
import fs from "fs"

// Load compiled policy:
const policyWasm = fs.readFileSync("./policy.wasm")
const policy = await loadPolicy(policyWasm)

// Evaluate locally (no network call):
const result = policy.evaluate({
  user: { id: "user-123", roles: ["author"] },
  action: "update",
  resource: { id: "pkg-456", ownerId: "user-123", status: "published" },
})

console.log(result[0]?.result)  // { allow: true }

Express middleware

import express from "express"

const app = express()

function authorize(action: string) {
  return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
    const response = await fetch("http://localhost:8181/v1/data/authz/allow", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        input: {
          user: { id: req.user!.id, roles: req.user!.roles },
          action,
          resource: req.resourceAttrs || {},
        },
      }),
    })

    const { result } = await response.json()

    if (!result) {
      return res.status(403).json({ error: "Forbidden" })
    }

    next()
  }
}

app.put("/packages/:id", authorize("update"), updatePackage)

Feature Comparison

FeatureCerbosPermit.ioOPA
TypeOpen-source engineSaaS platformOpen-source engine
Policy languageYAMLVisual editor + APIRego
Self-hosted✅ (PDP)
Cloud offeringCerbos Hub✅ (primary)Styra DAS
RBAC✅ (manual)
ABAC
ReBAC✅ (manual)
Audit logs
Visual editor
WASM evaluation
Multi-tenant✅ (manual)
Node.js SDK
Kubernetes✅ (sidecar)✅ (sidecar)✅ (native)
Use caseApp authorizationApp authorizationGeneral-purpose policy
Learning curveLowLowHigh (Rego)

When to Use Each

Use Cerbos if:

  • Want open-source, self-hosted authorization
  • Prefer YAML policy-as-code (version controlled)
  • Need fast local policy evaluation (sidecar pattern)
  • Building microservices that need decoupled authorization

Use Permit.io if:

  • Want managed authorization with a visual policy editor
  • Need RBAC + ABAC + ReBAC without writing policy code
  • Want audit logs and compliance features out of the box
  • Building multi-tenant SaaS applications

Use OPA if:

  • Need general-purpose policy enforcement (not just app authz)
  • Also need Kubernetes admission control or Terraform policy
  • Want WASM-compiled policies for embedded evaluation
  • Already using CNCF tooling and want ecosystem integration

Methodology

Feature comparison based on Cerbos v0.35.x, Permit.io SDK v2.x, and OPA v0.68.x as of March 2026.

Compare security and authorization libraries on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.