Skip to content

Enforcing Decisions & Querying

Once your policy is published and your backend is supplying facts, your application asks Verdict Server two kinds of authorization questions:

  • Point checks (authorize): Can this specific actor perform this specific action on this specific resource right now?
  • Predicate queries (query): Which resources, actions, or actors satisfy a predicate pattern?

This guide shows how to enforce point checks in handlers, evaluate open-ended queries with wildcards, and keep your handler checks consistent with policy queries.

In a request handler or route middleware, first authenticate the caller using your application’s session or token layer, then pass the typed actor identity, action string, and target resource identity to client.authorize(actor, action, resource) (POST /v1/authorize):

import { identity } from "@useverdict/client";
async function updateDocumentHandler(
currentUserId: string,
documentId: string,
body: { content: string },
): Promise<Response> {
const allowed = await client.authorize(
identity("User", currentUserId),
"edit",
identity("Document", documentId),
);
if (!allowed) {
return new Response("Forbidden", { status: 403 });
}
const updated = await db.documents.update(documentId, body);
return Response.json(updated);
}

client.authorize grants access when allow(actor, action, resource) matches and deny(actor, action, resource) does not match against your active policy and stored facts:

  • Policy denials return false: When no rule proves allow, or a deny matches, client.authorize resolves to false (and over REST, POST /v1/authorize returns HTTP 200 OK with {"allowed": false}).
  • Operational failures throw errors: A rejected promise (or a non-200 HTTP status over REST) indicates a transport failure, an invalid API key (401), a missing policy (409), or a malformed argument (400), rather than a normal authorization denial. Let operational errors surface to your application’s error handler and telemetry rather than silently treating every exception as a 403 Forbidden.

The examples below query allow directly. These queries report grants independently of deny rules. If your policy defines deny, check each candidate with client.authorize before treating it as an effective permission.

Point checks work well when the request URL already identifies a single target resource, such as GET /documents/handbook. When building index pages, search filters, or UI permission maps, your backend instead needs to ask open-ended questions across unbound variables.

Use client.query (POST /v1/query) to evaluate any predicate in your policy with wildcards in the positions you want Verdict to solve for. Each returned row is a tuple containing the bound values for the wildcard positions in the exact order they appeared in the request.

Pass { wildcard: { type: "Document" } } in the resource position of allow to find every Document identity that User:alice is allowed to "read":

const rows = await client.query([
"allow",
identity("User", "alice"),
"read",
{ wildcard: { type: "Document" } },
]);
// Each row has 1 element corresponding to the single wildcard position
const readableIds = rows.map(([doc]) => (typeof doc === "object" ? doc.id : String(doc)));
const documents = await db.documents.findManyByIds(readableIds);

Using a typed wildcard ({ wildcard: { type: "Document" } }) instead of an untyped null wildcard restricts the search to Document identities, even if your policy also grants "read" on Folder or Organization identities.

To determine which UI controls (such as an “Edit” or “Share” button) to enable for a user viewing Document:handbook, place null in the action position while binding the actor and resource:

const rows = await client.query([
"allow",
identity("User", "alice"),
null,
identity("Document", "handbook"),
]);
// e.g. ["read", "edit"]
const permittedActions = rows.map(([action]) => String(action));

client.query enumerates a wildcard position when the matching policy rules bind that position to concrete constants or stored facts (such as "read" and "edit"). An unconstrained head-only variable (such as allow(actor, action, resource) if is_admin(actor);) matches concrete authorize checks, but cannot enumerate arbitrary unbound action values in query.

client.query is not limited to allow. You can query any rule or fact predicate in your policy (such as has_permission, has_role, or is_ancestor) and include multiple wildcards in the same call:

// Find every (User, role) pair on Document:handbook, including inherited roles
const roleRows = await client.query([
"has_role",
{ wildcard: { type: "User" } },
null,
identity("Document", "handbook"),
]);
// Each row has 2 elements matching the 2 wildcards: [userIdentity, roleString]
for (const [user, role] of roleRows) {
console.log(user, role);
}

When integrating Verdict into an existing backend, it is tempting to keep certain quick checks inside application code, such as short-circuiting client.authorize in a read handler when a database column document.isPublic is true:

// Watch out: Verdict does not know which documents are public
if (document.isPublic || (await client.authorize(user, "read", docIdentity))) {
return Response.json(document);
}

While that if statement works for single-document requests, client.query can only derive answers from facts and rules known to Verdict Server. If is_public is checked only in TypeScript and never stored in Verdict, calling client.query(["allow", user, "read", { wildcard: { type: "Document" } }]) will omit public documents.

To keep point checks (authorize) and resource queries (query) aligned, choose one boundary intentionally:

  1. Model the condition in your .vd policy: Synchronize is_public(Document) as a fact and add an allow rule so both authorize and query include public documents automatically:
declare has_role(User, string, Organization);
declare belongs_to(Document, Organization);
declare is_owner(User, Document);
declare is_public(Document);
allow(user: User, "read", document: Document) if
is_owner(user, document);
allow(user: User, "read", document: Document) if
belongs_to(document, org: Organization) and
has_role(user, "member", org) and
is_public(document);
  1. Combine filters explicitly at the database layer: If you intentionally keep public visibility purely in your application database (for example, when there are millions of anonymous public posts), query Verdict for the user’s private accessible document IDs and combine them with an OR is_public = true filter in your SQL query.