Cerbos vs Permit.io vs OPA: Authorization as a Service (2026)
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
| Feature | Cerbos | Permit.io | OPA |
|---|---|---|---|
| Type | Open-source engine | SaaS platform | Open-source engine |
| Policy language | YAML | Visual editor + API | Rego |
| Self-hosted | ✅ | ✅ (PDP) | ✅ |
| Cloud offering | Cerbos 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 case | App authorization | App authorization | General-purpose policy |
| Learning curve | Low | Low | High (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.