Open source · Apache-2.0 · TypeScript, zero-dependency core

Authorization, decided in microseconds.

Verdict compiles your RBAC, ABAC, and ReBAC policies once, then decides in-process — on Cloudflare Workers, Lambda, Node, Bun, Deno, or a container. Same bundle, byte-identical decisions, everywhere.

verdict.explain(request)
// who → what → may they?
{ principal: { id: "mgr-7", roles: ["employee"] },
  resource:  { kind: "leave", attr: { managerId: "mgr-7" } },
  actions:   ["approve"] }
blocked-countries (deny)CONDITION_FALSE
hr-fullROLES_MISS
manager-approvesMATCHED
ALLOWrule manager-approves · 12 µs · bundle v7
12 µsmean warm check, 100-policy bundle¹
9.6 msbatch of 1,000 decisions¹
30 µscold bundle load — no parsing at runtime¹
denythe default, always — no rule, no access

Deployment

Three modes. One policy bundle.

Policies compile to an immutable, content-hashed, Ed25519-signed bundle. Where that bundle runs is a deployment choice, not a rewrite.

Mode 1 — Embedded

Inside your app

Worker / Lambda / process
 ├─ @gomagentic/verdict-engine
 ├─ compiled bundle
 └─ check() → µs

Zero network calls on the decision path. Cold start is one deserialize — the bundle is pre-compiled bytecode, not source.

Mode 2 — Remote PDP

A central service

any language
   │  POST /v1/check
   ▼
Verdict PDP ──► Postgres
(Hono: runs anywhere)

One HTTP call from Go, Python, Java, anything. Policy CRUD, publish, rollback, audit, and keys included.

Mode 3 — Hybrid

Local + synced

edge worker
 ├─ SyncedVerdict → µs
 └─ background sync
     ETag · SSE · queues
     verify · never block

Decide locally, sync bundles from the control plane. Signature checks, replay protection, and fail-static or fail-closed staleness.

Policy language

Policies that read like the rule you meant.

VPL is a small, typed policy language — or write the same policies as JSON/YAML. Both compile to the same optimized bytecode with constant folding, rule indexing, and a hard instruction budget.

  • RBAC — roles, role groups, wildcards and globs
  • ABAC — typed conditions over user, resource, and request context
  • ReBACrelated(user.id, "editor", resource.id) against your relationship graph
  • Deny overrides — a matching deny beats every allow, across all policies
  • Sandboxed — no eval, bounded execution, errors can never grant
derived_role "leave_owner" {
  parent_roles = ["employee"]
  condition    = user.id == resource.attr.ownerId
}

resource "leave" {
  deny "blocked-countries" {
    actions   = ["*"]
    condition = user.country in ["KP", "IR"]
  }
  allow "hr-full"     { roles = ["hr"] actions = ["*"] }
  allow "self-approve" {
    actions   = ["approve"]
    condition = user.id == resource.attr.ownerId
             && resource.attr.days <= 2
             && context.time.hour >= 9
  }
}
// "why was this denied?" is a first-class answer
const d = verdict.explain(request);

d.results.approve.reason   // "MISSING_ATTRIBUTES"
d.trace.missingAttributes  // ["user.attr.level"]
d.trace.rules
// hr-full          ROLES_MISS
// manager-approves CONDITION_FALSE
// self-approve     CONDITION_UNKNOWN ← the gap

Explainability

Every decision can explain itself.

The trace is produced by the evaluator as it runs — not reconstructed afterwards. Matched policy, matched rule, every condition outcome, and exactly which attributes were missing. Denials become debuggable; audits become answerable.

  • Three-valued logic — a missing attribute is unknown, never a crash, never an accidental allow
  • Simulation — dry-run drafts against real requests before publishing
  • Audit trail — who, what, decision, rule, latency; written async, never blocking

Batteries included

The whole control plane, not just an evaluator.

Versioning & rollback

Publishing freezes immutable versions; rollback republishes old content as a new version. History never rewrites — the database enforces it with triggers.

Signed bundles

Content-hashed, Ed25519-signed artifacts with replay protection. Decision points refuse what they can't verify.

Multi-tenant

Per-tenant policies and bundles, shared ~global policies with tenant overrides — merged at build time, so tenants pay zero cost per decision.

Auth that ships

Scoped API keys (hashed, revocable) and OIDC/JWT with JWKS caching. Per-identity rate limiting built in.

Console & dashboard

Self-serve accounts and projects, then a full editor: validate with line-precise diagnostics, simulate with traces, publish, roll back, read the audit log.

Storage adapters

Postgres, SQLite, Cloudflare D1, R2/S3-compatible object storage, filesystem, or in-memory — behind one interface.

Get started

Two ways in. Both short.

Create a project in the console — sign in, name it, and you have a published policy, an API key, and a playground in about thirty seconds. Or skip the server entirely: embedded mode is a library call.

The console is a static page pointed at your server. There's no Verdict-hosted backend — accounts, projects, and policies live in your own deployment.

$ npm install @gomagentic/verdict-engine @gomagentic/verdict-dsl

import { Verdict } from "@gomagentic/verdict-engine";
import { allows } from "@gomagentic/verdict-core";

const verdict = await Verdict.fromBundle(bundle);
allows(verdict.check(request), "approve");  // → true, in ~12 µs