Skip to content

Running Verdict Server

Verdict Server provides a dedicated, high-performance authorization service that evaluates your access policies, stores authorization facts, and answers permission checks over standard HTTP and Connect RPC protocols. Running Verdict as a standalone service lets your backend replace complex database joins and scattered permission checks with a single authorization call, while allowing you to update policies centrally without redeploying application code.

This guide covers how to operate a single-instance Verdict Server: starting the standalone daemon, configuring SQLite persistence, securing access with API keys, monitoring health and readiness, and embedding Verdict directly into a Node.js process. If you are exploring Verdict for the first time, see the Quickstart for a quick introductory walkthrough.

Starting a standalone server with the Verdict CLI gives your application services a shared authorization endpoint. Run verdict serve in your terminal to launch the server:

verdict serve

By default, the server listens on port 4891 and uses a local SQLite database named ./verdict.db in your current working directory. You can specify a different port using the --port flag or the VERDICT_PORT environment variable:

verdict serve --port 5000

On startup, the server prints a status summary to standard output:

Verdict server listening on port 4891
Database: ./verdict.db
Authorization mode: proof
Maintained closures: disabled
Auth: disabled
Policy: none

This startup banner confirms key runtime settings:

  • Database: The file path where policies, stored facts, and mutation receipts are persisted.
  • Authorization mode: The policy evaluation strategy (defaults to proof).
  • Auth: Indicates whether bearer token authentication is enabled.
  • Policy: Indicates whether an active .vd policy is currently loaded into memory (loaded or none).

Once started, Verdict Server accepts requests on two protocols at the same port: the /v1 REST API for standard HTTP/JSON requests, and Connect RPC for typed SDK clients such as @useverdict/client. See the verdict serve CLI reference for complete flag documentation.

Server policy intake uses the authorization profile and defaults to stored-fact-only negation (fact_only). Set VERDICT_POLICY_NEGATION=unrestricted to admit broader negation. The configured server mode takes precedence over policy source directives; a source profile=general request does not bypass authorization intake. See Language Reference for supported forms and the serve reference for server options.

Verdict Server uses SQLite (powered by Node’s synchronous node:sqlite) to persist published policies, stored facts, and mutation idempotency receipts across process restarts. Because SQLite executes synchronously in-process, policy checks incur minimal query overhead and require no external database services.

By default, the server writes to ./verdict.db in the working directory where verdict serve is invoked. You can configure a specific database path using the --db flag or the VERDICT_DB_PATH environment variable:

verdict serve --db /var/data/verdict/verdict.db

Or by setting the environment variable in your service configuration:

export VERDICT_DB_PATH="/var/data/verdict/verdict.db"
verdict serve

When running automated test suites or CI pipelines where each run requires a fresh, isolated state, you can pass :memory: as the database path:

verdict serve --db :memory:

In-memory mode keeps all published policies and facts in RAM, discarding all stored state as soon as the server process stops.

In containerized environments such as Docker or Kubernetes, mount the directory containing your SQLite file to a persistent volume so that published policies and synchronized facts survive container redeployments. Ensure the container process has read and write permissions to both the SQLite file and its containing directory, which SQLite requires to create write-ahead log (.wal) and shared memory (.shm) files.

In local development, running without authentication allows you to test policies and inspect facts quickly. In shared staging and production environments, you should protect the server by requiring API keys.

When running without authentication configured, verdict serve accepts all incoming requests and outputs a warning to standard error:

WARNING: VERDICT_API_KEYS is not set; all requests will be accepted without authentication.

To enforce authentication, set the VERDICT_API_KEYS environment variable to one or more comma-separated secret tokens:

export VERDICT_API_KEYS="sec_live_prod_abc123,sec_live_prod_def456"
verdict serve

When VERDICT_API_KEYS is set, all incoming requests (with the exception of health checks) must include an Authorization: Bearer <key> header matching one of the configured keys. Requests with missing or invalid tokens receive an HTTP 401 Unauthorized response:

{
"error": "unauthorized",
"message": "Invalid API key"
}

Configuring multiple comma-separated keys allows seamless credential rotation without downtime:

  1. Add the new secret token to VERDICT_API_KEYS alongside your existing active key and deploy the server.
  2. Update your application services with the new token.
  3. Remove the retired token from VERDICT_API_KEYS and redeploy the server.

Verdict Server validates bearer tokens using timing-safe byte comparison (node:crypto.timingSafeEqual), preventing side-channel timing attacks from discerning valid keys.

Infrastructure monitors and container orchestrators need clear signals to determine when a server process is running (liveness) and ready to evaluate authorization checks (readiness).

Verdict Server provides a dedicated health check endpoint at GET /health. This endpoint is always publicly accessible without authentication, even when VERDICT_API_KEYS is configured, so load balancers and container probes can poll it without needing credentials:

curl http://localhost:4891/health

The endpoint returns an HTTP 200 response with a JSON body:

{
"status": "ok",
"policy_loaded": true
}

This response separates two operational concerns:

  • Server liveness (status: "ok"): Confirms that the HTTP server is responsive and connected to its SQLite database. Use this check for container liveness probes to detect process crashes or hangs.
  • Policy readiness (policy_loaded: true): Indicates whether an active .vd policy has been published and compiled into memory. When a server starts with a fresh database, policy_loaded is false until you publish a policy. Because authorization checks and queries require an active policy to evaluate, use policy_loaded for readiness probes to ensure traffic is only routed to instances that have loaded their policy.

You can check server status from the command line using verdict status:

verdict status --local

The command queries the server’s health endpoint and formats the outcome:

Server: ok
Policy: loaded

For automated scripts and CI pipelines, pass --json to receive structured output:

verdict status --local --json
{
"status": "ok",
"policy_loaded": true
}

The command exits with status 0 when the server is healthy. If the server cannot be reached, it outputs an error message (or {"status": "unreachable"} in JSON mode) and exits with status 1. See the verdict status CLI reference for details on exit statuses and connection profiles.

While running Verdict Server as an independent process is the standard deployment pattern for distributed backends, you can also embed the authorization engine directly into an existing Node.js application process. This is particularly useful for monolithic architectures, local integration tests, or custom server runtimes where running a separate daemon adds unnecessary operational overhead.

Import createServer from verdict-server/node and supply the database path:

import { createServer } from "verdict-server/node";
const server = createServer({
dbPath: "./verdict.db",
});

The returned server object provides a web-standard fetch(request: Request): Promise<Response> function that handles incoming Connect RPC and REST requests.

You can mount the embedded server using @hono/node-server to listen for HTTP connections on a specific port:

import { serve } from "@hono/node-server";
import { createServer } from "verdict-server/node";
const server = createServer({
dbPath: "./verdict.db",
});
const listener = serve({
fetch: server.fetch,
port: 4891,
});
// Clean shutdown
process.on("SIGTERM", () => {
listener.close(() => {
server.close();
});
});

If you are hosting Verdict routes alongside existing application APIs in a framework like Hono, pass basePath to mount Verdict under a specific URL prefix:

import { Hono } from "hono";
import { createServer } from "verdict-server/node";
const server = createServer({
dbPath: "./verdict.db",
basePath: "/api/verdict",
});
const app = new Hono();
// Forward requests beneath the base path to Verdict
app.all("/api/verdict/*", (c) => server.fetch(c.req.raw));

When basePath is configured, server.fetch automatically strips the prefix before routing requests internally. In this example, the health endpoint is served at /api/verdict/health, and requests outside /api/verdict return 404 Not Found.

When your application shuts down or an automated test finishes, call server.close(). This ensures that all active SQLite prepared statements are released and the underlying database adapter is closed cleanly, preventing file locks and data corruption.