Enforcer
Disclosure first: we build Enforcer, so we have an obvious bias. We have tried to write the comparison we wanted when we were choosing, which means naming the cases where something else is the better answer. Everything here is from public documentation, and we have said where we could not confirm something rather than guessing.
Where does the check run, and what do you have to keep in sync? Almost every difference that will annoy you in month three comes from those two.
| Tool | Model | You must sync your data into it | Returns a reason | Also handles sign-in |
|---|---|---|---|---|
| Open Policy Agent | Policy engine, Rego | Yes, via bundles or push | Partially, with decision logs | No |
| Cerbos | Policy engine, YAML | No, you pass context per request | Yes | No |
| OpenFGA / SpiceDB | Relationship, Zanzibar | Yes, dual writes | Limited | No |
| WorkOS FGA | Relationship + roles | Yes, resources registered in their system | Not documented per endpoint | Yes, via AuthKit |
| Permit.io | Policy + MCP gateway | Yes | Yes | No |
| Oso Cloud | Policy, Polar | Yes, facts | Yes | No |
| Arcade.dev | Actions runtime + tool auth | n/a, it runs the tools | n/a | Yes, tool OAuth |
| Enforcer | Policy over a resolved caller | No, it already holds identity | Yes, with the policy id | Yes |
Use a relationship engine (OpenFGA, SpiceDB, WorkOS FGA) when the core problem is a sharing graph. Any user shares any object with any other, arbitrary nesting, and you need to list every document a person can see. That is what Zanzibar was designed for and we would not pretend otherwise. The cost you accept is keeping a copy of your data in the permissions store.
Use Cerbos when you want a stateless policy engine you can run yourself next to your service, and you are happy passing the context in on every request. It is the simplest thing that works and has the shortest quickstart in the category, three commands and no account.
Use OPA when you already run it for infrastructure policy and want one engine. Be aware that Rego is the most complained-about part of the category.
Use Arcade when your problem is really tool authentication, holding OAuth tokens for the services an agent acts on, rather than deciding whether an action is permitted.
Use Enforcer when the question is whether this person, in this tenant, with this role and this verification status, may take this action within these limits, and you need to prove afterwards why it was allowed. Particularly where the answer depends on something the person earned, like passing an identity check.
Most of the products above are a decision layer. You hand them a subject id and they answer true or false, which means they need a copy of your data to answer at all. Enforcer signed the user in, so a policy is handed the resolved caller with role, tenant and group memberships already attached. Nothing to sync, nothing to drift.
It also means a group can be earned rather than assigned. Someone passes an identity check and lands in a group, and that group is what the rule reads, so "completed verification" and "allowed to move money" are the same recorded fact rather than two systems kept in step. That is also why a denial can say what would make it an allow, instead of returning false.
We do not do list filtering. If you need "return every document this user can see" as a query, a relationship engine will serve you better today. That is the most common gap people hit with Cerbos too, and it is honest to say we share it.