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.
Enforcing point checks
Section titled “Enforcing point checks”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 provesallow, or adenymatches,client.authorizeresolves tofalse(and over REST,POST /v1/authorizereturns HTTP200 OKwith{"allowed": false}). - Operational failures throw errors: A rejected promise (or a non-
200HTTP 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 a403 Forbidden.
Querying accessible resources and actions
Section titled “Querying accessible resources and actions”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.
Listing resources an actor can access
Section titled “Listing resources an actor can access”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 positionconst 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.
Listing permitted actions on a resource
Section titled “Listing permitted actions on a resource”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.
Querying intermediate predicates
Section titled “Querying intermediate predicates”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 rolesconst 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);}Aligning handler checks with queries
Section titled “Aligning handler checks with queries”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 publicif (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:
- Model the condition in your
.vdpolicy: Synchronizeis_public(Document)as a fact and add anallowrule so bothauthorizeandqueryinclude 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);- 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 = truefilter in your SQL query.
Next steps
Section titled “Next steps”- Review how to synchronize domain facts and replace values atomically in Managing Facts.
- Explore
VerdictClient,PatternInput, and Sessions, consistency, and retries in the TypeScript client reference. - See the HTTP request and response schemas for Authorize and Query in the REST API reference.