Skip to content

Policy Fundamentals

Verdict replaces scattered authorization checks in application code with a declarative set of rules in one place. A file containing these rules is called a policy and is written in the Verdict language (.vd).

The core building block of a Verdict policy is the predicate:

// Examples of predicates
allow(user, action, resource)
has_role(user, role, organization)
is_owner(user, document)
is_archived(document)

Although a predicate looks like a function call in imperative languages, it represents a logical statement that is true or false depending on the terms inside.

Every predicate has a snake_case name followed by 1 to 3 terms in parentheses. Aside from allow, predicate names are up to you and use your application’s domain vocabulary.

Each slot inside a predicate holds a term:

// predicate(term1, term2, term3)
// [name] [term] [term] [term]

A term can be a concrete value or a variable:

  • Identities (TypeName:id) reference specific application entities such as User:alice, Document:handbook, or Organization:acme. The type name starts with an uppercase letter (PascalCase), and the ID after the colon is treated as a string key. Quote the ID if it contains characters like @ or - (for example, User:"alice@example.com").
  • Constants are primitive values: strings ("read"), integers (32), or booleans (true, false).
  • Variables are lowercase snake_case names (user, document, org) that act as placeholders matching concrete values when Verdict evaluates a policy.
// has_role(User:alice, "member", org)
// identity string variable
// min_clearance(Document:handbook, 2)
// identity int
// is_public(Document:handbook, false)
// identity boolean

Because an identity includes its type, User:alice and Organization:alice are distinct values and never match each other.

Verdict separates your application’s changing authorization data (facts) from your access control logic (rules).

A fact is a predicate where every term is a concrete value (also called fully bound):

is_owner(User:alice, Document:handbook);
has_role(User:alice, "member", Organization:acme);
is_archived(Document:handbook);

Facts represent relationships and attributes from your application, such as who owns a document or who belongs to an organization. You can write concrete facts directly in a .vd file for self-contained examples and tests, or supply live application facts at runtime through Verdict Server or Verdict Postgres.

Write declare declarations to document the shapes of facts your policy expects:

declare is_owner(User, Document);
declare has_role(User, string, Organization);
declare is_archived(Document);

Each position in a declare declaration names the expected term type: an identity type (User, Document, Organization) or a primitive type (string, int, boolean). Declaring your input facts lets verdict validate and editor tooling catch typos in predicate names and arity mismatches across your rules. See fact type declarations for the supported forms.

A rule defines when a predicate is true based on other predicates. It has a head (the conclusion before if) and a body (the conditions after if):

// If user is the owner of document, allow user to "read" document
allow(user, "read", document) if
is_owner(user, document);

Variables in a rule can include a type qualifier (variable: TypeName) to restrict which values they can match:

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

Notice how casing before the colon distinguishes a concrete identity from a qualified variable:

  • User:alice (uppercase on the left) is one specific identity: the User with ID "alice".
  • user: User (lowercase on the left) is a variable named user that matches any identity of type User.

Unlike an imperative chain of if statements that runs top to bottom, rules in Verdict are declarative and order-independent. Verdict searches across all rules in the policy and uses whichever rules match the question being asked.

The entry point for authorization checks is the allow predicate:

allow(actor, action, resource)

When your application calls authorize, it supplies concrete values for all three positions: the actor making the request, the action being performed, and the target resource.

Suppose your policy contains this rule:

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

When your application checks whether User:alice can "read" Document:handbook, Verdict evaluates allow(User:alice, "read", Document:handbook):

// 1. Match the query against the rule head:
// user = User:alice, action = "read", document = Document:handbook
// 2. Substitute those values into the rule:
allow(User:alice, "read", Document:handbook) if
is_owner(User:alice, Document:handbook);
// 3. Check whether is_owner(User:alice, Document:handbook) is true

If is_owner(User:alice, Document:handbook) is present, the rule’s condition is satisfied and Verdict returns true (allowed).

Authorization in Verdict is denied by default. If no rule can prove allow(actor, action, resource), the decision is false (denied).