Skip to content

Designing Policies

Verdict lets you model access directly from the relationships and state your application already tracks (such as who owns a document, which team a user belongs to, or whether a record is archived) without forcing your product into a single rigid paradigm.

When your backend calls authorize, Verdict Server checks that allow(actor, action, resource) evaluates to true and no matching deny(actor, action, resource) holds.

For simple access rules, you can write allow rules directly over your domain facts:

declare is_owner(User, Document);
allow(user: User, "read", document: Document) if
is_owner(user, document);

If no rule proves allow, authorization is denied by default.

Most policies model access entirely through positive allow rules and helper predicates. However, when you need a blanket restriction that overrides any grant (such as suspending an account or freezing a compromised credential), you can write an optional deny rule:

declare is_suspended(User);
deny(user: User, action, resource) if
is_suspended(user);

A matching deny rule eliminates any matching allow, regardless of which rules or roles proved access. Position deny as an optional tool: keep the bodies of deny rules simple, and rely on composing positive paths through allow and helper predicates for your standard access logic.

See Predicates, rules, and facts for how facts and rules evaluate.

As your application grows, you can combine multiple patterns inside the same .vd policy:

PatternUse it to expressGuide
PermissionsDirect action grants, a shared has_permission layer, and universal guardsPermissions
RolesResponsibilities scoped to organizations or resourcesRoles
RelationshipsOwnership, organization membership, containment, and sharingRelationships
HierarchiesAccess inherited through nested folders or user groupsHierarchies & Recursion
AttributesConditions on actor and resource propertiesAttributes

Most policies start by establishing a permission layer, which gives roles, relationships, and direct grants a single target predicate (has_permission) before forwarding to allow.

While you can name predicates anything in your domain (is_owner, belongs_to), conventional 3-term predicates are useful when you want to query or delete related facts with wildcards:

PredicateWildcard example
has_permission(actor, action, resource)has_permission(User:alice, ?action, Document:handbook)
has_role(actor, role, resource)has_role(User:alice, ?role, Organization:acme)
has_relation(resource, relation, related)has_relation(Document:handbook, ?rel, _)
has_attribute(entity, key, value)has_attribute(Document:handbook, ?key, ?val)

Putting the varying action, role, relation, or key in the middle slot lets your application list all roles a user holds or revoke all of a user’s roles in an organization (has_role(User:alice, _, Organization:acme)) in a single call instead of querying separate predicates like is_viewer and is_editor.

Dedicated predicates like is_owner or parent remain a good choice when they state a relationship more directly.