Open source · Apache-2.0 · TypeScript, zero-dependency core
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.
// who → what → may they? { principal: { id: "mgr-7", roles: ["employee"] }, resource: { kind: "leave", attr: { managerId: "mgr-7" } }, actions: ["approve"] }
Deployment
Policies compile to an immutable, content-hashed, Ed25519-signed bundle. Where that bundle runs is a deployment choice, not a rewrite.
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.
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.
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
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.
related(user.id, "editor", resource.id) against your relationship graphderived_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
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.
Batteries included
Publishing freezes immutable versions; rollback republishes old content as a new version. History never rewrites — the database enforces it with triggers.
Content-hashed, Ed25519-signed artifacts with replay protection. Decision points refuse what they can't verify.
Per-tenant policies and bundles, shared ~global policies with tenant overrides — merged at build time, so tenants pay zero cost per decision.
Scoped API keys (hashed, revocable) and OIDC/JWT with JWKS caching. Per-identity rate limiting built in.
Self-serve accounts and projects, then a full editor: validate with line-precise diagnostics, simulate with traces, publish, roll back, read the audit log.
Postgres, SQLite, Cloudflare D1, R2/S3-compatible object storage, filesystem, or in-memory — behind one interface.
Get started
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