Documentation

VectorLedger Docs v1.5.1

Everything you need to install, configure, and operate VectorLedger — from a first-run quick start to production HSM deployment, MCP server setup, and compliance reporting.

Table of Contents
Introduction

Overview

VectorLedger is a purpose-built, append-only financial ledger written entirely in Rust. Every journal entry is linked by a tamper-evident BLAKE3 hash chain, every page of data is encrypted at rest with AES-256-GCM, and every query result can carry a cryptographic Merkle proof that the returned data has not been modified since it was written.

Historical tampering is cryptographically detectable any modification to a past record invalidates every hash in the chain from that point to the present, provided that verification checkpoints are independently protected (which the HSM architecture is specifically designed to enforce).

Built by VectorGuard Labs · Version 1.5.1 · License: BUSL-1.1

Why VectorLedger?

Traditional relational databases treat audit trails as an afterthought: triggers that can be disabled, log tables that can be truncated, and backup files that can be silently replaced. For organizations operating under SOC 2, PCI-DSS, financial regulation, or internal zero-trust policies, this is not good enough.

  • A row written five years ago cannot be changed without invalidating every hash in the chain from that point to the present.
  • Every SELECT response optionally carries a Merkle proof that any client can independently verify.
  • The audit log is WORM-append-only each event is hashed into the next, forming a second independent tamper-evident chain.
  • The compliance engine generates machine-generated technical evidence supporting SOC 2 Type II and PCI-DSS v4 control assessments — not pre-written documentation. This evidence supports an auditor's work; it does not by itself make an organization compliant. Organizational compliance requires additional controls, policies, and independent auditor assessment beyond what any database engine can provide.
Scope note: VectorLedger generates machine-generated technical evidence supporting SOC 2 and PCI-DSS control assessments. This evidence is a technical input to an audit it does not by itself make an organization compliant. Organizational compliance requires additional policies, procedures, personnel controls, and independent auditor assessment that are outside the scope of any database engine.

Deployment Architecture

VectorLedger supports two PyHSM deployment models. Model 1 is suitable for development and single-server production. Model 2 isolates key material on a dedicated host with no public IP, which is the recommended configuration for production.

VectorLedger │ ┌──────────┴──────────┐ │ │ Local PyHSM Remote PyHSM (Model 1) (Model 2) │ │ Unix socket TLS 1.3 + mTLS /tmp/pyhsm.sock port 8443 │ │ ▼ ▼ PyHSM PyHSM same host separate host (private subnet, no public IP)

See Model 1 Local PyHSM and Model 2 Remote PyHSM for full setup instructions.

Setup

Prerequisites

RequirementMinimum VersionNotes
Rust toolchain1.80Install via rustup.rs
macOS, Linux, or WindowsmacOS and Linux are fully supported; Windows 10/11 and Windows Server 2019/2022 (x86_64 and ARM64) are supported
GitAny recentTo clone the repository

No other runtime dependencies are required. All cryptographic libraries are statically linked via Cargo.

Setup

Installation

Option 1 Install via curl (recommended)

The fastest way to install VectorLedger. The installer detects your OS and architecture, downloads the correct pre-built binary, verifies its SHA-256 checksum, and places vledger on your PATH.

curl --proto '=https' --tlsv1.2 -sSf \ https://raw.githubusercontent.com/pavondunbar/VectorLedger/main/install.sh | bash

Install a specific version

curl --proto '=https' --tlsv1.2 -sSf \ https://raw.githubusercontent.com/pavondunbar/VectorLedger/main/install.sh \ | VLEDGER_VERSION=v1.0.18 bash

Install to a custom directory

curl --proto '=https' --tlsv1.2 -sSf \ https://raw.githubusercontent.com/pavondunbar/VectorLedger/main/install.sh \ | VLEDGER_INSTALL_DIR="$HOME/.local/bin" bash

Installer environment variables

VariableDefaultDescription
VLEDGER_VERSIONlatestRelease tag to install, e.g. v1.0.18
VLEDGER_INSTALL_DIR/usr/local/binDirectory to place the vledger binary
VLEDGER_NO_MODIFY_PATH0Set to 1 to skip adding the install dir to your shell profile

After installation, verify it works:

vledger --version vledger self-test

Windows (PowerShell)

Run this in PowerShell 5.1+ or PowerShell 7+:

irm https://raw.githubusercontent.com/pavondunbar/VectorLedger/main/install.ps1 | iex

The installer detects your architecture (x86_64 or ARM64), downloads the signed release zip, verifies its SHA-256 checksum, installs vledger.exe to %LOCALAPPDATA%\vledger\bin, and adds it to your user PATH.

Install a specific version

$env:VLEDGER_VERSION = "v1.0.18" irm https://raw.githubusercontent.com/pavondunbar/VectorLedger/main/install.ps1 | iex

Install to a custom directory (with parameters)

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/pavondunbar/VectorLedger/main/install.ps1))) ` -InstallDir "C:\Tools\vledger"

Windows installer parameters

ParameterDefaultDescription
-VersionlatestRelease tag, e.g. v1.0.18. Also reads $env:VLEDGER_VERSION.
-InstallDir%LOCALAPPDATA%\vledger\binDirectory to install vledger.exe
-NoPathUpdateoffSkip adding the install dir to your user PATH
Windows-specific notes: PyHSM uses TCP instead of a Unix socket on Windows. Start the PyHSM daemon with $env:PYHSM_TCP_PORT = 7777 and pass --pyhsm-socket 127.0.0.1:7777 to vledger init. File permissions (0o600) are not enforced on Windows protect your data directory using NTFS ACLs. Graceful shutdown responds to CTRL-C; use Stop-Process or the service manager instead of SIGTERM.

Option 2 Build from source

1 Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" rustc --version # should be 1.80 or newer cargo --version
2 Clone the repository
git clone https://github.com/pavondunbar/VectorLedger.git cd VectorLedger
3 Build
# Development build cargo build # Optimized production binary cargo build --release

The binary is written to target/debug/vledger (dev) or target/release/vledger (release). Install to your PATH:

cargo install --path crates/vledger # Or manually: sudo cp target/release/vledger /usr/local/bin/vledger
4 Verify the build
./target/release/vledger self-test ./target/release/vledger self-test-phase3

Expected output:

── VectorLedger Phase 2 Self-Test ─────────────── [1/7] Hash chain ... ✓ [2/7] AES-256-GCM encryption ... ✓ [3/7] Merkle proofs ... ✓ [4/7] WAL-backed ledger ... ✓ [5/7] Page encryption ... ✓ [6/7] SQL engine ... ✓ [7/7] Verifiable query proof ... ✓ ✓ All Phase 2 self-tests passed. ── VectorLedger Phase 3 Self-Test ─────────────── [1/7] Audit log (WORM + chain) ... ✓ [2/7] Four-eyes workflow ... ✓ [3/7] Compliance report (SOC 2) ... ✓ [4/7] Compliance report (PCI-DSS) ... ✓ [5/7] Backup & restore round-trip ... ✓ [6/7] PgWire message encoding ... ✓ [7/7] SQL optimizer (agg + window) ... ✓ ✓ All Phase 3 self-tests passed.
Setup

Quick Start

VectorLedger uses PyHSM a VectorGuard Labs product as its default key management backend. PyHSM acts as a local software HSM: VectorLedger's master encryption key is sealed inside PyHSM's encrypted keystore and never touches disk in plaintext.

The steps below walk through the full setup from scratch. If you are not using PyHSM and prefer a different key backend, jump to Key Source Backends.

1 Install and start PyHSM

PyHSM ships as both a Python package and a TypeScript daemon. VectorLedger connects to the TypeScript daemon via a Unix domain socket.

Install Node.js (18+) if you don't have it

brew install node # Or download from https://nodejs.org

Clone PyHSM and install dependencies

git clone https://github.com/pavondunbar/PyHSM.git ~/PyHSM cd ~/PyHSM/pyhsm-ts npm install

Generate a PyHSM master password

openssl rand -base64 32
Important: Store this password somewhere safe before proceeding. If it is lost, the keystore cannot be unlocked.

Start the PyHSM daemon

cd ~/PyHSM PYHSM_MASTER_PASSWORD="<your-password>" \ PYHSM_KEYSTORE_PATH="$HOME/PyHSM/pyhsm-keystore.enc" \ PYHSM_AUDIT_LOG_PATH="$HOME/PyHSM/pyhsm-audit.jsonl" \ npx tsx pyhsm-ts/process.ts

Expected output: [PyHSM] Listening on /tmp/pyhsm.sock

Confirm the socket is ready in a new terminal:

ls -la /tmp/pyhsm.sock # srw------- 1 you wheel 0 ... /tmp/pyhsm.sock
Important: PyHSM must always start before VectorLedger. If the daemon is not running when vledger start is called, startup will fail with a clear error.
2 Initialise the database
cd /path/to/VectorLedger vledger init --data-dir ./vledger-data --key-source pyhsm

Verify the backend was recorded correctly:

cat vledger-data/keys/key_source.json # Must contain "backend": "py_hsm"
Never run vledger init without --key-source pyhsm in production. The default without this flag uses an environment variable backend which stores key material in your shell environment.
3 Lock down the data directory
chmod 700 vledger-data/ chmod 700 vledger-data/keys/ chmod 700 vledger-data/catalog/ chmod 700 vledger-data/audit/ chmod 700 vledger-data/wal/ chmod 700 vledger-data/pages/
4 Start the server
vledger start --data-dir ./vledger-data

On first start, credentials are written to ./vledger-data/catalog/.admin_initial_credentials (mode 0600) not printed to the terminal.

5 Change the admin password
cat vledger-data/catalog/.admin_initial_credentials vledger user set-password --username admin --data-dir ./vledger-data rm vledger-data/catalog/.admin_initial_credentials
6 Verify integrity
vledger verify --data-dir ./vledger-data
7 Run your first queries
vledger sql --data-dir ./vledger-data --username admin

Once in the REPL, use \x to toggle expanded (vertical) display — useful for wide rows like ledger entries. Use \? to see all available meta-commands.

-- Create accounts INSERT INTO accounts (code, name, account_type, currency, domain) VALUES ('CASH', 'Cash - USD', 'asset', 'USD', 'main'); INSERT INTO accounts (code, name, account_type, currency, domain) VALUES ('REVENUE', 'Revenue', 'income', 'USD', 'main'); -- Post a journal entry INSERT INTO ledger (description, debit_account, credit_account, amount, currency, domain) VALUES ('Customer payment', 'CASH', 'REVENUE', 100000, 'USD', 'main'); -- Check balance (amounts are in minor units — cents for USD) SELECT BALANCE('CASH'); -- balance -- ------- -- 100000 -- Verify the hash chain SELECT VERIFY_CHAIN(); -- status | entries_verified -- -------+----------------- -- OK | 1 -- Query all entries (one row per entry) SELECT * FROM ledger; -- Query entries in traditional accounting format (one row per debit/credit line) SELECT * FROM ledger_lines LIMIT 10; -- date | sequence | description | account_id | dr_cr | amount | currency | domain -- -----------+----------+------------------+------------+--------+--------+----------+------- -- 2026-08-17 | 1 | Customer payment | <uuid> | Debit | 1.00 | USD | main -- 2026-08-17 | 1 | Customer payment | <uuid> | Credit | 1.00 | USD | main
8 Also start the PostgreSQL wire-protocol listener
vledger start --data-dir ./vledger-data --pgwire # Then connect with any PostgreSQL client: psql "host=127.0.0.1 port=5432 user=admin sslmode=require"
9 Start the MCP server (AgenticAI license required)
vledger mcp --bind 127.0.0.1:3000 # Add to your AI assistant's MCP config: # { "mcpServers": { "vledger": { "url": "http://127.0.0.1:3000/sse" } } } curl http://127.0.0.1:3000/health
Operations

Reset / Starting Over

If you lose the admin password, or need to wipe the database and start fresh, delete the data directory and re-initialise. Make sure PyHSM is running before you init.

This is destructive. All data in the data directory will be permanently deleted. Stop the server first.
# Stop the server first (Ctrl-C in the terminal where it is running) rm -rf ./vledger-data vledger init --data-dir ./vledger-data --key-source pyhsm vledger start --data-dir ./vledger-data

The first start after a fresh init writes a new admin credential file. Read it, change the password, and delete the file before doing anything else.

CLI Reference

vledger init

Initialise a new database.

vledger init [OPTIONS] --data-dir <PATH> Data directory (default: ./vledger-data) --force Reinitialise an existing database --key-source <BACKEND> env | file | vault | aws_kms | pyhsm | remote-pyhsm (default: pyhsm) --vault-addr <URL> Vault server address --vault-mount <MOUNT> Vault KV v2 mount path (default: secret) --vault-path <PATH> Vault secret path (default: vledger/master_key) --kms-key-id <ARN> AWS KMS key ARN or alias --kms-region <REGION> AWS region (default: us-east-1) # Model 1 local PyHSM (same server) --pyhsm-socket <PATH> PyHSM Unix socket path (default: /tmp/pyhsm.sock) --pyhsm-caller-id <ID> Caller ID written to PyHSM audit log (default: vledger) # Model 2 remote PyHSM (same-region, separate server) --pyhsm-endpoint <URL> Remote PyHSM HTTPS endpoint; selects remote-pyhsm automatically when set Example: https://pyhsm.internal.example.com:8443 Env: PYHSM_ENDPOINT --pyhsm-ca-cert <PATH> CA certificate PEM to verify the PyHSM server Env: PYHSM_CA_CERT --pyhsm-client-cert <PATH> mTLS client certificate PEM (VectorLedger's identity) Env: PYHSM_CLIENT_CERT --pyhsm-client-key <PATH> mTLS client private key PEM Env: PYHSM_CLIENT_KEY --pyhsm-timeout-ms <MS> Per-request timeout in ms (default: 5000) Env: PYHSM_TIMEOUT_MS --pyhsm-max-retries <N> Max retries on transient errors (default: 3)
CLI Reference

vledger start

Start the server. Responds to SIGTERM and CTRL-C with a graceful drain — in-flight connections are allowed to complete before the process exits.

vledger start [OPTIONS] --data-dir <PATH> Data directory (default: ./vledger-data) --bind <ADDR> Bind address (default: 127.0.0.1:5433) --pgwire Also start the PostgreSQL wire-protocol listener on port 5432 --with-proofs Attach Merkle proofs to every SELECT response --wal-sync-mode <MODE> per_record | group_commit | no_sync (default: group_commit) --group-commit-delay-ms <MS> Group-commit flush interval in ms (default: 2) --query-timeout-ms <MS> Max SQL query duration before cancellation (default: 30,000) --metrics-addr <ADDR> Prometheus metrics HTTP bind address (default: 127.0.0.1:9090) Set to empty string to disable --max-connections <N> Max concurrent connections (default: 128) --tls-cert-path <PATH> Path to TLS certificate PEM (replaces self-signed) --tls-key-path <PATH> Path to TLS private key PEM (replaces self-signed)
Starting the MCP server: Run vledger mcp --bind 127.0.0.1:3000 alongside vledger start to enable AI assistant access. The MCP server auto-detects the running instance on port 5433. Requires AgenticAI license feature (Starter+). See vledger mcp and MCP Server Overview.
Integrated replication: If replication.json exists in the data directory with "role": "primary", vledger start automatically activates the WAL shipper alongside the SQL server — no separate start-primary process is required. Every committed journal entry is shipped to connected replicas automatically.
Merkle roots without --with-proofs: As of v1.0.36, the MERKLE_ROOT(from_seq, to_seq) SQL function is available at all times regardless of server startup flags. Any client with the admin, operator, or auditor role can call it directly: SELECT MERKLE_ROOT(786000, 786500); — no server flag required. The --with-proofs flag is a separate, optional feature that causes every SELECT to automatically append a Merkle proof as a pgwire NOTICE. Add it only if you want automatic per-query proofs on every SELECT result.

Prometheus metrics

A Prometheus metrics server starts automatically on a separate port (default 127.0.0.1:9090, configurable via --metrics-addr). It exposes two endpoints:

EndpointDescription
GET /metricsPrometheus text-format scrape target
GET /healthLiveness probe — returns 200 OK with body ok

The /health endpoint is suitable for load-balancer and orchestrator health checks (AWS ALB, Kubernetes livenessProbe, etc.). It does not require authentication and remains reachable when the connection semaphore is saturated.

scrape_configs: - job_name: vledger static_configs: - targets: ['your-host:9090'] scrape_interval: 15s

Available metrics

MetricTypeDescription
vledger_build_infogaugeBuild version and Rust version
vledger_uptime_secondsgaugeServer uptime in seconds
vledger_ledger_entries_totalcounterTotal journal entries posted
vledger_ledger_accounts_totalgaugeTotal accounts in chart of accounts
vledger_wal_segments_totalgaugeNumber of WAL segment files
vledger_wal_sync_lag_msgaugeMilliseconds since last WAL fsync
vledger_connections_activegaugeCurrently open client connections
vledger_connections_totalcounterTotal connections accepted since start
vledger_auth_successes_totalcounterSuccessful authentications
vledger_auth_failures_totalcounterFailed authentication attempts
vledger_queries_totalcounterSQL queries executed
vledger_query_duration_ms_sumcounterCumulative query execution time (ms)
vledger_chain_verification_failures_totalcounterHash chain integrity failures
vledger_backup_age_secondsgaugeSeconds since the last backup was created
vledger_replica_lag_lsngaugeReplication lag in WAL records
vledger_foureyes_pending_totalgaugeFour-eyes entries awaiting approval

Two background tasks run automatically after startup:

  • Hourly integrity check — calls VERIFY_CHAIN() every 60 minutes. Any failure is logged as an error and written to the audit log as INTEGRITY_ALERT.
  • 15-minute invariant monitor — checks that Σ(Assets+Expenses) == Σ(Liabilities+Equity+Income) every 15 minutes. Violations are logged as INVARIANT_ALERT.

When started with --with-proofs, every SELECT response in the vledger sql REPL includes a Merkle proof and root hash.

WAL sync modes

# Default group commit (2 ms flush interval) vledger start # Per-record fsync (safest, lower TPS) vledger start --wal-sync-mode per_record # Tune the flush interval vledger start --wal-sync-mode group_commit --group-commit-delay-ms 5 # no_sync is NOT available in release binaries — compile-gated behind --features dev-no-sync
CLI Reference

vledger sql

Interactive SQL REPL or single-statement execution. The REPL includes built-in line editing (arrow keys, up/down history). When vledger start is running, the CLI automatically detects it and connects over TLS. Use --server to point at a non-default address.

vledger sql [OPTIONS] --data-dir <PATH> Data directory --query <SQL> Run a single statement and exit (omit for interactive mode) --ask <QUESTION> Translate a natural-language question to SQL and run it. Requires OPENAI_API_KEY and AgenticAI license feature. The generated SQL is printed to stderr before execution. --username <USER> Username (or set VLEDGER_CLI_USERNAME) --password <PASS> Password (or set VLEDGER_CLI_PASSWORD, or enter interactively) --server <ADDR> Connect to a running server at host:port instead of opening the data directory directly (auto-detected if server is reachable at 127.0.0.1:5433) --ca-cert <PATH> CA certificate PEM to verify the server's TLS certificate. Required when connecting to a non-loopback address. Loopback connections (127.0.0.1 / localhost) accept the self-signed certificate automatically.

Natural-language queries

export OPENAI_API_KEY=sk-... vledger sql --ask "show me all payments over $10,000 last week" vledger sql --ask "what is the balance of the CASH account" # → SQL: SELECT BALANCE('CASH') # balance # ------- # 250000
No ledger data is sent to the LLM. Only the user's question text and a static schema context are transmitted. Key material, credentials, and row data stay local.

REPL meta-commands

CommandDescription
\xToggle expanded (vertical) display. Useful for wide rows.
\q or exitQuit the REPL.
\? or \helpShow available meta-commands.

The prompt changes to vledger (expanded)> while expanded mode is active. Use \x again to toggle back.

CLI Reference

vledger verify

Verify WAL integrity and the ledger hash chain. Use --self-test to run the full integrity suite against an isolated temporary database — your production data is never touched.

vledger verify --data-dir <PATH> [--self-test] Run full integrity suite in an isolated temp database [--entries <N>] Entries to generate for self-test (default: 100,000) [--keep-data] Keep the self-test database after completion
CLI Reference

vledger status

Show database version, WAL segment count, and active segment.

vledger status --data-dir <PATH>
CLI Reference

vledger backup

Create a point-in-time backup archive with a BLAKE3 manifest. Private key material is excluded only public keys are archived.

vledger backup --data-dir <PATH> [--output <FILE.tar>]
CLI Reference

vledger restore

Restore a backup archive. Decrypts every file using the .key sidecar, then verifies each file's BLAKE3 hash against the manifest before completing. --force is required to overwrite an existing data directory.

vledger restore --from <FILE.tar> [--target <PATH>] [--force]
CLI Reference

vledger backup-verify

Verify a backup archive without restoring it — checks the manifest hash and, when the master key is available, decrypts and hash-checks every file. Use --decrypt false to verify the manifest hash only (no decryption, no master key required).

vledger backup-verify --from <FILE.tar> [--decrypt]
CLI Reference

vledger rotate-keys

Rotate all HSM key slots and record audit events. Old key versions are archived for decryption of existing data; new versions are used for all new writes.

vledger rotate-keys [OPTIONS] --data-dir <PATH> Data directory (default: ./vledger-data) --caller-id <ID> Caller ID written to audit log # Model 1 local PyHSM --hsm-socket <PATH> PyHSM Unix socket path (default: /tmp/pyhsm.sock) # Model 2 remote PyHSM --pyhsm-endpoint <URL> Remote PyHSM HTTPS endpoint --pyhsm-ca-cert <PATH> CA certificate PEM --pyhsm-client-cert <PATH> mTLS client certificate PEM --pyhsm-client-key <PATH> mTLS client private key PEM --pyhsm-timeout-ms <MS> Per-request timeout in ms (default: 5000) --pyhsm-max-retries <N> Max retries on transient errors (default: 3)
CLI Reference

vledger audit-export

Export the WORM audit log, optionally filtered by date range.

vledger audit-export --data-dir <PATH> [--format json|csv] [--output <FILE>] [--from <RFC3339>] [--to <RFC3339>]

Audit log export is tier-gated:

TierDate range allowed
FreeLast 30 days
StarterLast 90 days
Growth / EnterpriseUnlimited
CLI Reference

vledger audit-package

Generate a portable, self-contained cryptographic audit evidence package. Default (commitment-only) mode computes the Merkle root over all entries in a single O(n) pass, signs it with the database Ed25519 key, and writes a compact JSON commitment. Completes in seconds at any scale.

vledger audit-package [OPTIONS] --data-dir <PATH> Data directory (default: ./vledger-data) --output <FILE> Output JSON file (default: ./vledger-audit-package-<ts>.json) --tenant <NAME> Organisation name to embed in the package Example: --tenant "Acme Financial" --description <TEXT> Description of this audit package Example: --description "Q3 2026 regulatory audit" --period-start <DATE> Start of the reporting period (YYYY-MM-DD or RFC 3339) --period-end <DATE> End of the reporting period (YYYY-MM-DD or RFC 3339) --include-entries Also embed all entries and per-entry Merkle proofs (only practical for ledgers with < ~10,000 entries)
vledger audit-package \ --data-dir ./vledger-data \ --tenant "Acme Financial" \ --description "Q3 2026 regulatory audit" \ --period-start 2026-07-01 \ --period-end 2026-09-30 \ --output audit-q3-2026.json

The output JSON meta block includes all provided fields:

{ "meta": { "tenant": "Acme Financial", "description": "Q3 2026 regulatory audit", "period_start": "2026-07-01", "period_end": "2026-09-30", "entry_count": 1000000, "merkle_root": "804efb54...", "root_signature": "...", "generated_at": "2026-09-30T23:59:59Z" } }
CLI Reference

vledger audit-proof

Generate a single-entry inclusion proof against a commitment package. Produces a self-contained JSON file the auditor can verify without any database access.

vledger audit-proof [OPTIONS] --data-dir <PATH> Data directory --commitment <FILE> Commitment package JSON (from vledger audit-package) --sequence <N> Sequence number of the entry to prove --output <FILE> Output proof JSON (default: ./vledger-entry-proof-<N>.json)
vledger audit-proof \ --data-dir ./vledger-data \ --commitment audit.json \ --sequence 406340 \ --output entry-406340-proof.json
CLI Reference

vledger verify-audit-package

Verify an audit package or entry proof. Requires no database access, no server, and no key files. Works on all three package types: commitment, entry_proof, and full.

vledger verify-audit-package --file <FILE>
Package typeWhat is verified
commitmentEd25519 root signature
entry_proofContent hash + chain hash + Merkle inclusion proof
fullAll of the above + all entries

Example output:

── VectorLedger Audit Verifier ───────────────── Type : entry_proof Entries : 1 [1/3] Content hash ✓ [2/3] Chain hash ✓ [3/3] Merkle proof ✓ ✓ VERIFIED — 1 entries, all checks passed. Merkle root : 804efb54ea31539a...

Auditors who cannot install software can download a pre-built binary from the GitHub release page (no Rust toolchain required):

curl --proto '=https' --tlsv1.2 -sSf \ https://raw.githubusercontent.com/pavondunbar/VectorLedger/main/install.sh | bash vledger verify-audit-package --file entry-406340-proof.json
CLI Reference

vledger compliance-report

Generate a SOC 2 or PCI-DSS compliance evidence report. Reports are generated by running checks against real filesystem state not pre-written text.

vledger compliance-report --data-dir <PATH> [--standard soc2|pci-dss] [--format markdown|json] [--output <FILE>] # Example generate a PCI-DSS report as Markdown: vledger compliance-report --data-dir ./vledger-data \ --standard pci-dss \ --format markdown \ --output pci-report.md
CLI Reference

vledger license

Show the active license tier, features, and expiry. Place the signed license.json file provided by VectorGuard Labs into your data directory to unlock paid features. If no file is present, the engine runs in Free tier.

vledger license --data-dir <PATH> # Example output (Growth tier): # ── VectorLedger License ──────────────────────── # Tier : Growth # Licensee : Acme Corp # Expires : 2027-08-07 # Status : Active (365 days remaining)
CLI Reference

vledger start-primary

Start a WAL replication primary listener. Requires a Growth or Enterprise license.

Tip: In most deployments you don't need this command. If replication.json exists with "role": "primary", vledger start automatically activates the WAL shipper. Use start-primary only when you want a shipper running without a SQL server.
vledger start-primary [OPTIONS] --data-dir <PATH> Data directory (default: ./vledger-data) --bind <ADDR> Bind address for the WAL shipper (overrides replication.json; default: 127.0.0.1:5434)

Reads <data-dir>/replication.json. If the file does not exist, a default config is written on first run. The primary auto-generates replication_secret.hex (mode 0o600) on first start — copy this file to every replica before starting them.

CLI Reference

vledger start-replica

Start a WAL replication replica. Requires a Growth or Enterprise license. The replication_secret.hex file must already be present (copied from the primary) before this command will succeed.

vledger start-replica [OPTIONS] --data-dir <PATH> Data directory (default: ./vledger-data) --primary <ADDR> Primary host:port to connect to (overrides replication.json)

The replica connects to the primary, performs the BLAKE3 HMAC handshake, and streams WAL records continuously. It reconnects automatically on disconnection with exponential backoff.

CLI Reference

vledger user

Manage user accounts. All subcommands read the user store directly from the data directory the server does not need to be running.

SubcommandDescription
set-passwordChange a user's password (revokes all active sessions)
set-roleChange a user's role (revokes all active sessions immediately)
createCreate a new user account
listList all accounts
set-enabledEnable or disable an account
deleteDelete an account
# Change a password vledger user set-password --data-dir <PATH> --username admin # Create a user (roles: admin, operator, auditor, readonly) vledger user create --data-dir <PATH> --username alice --role operator # List users vledger user list --data-dir <PATH> # Change a user's role (revokes all active sessions immediately) vledger user set-role --data-dir <PATH> --username alice --role auditor # Disable an account vledger user set-enabled --data-dir <PATH> --username alice --enabled false # Delete an account vledger user delete --data-dir <PATH> --username alice
Note on set-role: When a user's role is changed, all their active sessions are immediately revoked. They must log in again to receive the new permissions. No account deletion and recreation is needed.
CLI Reference

vledger seed

Populate the database with randomly generated journal entries for testing and benchmarking. Does not require a running server — opens the data directory directly.

The server must NOT be running when executing this command.
vledger seed [OPTIONS] --data-dir <PATH> Data directory (default: ./vledger-data) --entries <N> Number of journal entries to generate (default: 10,000) --accounts <N> Number of accounts to create (default: 20) --seed <N> RNG seed for reproducible datasets (default: random) --progress <N> Print progress every N entries; 0 = silent (default: 100,000)
# Generate 10 million entries vledger seed --data-dir ./vledger-data --entries 10000000 --accounts 50 --progress 500000 # Reproducible dataset — same data every time vledger seed --data-dir ./vledger-data --entries 10000000 --seed 12345
CLI Reference

vledger migrate-to-sqlite

One-time migration that populates the SQLite entry index (vledger.db) from the WAL. Run this if the SQLite index is empty after a large bulk import or after upgrading from a version that predates the SQLite index. The server must NOT be running.

vledger migrate-to-sqlite [OPTIONS] --data-dir <PATH> Data directory (default: ./vledger-data)

Reads the WAL in batches and writes entries to the SQLite index incrementally. Progress is printed every 100,000 entries. It is safe to interrupt and re-run — already-indexed entries are skipped. On completion it writes wal-checkpoint.json so future startups skip the WAL replay.

SituationAction
SQLite index (vledger.db) is empty after a large vledger importRun migrate-to-sqlite once after import completes
Upgrading from a version that did not maintain the SQLite indexRun migrate-to-sqlite once after upgrading the binary
Normal operationNot needed — the index is maintained automatically

After migration completes, run vledger verify --data-dir ./vledger-data to confirm WAL and chain integrity.

CLI Reference

FTS Index Rebuild (Automatic on Startup)

From v1.0.30, VectorLedger maintains a SQLite FTS5 full-text index over all entry metadata. This index is built and maintained automatically — no manual action is required in normal operation.

SituationWhat happens
New entries (any version)Indexed at insert time automatically
Existing database, first startup after upgrading to v1.0.30+VectorLedger detects empty FTS index, rebuilds it automatically before accepting connections. Progress logged to nohup.out
Subsequent startupsRebuild skipped entirely — index already populated

Example progress output in nohup.out during backfill:

INFO FTS index empty — rebuilding from existing entries total=17652378 INFO FTS rebuild progress processed=1000000 INFO FTS rebuild progress processed=2000000 ... INFO FTS index rebuilt entries_indexed=17652378

No manual steps are required. The index works with any metadata field name — future CSV imports with different column layouts are automatically searchable without schema changes.

CLI Reference

vledger import

Import journal entries from a CSV or JSON file. Every row goes through the full post_entry() integrity path — debit/credit validation, account existence, currency check, balance constraints, idempotency, hash chain, WAL persistence. The server must NOT be running.

vledger import [OPTIONS] -f, --file <PATH> Import file path (required) --format <csv|json> File format; auto-detected from extension if omitted --dry-run Validate only — no data written; reports all errors --map <SRC=TARGET> Column mapping (repeatable). Target fields: description, debit_account, credit_account, amount, currency, domain, external_ref, idempotency_key, effective_date Example: --map memo=description --mapping-file <PATH> JSON mapping file {"source_col":"target_field",...} --domain <DOMAIN> Default domain for imported entries (default: main) --default-currency <CODE> Default currency when not in source (default: USD) --id-column <COL> Source column used as idempotency key. If omitted, a BLAKE3 hash of the full row is used --on-error <abort|skip|collect> Behaviour on row error (default: abort) --batch-size <N> Rows per checkpoint (default: 1,000) --state-file <PATH> Checkpoint state file (default: import-state.json) --resume Resume an interrupted import from last checkpoint --progress <N> Print progress every N rows; 0 = silent (default: 10,000) --manifest <PATH> Cryptographic import manifest output path (default: import-manifest.json) --create-accounts Auto-create referenced accounts not yet in ledger --metadata-columns <LIST> Comma-separated source columns to pack into the metadata JSON field on every imported entry. Example: --metadata-columns sender_name,channel,status --wal-sync-mode <MODE> group_commit (default) | no_sync | per_record

Amount handling: Decimal amounts are automatically converted to minor units (cents). 915.87 becomes 91587. If your source column is already in minor units, it is passed through unchanged.

Recommended workflow

# Step 1 — Validate first (no data written) vledger import --file export.csv --dry-run \ --map sender_account=debit_account \ --map receiver_account=credit_account \ --metadata-columns sender_name,receiver_name,channel,status # Step 2 — Execute vledger import --file export.csv \ --map sender_account=debit_account \ --map receiver_account=credit_account \ --metadata-columns sender_name,receiver_name,channel,status \ --create-accounts --on-error skip --progress 100000 # If interrupted, resume from last checkpoint vledger import --file export.csv --resume
CLI Reference

vledger reconcile

Recompute all account balances from journal entries and compare against the running balance cache. Exits non-zero if any discrepancy is found. Does not require a running server.

vledger reconcile [OPTIONS] --data-dir <PATH> Data directory (default: ./vledger-data) --format <text|json> Output format (default: text) -o, --output <PATH> Write output to file instead of stdout

A clean ledger produces:

── VectorLedger Reconciliation ───────────────── ✓ All accounts reconcile. Balance cache matches journal entries.
CLI Reference

vledger settle

Transition a journal entry through the settlement lifecycle. Status transitions: Posted → Pending → Settled | Failed. The original entry row is never modified — transitions are stored as append-only SettlementEvent records. Legal holds block settlement transitions. Does not require a running server.

vledger settle [OPTIONS] --data-dir <PATH> Data directory (default: ./vledger-data) --entry-id <UUID> Entry ID to transition (required) --status <pending|settled|failed> New status (required) --notes <TEXT> Optional notes (settlement reference, failure reason)
CLI Reference

vledger hold

Manage legal holds on accounts. While a hold is active, no new entries, reversals, or settlement transitions involving any line of the held account are permitted. Does not require a running server.

vledger hold place --data-dir <PATH> --account <CODE_OR_UUID> vledger hold lift --data-dir <PATH> --account <CODE_OR_UUID> vledger hold list --data-dir <PATH>
CLI Reference

vledger retention

Manage data retention policies. Policies are stored in <data-dir>/catalog/retention_policy.json. Does not require a running server. Use --days 0 to keep forever; --domain applies the policy to a specific domain only.

vledger retention show --data-dir <PATH> vledger retention set --data-dir <PATH> --days <N> [--domain <DOMAIN>] vledger retention clear --data-dir <PATH>
CLI Reference

vledger rules

Record and track accounting rule versions. Rule versions are stored in <data-dir>/catalog/accounting_rules.json. Does not require a running server.

vledger rules show --data-dir <PATH> vledger rules set --data-dir <PATH> --version <VERSION> --description <TEXT> [--effective-date <YYYY-MM-DD>] vledger rules history --data-dir <PATH>
vledger rules set --data-dir ./vledger-data \ --version "2026-Q3" \ --description "Updated FX conversion rules per IFRS 9 amendment" \ --effective-date 2026-07-01
CLI Reference

vledger mcp

Start the MCP (Model Context Protocol) server. Requires AgenticAI license feature (Starter, Growth, or Enterprise). Auto-detects a running vledger start on port 5433 and switches to network mode automatically.

vledger mcp [OPTIONS] --bind <ADDR> MCP server bind address (default: 127.0.0.1:3000) --server <HOST:PORT> Proxy to a running vledger start instance --ca-cert <PATH> CA cert for non-loopback TLS -u, --username <USER> Falls back to VLEDGER_CLI_USER env var -p, --password <PASS> Falls back to VLEDGER_CLI_PASSWORD
# Embedded — auto-detects vledger start on port 5433 (recommended) vledger mcp --bind 127.0.0.1:3000 # Explicit network mode vledger mcp \ --bind 127.0.0.1:3000 \ --server 127.0.0.1:5433 \ --username admin
The MCP server does not implement TLS. Bind to 127.0.0.1 (the default) or place behind a TLS-terminating reverse proxy for non-loopback access. All MCP tool calls go through the same RBAC enforcement as direct SQL — no tool bypasses authorization.
MCP Server

MCP Server Overview

VectorLedger ships a Model Context Protocol server (port 3000, HTTP+SSE) that lets AI assistants — Claude Desktop, Cursor, Kiro, and any other MCP-capable client — query and write to the ledger directly without SQL. Requires AgenticAI license feature (Starter, Growth, or Enterprise).

Health check:
curl http://127.0.0.1:3000/health
Response: {"ok":true,"service":"vledger-mcp","version":"1.5.1","tools":15,"agent_queries_used":3,"agent_queries_limit":100,"agent_queries_remaining":97}

All 15 MCP Tools

ToolCategoryDescription
query_ledgerQueryRun any SELECT, BALANCE(), VERIFY_CHAIN(), MERKLE_ROOT()
post_entryWriteRecord a new double-entry journal entry
get_balanceQueryCurrent balance of any account
list_accountsQueryAll accounts with balances (optional domain/currency filter, default limit 500)
query_ledger_linesQueryIndividual debit/credit lines
verify_chainIntegrityVerify the BLAKE3 hash chain
merkle_rootIntegrityBLAKE3 Merkle commitment over a sequence range
explain_balanceReasoningWhy is an account at its current balance? Chains metadata → debits → credits → narrative
reconcile_accountReasoningDoes stored balance match sum(posted lines)? Returns BALANCED or exact discrepancy
find_policy_violationsReasoningLarge transactions, pending-too-long, missing external refs, failed entries
summarize_periodReasoningNatural-language summary: counts by status, volume, Merkle commitment, chain integrity
audit_reportReasoningFull cryptographic audit evidence report for a period
resolve_accountIdentityResolve name/code/UUID → authoritative account ID (mandatory before writes)
propose_correctionCorrectionShow reversal+correction plan (writes nothing)
execute_correctionCorrectionExecute reversal + correction after user confirms

All tools execute through the same parse → plan → privilege-check → execute pipeline as direct SQL. No tool bypasses RBAC.

MCP Server

MCP Client Configuration

Add to your AI assistant's MCP config file (Claude Desktop, Cursor, Kiro):

{ "mcpServers": { "vledger": { "url": "http://127.0.0.1:3000/sse" } } }

AGENT_SYSTEM_PROMPT

A public AGENT_SYSTEM_PROMPT constant is exported from vledger-mcp and returned in the MCP initialize response's instructions field. It contains financially-aware instructions covering minor-units semantics, double-entry rules, account type normal balances, entry status lifecycle, tool routing guidance, and behavioral rules.

from vledger_mcp import AGENT_SYSTEM_PROMPT agent = Agent( name="VectorLedger Agent", instructions=AGENT_SYSTEM_PROMPT, mcp_servers=[vledger], )
MCP Server

Agent Query Billing

One Agent Query is counted per initialize JSON-RPC request (one conversation), regardless of how many internal tool calls the agent makes within that session.

Session idle timeout: 30 minutes. After 30 minutes of inactivity, the next question starts a new Agent Query.

TierAgent Queries / Month
FreeDisabled
Starter10
Growth100
EnterpriseUnlimited

The counter file (mcp_queries.json) resets automatically on the first day of each UTC month. It is a commercial metering mechanism — not a security control.

MCP Server

Natural-Language Queries (--ask)

The vledger sql --ask flag translates a natural-language question into SQL via an LLM and executes it. Requires OPENAI_API_KEY and the AgenticAI license feature (Starter, Growth, or Enterprise).

export OPENAI_API_KEY=sk-... vledger sql --ask "show me all payments over $10,000 last week" vledger sql --ask "what is the balance of the CASH account" vledger sql --ask "compute the Merkle root over entries 100000 to 200000"

The generated SQL is printed to stderr as → SQL: ... before execution. No ledger data, credentials, or key material is transmitted to the LLM provider — only the user's question text and static schema context.

Environment VariableDefaultPurpose
OPENAI_API_KEY—Required. Your API key.
OPENAI_MODELgpt-4oModel to use for translation.
OPENAI_BASE_URLhttps://api.openai.com/v1Override for Ollama, Groq, etc.
Using a local model? Set OPENAI_BASE_URL to your Ollama or Groq endpoint to avoid sending queries to OpenAI.
Key Source Backends

Environment Variable

Best for containerised environments where secrets are injected as environment variables (Kubernetes Secrets, AWS ECS task definitions, etc.).

export VectorLedger_MASTER_KEY="$(openssl rand -hex 32)" vledger init --key-source env
Key Source Backends

HashiCorp Vault KV v2

VAULT_TOKEN must be set in the environment before starting the server. VectorLedger checks the token's TTL at startup and logs a warning if it expires within 24 hours.

# One-time setup: write the key to Vault vault kv put secret/vledger/master_key value="$(openssl rand -hex 32)" # Initialise with Vault backend export VAULT_TOKEN="<your-vault-token>" vledger init \ --key-source vault \ --vault-addr http://127.0.0.1:8200 \ --vault-mount secret \ --vault-path vledger/master_key
Key Source Backends

AWS KMS

On first start, VectorLedger calls GenerateDataKey and caches the encrypted ciphertext blob locally (kms_data_key.enc). On subsequent restarts, it calls Decrypt against the cached blob. The cache file is protected with an HMAC-SHA256 integrity check a tampered blob is detected before any network call.

export AWS_ACCESS_KEY_ID="..." export AWS_SECRET_ACCESS_KEY="..." # AWS_SESSION_TOKEN is also supported for temporary credentials vledger init \ --key-source aws_kms \ --kms-key-id "arn:aws:kms:us-east-1:123456789012:key/mrk-..." \ --kms-region us-east-1
Key Source Backends

File (development only)

Generates a random key and writes it to vledger-data/keys/master_key.hex with mode 0o600.

Not recommended for production. Move to Vault, AWS KMS, or PyHSM before deployment.
vledger init --key-source file

Key Backend Summary

Backend--key-sourceKey never on diskExternal dependency
PyHSM local Model 1pyhsm✓PyHSM daemon on same host
PyHSM remote Model 2remote-pyhsm✓PyHSM daemon on private subnet + TLS certs
Environment variableenv✗ (in env)None
Disk filefile✗None
HashiCorp Vaultvault✓Vault server + token
AWS KMSaws_kms✓AWS credentials + KMS key
Key Source Backends

PyHSM (Recommended)

PyHSM is a VectorGuard Labs product and the default key backend for VectorLedger. The master encryption key is sealed inside PyHSM's AES-256-GCM-SIV encrypted keystore and never touches disk in plaintext.

Two ways to install PyHSM

Option A TypeScript daemon (required for VectorLedger integration)

This is what VectorLedger connects to. The daemon listens on a Unix domain socket and handles all key operations via IPC.

git clone https://github.com/pavondunbar/PyHSM.git ~/PyHSM cd ~/PyHSM/pyhsm-ts npm install # Start the daemon: PYHSM_MASTER_PASSWORD="<your-password>" \ PYHSM_KEYSTORE_PATH="$HOME/PyHSM/pyhsm-keystore.enc" \ PYHSM_AUDIT_LOG_PATH="$HOME/PyHSM/pyhsm-audit.jsonl" \ npx tsx ~/PyHSM/pyhsm-ts/process.ts

Option B Python CLI

The Python package installs the vectorguard-pyhsm command a key management CLI for generating, rotating, and inspecting keys. VectorLedger does not connect to it; use it to administer the keystore independently.

pip install vectorguard-pyhsm # List keys in the keystore vectorguard-pyhsm --store ~/PyHSM/pyhsm-keystore.enc list # Rotate the VectorLedger wrapping key vectorguard-pyhsm --store ~/PyHSM/pyhsm-keystore.enc rotate vledger.master-key # Verify audit log integrity vectorguard-pyhsm --store ~/PyHSM/pyhsm-keystore.enc audit --verify
Summary: Install both if you want the full toolkit. The TypeScript daemon is what VectorLedger needs to run. The Python CLI is what you use to administer the keystore.

PyHSM environment variables

VariableDefaultDescription
PYHSM_MASTER_PASSWORDRequired. Password that unlocks the PyHSM keystore.
PYHSM_KEYSTORE_PATH./pyhsm-keystore.encPath where the encrypted keystore is stored. Set this to a persistent, backed-up location.
PYHSM_AUDIT_LOG_PATH<keystore>.audit.jsonlPath for PyHSM's own tamper-evident audit log.
PYHSM_SOCKET_PATH/tmp/pyhsm.sockUnix socket path the daemon listens on (Model 1).
PYHSM_CALLER_SECRETOptional shared secret for IPC caller authentication.
PYHSM_RATE_LIMIT100Max operations per rate window.
PYHSM_RATE_WINDOW_MS60000Rate window in milliseconds.

How VectorLedger uses PyHSM across restarts

EventWhat happens
First vledger init --key-source pyhsmVectorLedger generates a 32-byte master key in-process, asks PyHSM to encrypt it, stores only the encrypted blob at vledger-data/keys/pyhsm_master_key.enc with an HMAC-SHA256 integrity seal.
Every vledger startHMAC verified locally first, blob sent to PyHSM for decryption, plaintext key used briefly for key derivation then immediately zeroized from memory.
PyHSM daemon not running at startupvledger start fails immediately with a clear error no data is touched.
Cache file tamperedHMAC check fails, startup aborts before any IPC call.
Remote PyHSM unreachable (Model 2)Startup fails closed VectorLedger never falls back to a weaker key source.
HSM Deployment

Model 1 Local PyHSM Same Server

PyHSM and VectorLedger run on the same host. Communication uses a Unix domain socket at /tmp/pyhsm.sock. Zero network overhead. Suitable for development, CI, and single-server production deployments.

┌──────────────────────────────┐ │ Server │ │ │ │ VectorLedger │ │ │ │ │ ▼ /tmp/pyhsm.sock │ │ PyHSM daemon │ └──────────────────────────────┘
vledger init --data-dir ./vledger-data --key-source pyhsm
HSM Deployment

Model 2 Remote PyHSM Separate Server

PyHSM runs on a dedicated server in the same region's private subnet. VectorLedger connects over TLS 1.3 with mutual certificate authentication (mTLS) on port 8443. PyHSM's private key material is never accessible from the VectorLedger host. Recommended for production.

Private subnet (e.g. AWS VPC) ┌────────────────────┐ ┌────────────────────┐ │ Server A │ │ Server B │ │ VectorLedger │──TLS 1.3 ──▶│ PyHSM daemon │ │ encrypted data │ + mTLS │ port 8443 │ │ WAL / application │ │ no public IP │ └────────────────────┘ └────────────────────┘

Model 2 environment variables

VariableDescription
PYHSM_ENDPOINTHTTPS endpoint of the remote PyHSM daemon
PYHSM_CA_CERTPath to the CA certificate PEM
PYHSM_CLIENT_CERTPath to the mTLS client certificate PEM
PYHSM_CLIENT_KEYPath to the mTLS client private key PEM
PYHSM_TIMEOUT_MSPer-request timeout in milliseconds
HSM Deployment

Model 2 Step-by-Step Setup

This section walks through every step required to connect VectorLedger on Server A to PyHSM running on Server B, on the same-region private subnet.

Prerequisites: Both servers are in the same AWS VPC (or equivalent private network). Server B has no public IP. openssl is available on whichever machine you generate certificates on.
1 Generate TLS certificates
# 1a. Create the Certificate Authority openssl genrsa -out ca.key 4096 openssl req -new -x509 -key ca.key -sha256 -days 3650 \ -subj "/CN=VectorLedger-PyHSM-CA" \ -out ca.crt # 1b. Create the PyHSM server certificate # Replace 10.0.1.50 with Server B's actual private IP. openssl genrsa -out pyhsm-server.key 4096 openssl req -new -key pyhsm-server.key \ -subj "/CN=pyhsm.internal" \ -out pyhsm-server.csr openssl x509 -req -in pyhsm-server.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -days 3650 -sha256 \ -extfile <(printf "subjectAltName=IP:10.0.1.50,DNS:pyhsm.internal") \ -out pyhsm-server.crt # 1c. Create the VectorLedger mTLS client certificate openssl genrsa -out vledger-client.key 4096 openssl req -new -key vledger-client.key \ -subj "/CN=vledger-client" \ -out vledger-client.csr openssl x509 -req -in vledger-client.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -days 90 -sha256 \ -out vledger-client.crt
90-day validity on the client cert is intentional. Short-lived client certificates limit the blast radius if the private key is ever exposed. Set a calendar reminder to rotate before expiry.
2 Distribute certificates
# On Server B (PyHSM server): sudo mkdir -p /etc/pyhsm/tls sudo cp ca.crt pyhsm-server.crt pyhsm-server.key /etc/pyhsm/tls/ sudo chmod 600 /etc/pyhsm/tls/pyhsm-server.key # On Server A (VectorLedger server): sudo mkdir -p /etc/vledger/pyhsm sudo cp ca.crt vledger-client.crt vledger-client.key /etc/vledger/pyhsm/ sudo chmod 600 /etc/vledger/pyhsm/vledger-client.key
3 Open the security group
FieldValue
TypeCustom TCP
Port8443
SourceServer A's security group ID (or its private IP /32)

No other inbound rule is needed on Server B. There must be no public IP on Server B and no 0.0.0.0/0 rule for port 8443.

4 Start PyHSM in TLS mode on Server B
PYHSM_MASTER_PASSWORD="<your-pyhsm-password>" \ PYHSM_KEYSTORE_PATH="/var/lib/pyhsm/pyhsm-keystore.enc" \ PYHSM_AUDIT_LOG_PATH="/var/log/pyhsm/pyhsm-audit.jsonl" \ PYHSM_TLS_CERT="/etc/pyhsm/tls/pyhsm-server.crt" \ PYHSM_TLS_KEY="/etc/pyhsm/tls/pyhsm-server.key" \ PYHSM_TLS_CA="/etc/pyhsm/tls/ca.crt" \ PYHSM_LISTEN="0.0.0.0:8443" \ npx tsx ~/PyHSM/pyhsm-ts/process.ts
5 Initialise VectorLedger with remote-pyhsm

Run on Server A. Replace 10.0.1.50 with Server B's private IP:

vledger init \ --data-dir ./vledger-data \ --key-source remote-pyhsm \ --pyhsm-endpoint https://10.0.1.50:8443 \ --pyhsm-ca-cert /etc/vledger/pyhsm/ca.crt \ --pyhsm-client-cert /etc/vledger/pyhsm/vledger-client.crt \ --pyhsm-client-key /etc/vledger/pyhsm/vledger-client.key

Verify key_source.json shows "backend": "remote_py_hsm". If it shows "env" or "file", the connection to PyHSM failed delete the data directory, confirm PyHSM is reachable, and re-run init.

6 Start VectorLedger
vledger start --data-dir ./vledger-data

On every start, VectorLedger connects to the remote PyHSM, decrypts the master key blob, uses the key briefly for derivation, and immediately zeroizes it. If PyHSM is unreachable, startup fails closed it never falls back to a weaker key source.

HSM Deployment

Rotating the Client Certificate

The mTLS client certificate should be rotated before it expires (90-day validity recommended). No data migration is required only the transport credential changes.

# 1. Generate a new client cert signed by the same CA openssl genrsa -out vledger-client-new.key 4096 openssl req -new -key vledger-client-new.key \ -subj "/CN=vledger-client" -out vledger-client-new.csr openssl x509 -req -in vledger-client-new.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -days 90 -sha256 -out vledger-client-new.crt # 2. Copy to Server A sudo cp vledger-client-new.crt /etc/vledger/pyhsm/vledger-client.crt sudo cp vledger-client-new.key /etc/vledger/pyhsm/vledger-client.key sudo chmod 600 /etc/vledger/pyhsm/vledger-client.key # 3. Restart VectorLedger to pick up the new certificate # (no vledger init needed key_source.json paths are unchanged)
HSM Deployment

Replay-Attack Prevention (Model 2)

Every request sent to a remote PyHSM over TLS includes two additional fields that PyHSM should validate:

FieldValuePurpose
requestIdUUID v4PyHSM rejects duplicate IDs within its replay window (recommended: 5 min)
timestampRFC 3339 UTCPyHSM rejects requests more than 2 minutes stale or in the future

These fields are injected automatically by VectorLedger — no configuration required. They are not present on Model 1 (local socket) requests, where replay is not a meaningful threat.

Note: Replay-attack prevention applies to Model 2 (remote PyHSM) only. Ensure PyHSM is configured to validate requestId uniqueness within a 5-minute window and reject stale timestamp values. This is also a requirement in the Production Deployment Checklist.
HSM Deployment

PyHSM on Windows

PyHSM uses TCP instead of a Unix socket on Windows. Start the daemon with PYHSM_TCP_PORT and pass the TCP address to VectorLedger:

$env:PYHSM_MASTER_PASSWORD = "your-password" $env:PYHSM_TCP_PORT = 7777 npx tsx ~/PyHSM/pyhsm-ts/process.ts # Init with Model 1 TCP loopback: vledger init --key-source pyhsm --pyhsm-socket 127.0.0.1:7777
HSM Deployment

Hardware HSM PKCS#11 Integration Enterprise

Enterprise license required. Hardware HSM PKCS#11 integration is available on the Enterprise tier only. Ensure a valid license.json is present in your data directory before proceeding.

VectorLedger does not link a PKCS#11 .so directly. Instead it communicates with a separate PyHSM daemon (a TypeScript/Node.js process) over either a Unix socket (Model 1, same server) or mTLS over TCP (Model 2, separate server). Raw key material never leaves PyHSM — the master key is AES-wrapped inside it, and VectorLedger only ever holds the ciphertext blob on disk.

Step 1 — Ensure an Enterprise license is installed

cp your-license.json ./vledger-data/license.json vledger license --data-dir ./vledger-data # Tier must show: Enterprise

Step 2 — Start the PyHSM daemon

PyHSM must be listening on /tmp/pyhsm.sock (Model 1) before VectorLedger is initialized or started.

PYHSM_MASTER_PASSWORD="<your-password>" \ PYHSM_KEYSTORE_PATH="$HOME/PyHSM/pyhsm-keystore.enc" \ PYHSM_AUDIT_LOG_PATH="$HOME/PyHSM/pyhsm-audit.jsonl" \ npx tsx ~/PyHSM/pyhsm-ts/process.ts # Expected: [PyHSM] Listening on /tmp/pyhsm.sock

Step 3 — Initialize with PyHSM as the key source

# Model 1 — local PyHSM (same server, Unix socket) vledger init --data-dir ./vledger-data --key-source pyhsm
# Model 2 — remote PyHSM over mTLS (separate server) vledger init --data-dir ./vledger-data \ --key-source remote-pyhsm \ --pyhsm-endpoint https://pyhsm.internal.example.com:8443 \ --pyhsm-ca-cert /etc/vledger/pyhsm/ca.pem \ --pyhsm-client-cert /etc/vledger/pyhsm/client.pem \ --pyhsm-client-key /etc/vledger/pyhsm/client-key.pem

This writes vledger-data/keys/key_source.json — for Model 1 it looks like:

{ "backend": "py_hsm", "socket_path": "/tmp/pyhsm.sock", "caller_id": "vledger", "key_id": "vledger.master-key" }

Step 4 — Start the server normally

On every boot VectorLedger sends the cached blob to PyHSM to unwrap. If PyHSM is not running at startup, VectorLedger fails immediately — it never falls back silently to a weaker key source.

vledger start --data-dir ./vledger-data --with-proofs

Dedicated hardware backends (AWS CloudHSM & Azure Dedicated HSM)

For actual hardware HSMs, bridge sidecars translate the JSON IPC protocol to hardware PKCS#11 calls:

HardwareBridge sidecar
AWS CloudHSMvledger-hsm-aws-bridge
Azure Dedicated HSM (Thales Luna Network HSM 7)vledger-hsm-azure-bridge

Configure the bridge via vledger-data/keys/hsm_config.json. Example for AWS CloudHSM:

{ "backend": "aws_cloud_hsm", "bridge_socket": "~/.vledger-hsm-aws/bridge.sock", "cluster_id": "cluster-xxxxxxxx", "crypto_user": "vgdb-cu", "verify_bridge_tls": true }

Raw key material never crosses the hardware boundary on either cloud provider — all cryptographic operations run inside the device. Contact engineering@vectorguardlabs.com for bridge sidecar setup guides.

Operations

Compliance Reporting

Important scope note: VectorLedger generates machine-generated technical evidence supporting SOC 2 and PCI-DSS control assessments. This evidence is a technical input to an audit — it does not by itself make an organization compliant. Organizational compliance requires additional policies, procedures, personnel controls, and independent auditor assessment.

SOC 2 Type II controls covered

CC6.1, CC6.2, CC6.3, CC6.6, CC6.7, CC7.2, CC8.1, A1.1

PCI-DSS v4 controls covered

Req 2.2, 3.4, 3.5, 4.2, 7.1, 10.2, 10.3, 10.5, 11.5

Generate reports

# SOC 2 report as Markdown vledger compliance-report --data-dir ./vledger-data \ --standard soc2 --format markdown --output soc2-report.md # PCI-DSS report as JSON vledger compliance-report --data-dir ./vledger-data \ --standard pci-dss --format json --output pci-report.json
Operations

Cryptographic Audit Package

VectorLedger can generate a portable, self-contained cryptographic audit evidence package that any third party can verify independently — no database access, no server, no credentials required.

Three-tier design:

Tier 1 — Commitment package (default, fast at any scale)

Computes the Merkle root over all entries in a single O(n) pass, signs it with the database Ed25519 key, and writes a compact JSON commitment. Completes in seconds regardless of ledger size.

vledger audit-package \ --data-dir ./vledger-data \ --tenant "Acme Financial" \ --description "Q3 2026 regulatory audit" \ --period-start 2026-07-01 \ --period-end 2026-09-30 \ --output audit-q3-2026.json

Tier 2 — On-demand entry proof (prove one specific entry)

Generates a single Merkle inclusion proof proving that a specific entry belongs to the committed root. The auditor receives a self-contained file they can verify without database access.

vledger audit-proof --data-dir ./vledger-data \ --commitment audit.json \ --sequence 406340 \ --output entry-proof.json

Tier 3 — Full export (small ledgers only)

Embeds all entries and per-entry proofs. Only practical for ledgers with fewer than ~10,000 entries.

vledger audit-package --data-dir ./vledger-data --include-entries

Verification (no database access required)

vledger verify-audit-package --file audit.json vledger verify-audit-package --file entry-proof.json

Example output:

[1/3] Content hash ✓ [2/3] Chain hash ✓ [3/3] Merkle proof ✓ ✓ VERIFIED — 1 entries, all checks passed. Merkle root : 804efb54ea31539a...

Auditors who cannot install software can use the pre-built binary from the GitHub release (no Rust toolchain required):

curl --proto '=https' --tlsv1.2 -sSf \ https://raw.githubusercontent.com/pavondunbar/VectorLedger/main/install.sh | bash vledger verify-audit-package --file entry-proof.json
SQL Reference

SQL Reference

VectorLedger supports a financial-ledger SQL dialect over both the native TLS connection (port 5433) and the PostgreSQL wire protocol (port 5432). It is PostgreSQL-compatible — not PostgreSQL — so standard PostgreSQL system catalog queries (\l, \dt, pg_catalog.*) are not supported.

Scan Safety — Default Row Cap

Unbounded full-table scans on a large ledger load all matching rows into memory before returning, which can exhaust server RAM. VectorLedger protects against this with an automatic 10,000-entry cap on non-point-lookup queries that have no explicit LIMIT.

Query patternBehaviour
SELECT * FROM ledger WHERE sequence = NNo cap — returns exactly 1 entry
SELECT * FROM ledger WHERE external_ref = 'X'No cap — point lookup
SELECT * FROM ledger LIMIT 500Exactly 500 rows — explicit limit honoured
SELECT * FROM ledgerCapped at 10,000 rows + pagination notice
SELECT * FROM ledger WHERE domain = 'x'Capped at 10,000 rows + pagination notice
SELECT * FROM ledger WHERE status = 'Posted'Capped at 10,000 rows + pagination notice

When the cap fires you will see: 10000 rows (capped at 10000 — use LIMIT n or WHERE sequence = x to paginate)

To page through a large dataset, use sequence-based pagination:

-- Page 1: entries 1 – 10,000 SELECT * FROM ledger LIMIT 10000; -- Page 2: entries 10,001 – 20,000 SELECT * FROM ledger WHERE sequence > 10000 LIMIT 10000; -- Page 3: entries 20,001 – 30,000 SELECT * FROM ledger WHERE sequence > 20000 LIMIT 10000;
SQL Reference

Tables

ledger — one row per journal entry

Columns: sequence, id, status, description, domain, effective_at, posted_at, external_ref, content_hash, chain_hash, lines, metadata

The lines column contains all debit and credit lines as a single semicolon-separated string. Use ledger_lines for the traditional accounting view.

The metadata column is an optional JSON blob for arbitrary extra fields (e.g. sender name, channel, original transaction ID). It is hash-protected — included in the entry's content hash so any post-hoc modification breaks the chain. Example value: {"sender_name":"John Smith","channel":"wire","status":"settled"}

Entry status values: Posted, Reversed, Reversal, PendingApproval, Rejected, Pending, Settled, Failed. The last three are part of the settlement lifecycle and are derived from append-only SettlementEvent records — the original entry row is never mutated.

-- Point lookups (no cap — always safe) SELECT * FROM ledger WHERE sequence = 406340; SELECT * FROM ledger WHERE external_ref = 'TXN-001'; -- Bounded queries (always safe) SELECT * FROM ledger LIMIT 100; SELECT * FROM ledger WHERE domain = 'main' LIMIT 500; -- Full scans (auto-capped at 10,000 — paginate for more) SELECT * FROM ledger; SELECT * FROM ledger WHERE status = 'Posted';

ledger_lines — one row per debit/credit line

Returns each debit and credit as its own row — the standard double-entry accounting view that accountants expect.

Columns: date, sequence, entry_id, description, domain, account_id, dr_cr, amount, currency, status, metadata

Amounts are displayed as integers in minor units (e.g. 100000 = $1,000.00 for USD). The metadata column mirrors the entry-level metadata JSON blob. The cap applies to the number of entries before line expansion — a cap of 10,000 entries yields up to 20,000 rows (two lines per entry) for a standard two-line transaction.

-- Point lookups (no cap — always safe) SELECT * FROM ledger_lines WHERE sequence = 406340; -- Bounded queries (always safe) SELECT * FROM ledger_lines LIMIT 100; SELECT * FROM ledger_lines WHERE domain = 'main' LIMIT 500; -- Full scans (auto-capped at 10,000 entries) SELECT * FROM ledger_lines; SELECT * FROM ledger_lines WHERE status = 'Posted';

Example output:

date | sequence | entry_id | description | domain | account_id | dr_cr | amount | currency | status -----------+----------+----------+------------------+--------+------------+--------+--------+----------+------- 2026-08-17 | 1 | <uuid> | Customer payment | main | <uuid> | Debit | 1.00 | USD | Posted 2026-08-17 | 1 | <uuid> | Customer payment | main | <uuid> | Credit | 1.00 | USD | Posted

accounts — chart of accounts

Columns: id, code, name, account_type, currency, status, domain, balance, legal_hold

The legal_hold column is true when the account is under a legal hold. While active, no entries, reversals, or settlement transitions involving this account are permitted.

SELECT * FROM accounts; SELECT * FROM accounts WHERE domain = 'main';
SQL Reference

Write Commands

Post a journal entry

INSERT INTO ledger (description, debit_account, credit_account, amount, currency, domain) VALUES ('Wire transfer', 'CASH', 'REVENUE', 100000, 'USD', 'main');
  • amount is in minor units (cents for USD — 100000 = $1,000.00)
  • debit_account and credit_account accept either account code or UUID
  • Optional fields: external_ref, idempotency_key
  • Entries are append-only — UPDATE and DELETE are not supported

Create an account

INSERT INTO accounts (code, name, account_type, currency, domain) VALUES ('CASH', 'Cash - USD', 'asset', 'USD', 'main'); -- Account types: asset, liability, equity, income, expense

Post a correction (reversal)

Corrections are made by posting a new reversal entry, never by modifying the original:

INSERT INTO ledger (description, debit_account, credit_account, amount, currency, domain) VALUES ('Reversal of TXN-001', 'REVENUE', 'CASH', 100000, 'USD', 'main');

Full reversal & correction workflow

The recommended 5-step process for correcting a posted entry:

-- Step 1: Find the entry to reverse SELECT * FROM ledger WHERE sequence = 19678432; SELECT * FROM ledger_lines WHERE sequence = 19678432; -- Step 2: Look up account codes from the UUIDs in ledger_lines SELECT id, code, name, balance FROM accounts WHERE id = 'c0a89fbd-8eea-45a9-9e5c-0893c1cafe08'; SELECT id, code, name, balance FROM accounts WHERE id = 'cd1f97bd-f93e-4596-866a-0f2f4a0de6ad'; -- Step 3: Post the reversal (flip debit and credit) INSERT INTO ledger (description, debit_account, credit_account, amount, currency, domain, external_ref, metadata) VALUES ( 'Reversal of sequence 19678432', '6f1c6012-b5df-461f-9259-bc790a063643', 'f93081c2-d1f0-4dd4-9b48-4f68f6d0023e', 44602, 'USD', 'main', 'reversal-of-f0da247e-8b29-4556-aaa4-cf424626adcc', '{"reverses":"f0da247e-8b29-4556-aaa4-cf424626adcc","reason":"correction"}' ); -- Step 4: Post the correction (with correct details) INSERT INTO ledger (description, debit_account, credit_account, amount, currency, domain, external_ref, metadata) VALUES ( 'Corrected transfer', 'f93081c2-d1f0-4dd4-9b48-4f68f6d0023e', '6f1c6012-b5df-461f-9259-bc790a063643', 44700, 'USD', 'main', 'correction-of-f0da247e-8b29-4556-aaa4-cf424626adcc', '{"corrects":"f0da247e-8b29-4556-aaa4-cf424626adcc"}' ); -- Step 5: Verify chain integrity SELECT VERIFY_CHAIN();

The original entry is never modified or deleted. All three entries (original, reversal, correction) remain permanently in the ledger. A reversal without a correction is also valid — post only the reversal if the transaction should simply not exist.

SQL Reference

Financial Functions

-- Account balance (returns minor units) SELECT BALANCE('CASH'); SELECT BALANCE('account-uuid-here'); -- Verify the entire BLAKE3 hash chain SELECT VERIFY_CHAIN(); -- Verify a range of entries SELECT VERIFY_CHAIN(1, 100000); SELECT VERIFY_CHAIN(1000000); -- Verify a single entry's content and chain hashes SELECT VERIFY_ENTRY(406340); -- Merkle root over a sequence range (v1.0.36+) — no --with-proofs flag required -- Privilege: admin, operator, or auditor role required (readonly cannot call MERKLE_ROOT) SELECT MERKLE_ROOT(786000, 786500); -- Merkle root for a single entry (v1.0.37+) — to_seq defaults to from_seq SELECT MERKLE_ROOT(786295);
MERKLE_ROOT() does not require --with-proofs. It is available at all times as a standard SQL function. The --with-proofs flag is a separate feature that auto-attaches a Merkle proof NOTICE to every SELECT response — use it only if you want that automatic behaviour. MERKLE_ROOT() returns the same BLAKE3 root values as --with-proofs and vledger audit-package — the three methods are directly comparable.
SQL Reference

Aggregates & Window Functions

SELECT COUNT(sequence) FROM ledger; SELECT SUM(amount) FROM ledger GROUP BY domain; SELECT AVG(amount) FROM ledger; SELECT MIN(sequence), MAX(sequence) FROM ledger; SELECT ROW_NUMBER() OVER () AS rn FROM ledger; SELECT RANK() OVER () FROM ledger;
SQL Reference

Joins

Supports INNER JOIN and LEFT OUTER JOIN.

SELECT * FROM ledger JOIN accounts ON ledger.domain = accounts.domain;
SQL Reference

Metadata Search

Every entry carries a metadata field — an arbitrary JSON blob (e.g. {"sender_name":"Alice","receiver_name":"Bob","channel":"mobile"}). From v1.0.30, metadata is indexed using a SQLite FTS5 full-text index, making search instant regardless of ledger size.

FTS index is automatic. No configuration needed. Queries using LIKE or ILIKE on the metadata column automatically use the FTS5 index. On first startup after upgrading to v1.0.30 on an existing database, the index is rebuilt automatically in the background.
-- Find all entries involving a person by name (uses FTS5 index) SELECT * FROM ledger WHERE metadata LIKE '%Elizabeth Cadet%'; SELECT * FROM ledger_lines WHERE metadata LIKE '%Elizabeth Cadet%'; -- Case-insensitive search SELECT * FROM ledger WHERE metadata ILIKE '%elizabeth cadet%'; -- Search for a specific role in the metadata SELECT * FROM ledger WHERE metadata LIKE '%"receiver_name":"Elizabeth Cadet"%'; SELECT * FROM ledger WHERE metadata LIKE '%"sender_name":"Elizabeth Cadet"%'; -- Exact full metadata match SELECT * FROM ledger WHERE metadata = '{"channel":"mobile","receiver_name":"Elizabeth Cadet","sender_name":"Basma Ammar","status":"completed","transaction_type":"fee"}';
  • Supported on both ledger and ledger_lines. Results are ordered by sequence.
  • Works with any metadata field name — future CSV imports with different column layouts (e.g. beneficiary, payee) are automatically searchable without schema changes.
  • LIKE performs case-sensitive matching; ILIKE is case-insensitive.
SQL Reference

Multiple Point Lookups (IN)

Use IN to fetch multiple specific entries or accounts in a single query. These are treated as point lookups — no scan cap applies.

-- Multiple ledger entries SELECT sequence, content_hash, chain_hash FROM ledger WHERE sequence IN (1, 2, 3); SELECT * FROM ledger WHERE sequence IN (19678432, 25000001, 25000002, 25000003); -- Multiple ledger_lines entries SELECT * FROM ledger_lines WHERE sequence IN (19678432, 25000001, 25000002); -- Multiple accounts by code SELECT * FROM accounts WHERE code IN ('0601095315', '0036011743', '1275341255'); -- Multiple accounts by UUID SELECT * FROM accounts WHERE id IN ('c0a89fbd-...', 'cd1f97bd-...', 'ae262006-...');
NOT IN is not supported. Use separate queries instead — e.g. fetch all sequences and filter client-side, or use a WHERE sequence < x OR sequence > y range approach.
SQL Reference

Compatibility Queries

These queries are supported for ORM and connection pooler health checks:

SELECT 1; SELECT version(); SELECT current_user(); SELECT current_database();
SQL Reference

What Is Not Supported

OperationWhy
UPDATEAppend-only — entries are permanent
DELETEAppend-only — entries are permanent
CREATE TABLE / DROP TABLESchema is fixed
pg_catalog.* system tablesNot PostgreSQL internally
\l, \dt, \du psql meta-commandsRely on pg_catalog
Multiple databases or schemasSingle-database engine
Stored procedures, triggers, sequencesNot implemented
Operations

Replication

VectorLedger supports synchronous hot-standby WAL replication with three independent security layers: TLS 1.3, optional mTLS, and BLAKE3-keyed HMAC challenge-response inside TLS.

License requirement: WAL replication requires a Growth or Enterprise license. Running vledger start-primary or vledger start-replica on a Free or Starter license returns a feature-not-entitled error.

Additional integrity guarantees:

  • Replica verifies the BLAKE3 hash of every received WAL record before applying it — a tampered record is rejected and the connection closed.
  • Exponential reconnect backoff (500 ms → 30 s) with faster escalation on auth failures (max 60 s).
  • Replication secret generated from OsRng, stored at mode 0o600.
  • Divergence detection via periodic DivergenceCheckpoint messages carrying a rolling BLAKE3 WAL chain hash — a mismatch means the replica must be re-seeded.
Operations

Replication Setup

1 Start the WAL shipper on the primary
# Option A — integrated mode (recommended): # If replication.json is present with role=primary, vledger start # automatically activates the WAL shipper alongside the SQL server. vledger start --data-dir ./vledger-data # Option B — standalone shipper only (no SQL server): vledger start-primary --data-dir ./vledger-data # Override the bind address: vledger start-primary --data-dir ./vledger-data --bind 0.0.0.0:5434

The primary auto-generates a 32-byte HMAC secret at vledger-data/replication_secret.hex (mode 0o600) on first run. This secret must be copied to every replica.

2 Copy the secret to the replica
scp vledger-data/replication_secret.hex replica-host:/path/to/vledger-data/replication_secret.hex
3 Create replication.json on the replica
{ "role": "replica", "replication_addr": "<primary-host>:5434", "ack_timeout_ms": 5000, "heartbeat_interval_ms": 1000, "send_buffer_bytes": 67108864, "tls": { "enabled": true, "server_hostname": "vledger-primary", "ca_cert": "/path/to/replication-ca.pem" } }
Development note: For development on a single machine with a self-signed primary cert, set "ca_cert": null and build with the dev-insecure-replication Cargo feature. Do not do this in production.
4 Start the replica
# Start the replica (connects to primary and streams WAL continuously) vledger start-replica --data-dir ./vledger-data # Override the primary address vledger start-replica --data-dir ./vledger-data --primary <primary-host>:5434

The replica connects to the primary, performs the HMAC handshake, and begins streaming WAL records. It reconnects automatically on disconnection.

Operations

replication.json Reference

FieldDefaultDescription
role"primary""primary" or "replica"
replication_addr"127.0.0.1:5434"Primary: bind address. Replica: primary's host:port
ack_timeout_ms5000How long the primary waits for a replica ACK (ms)
heartbeat_interval_ms1000Heartbeat frequency (ms)
send_buffer_bytes67108864Max bytes buffered per replica connection (64 MiB)
secret_pathnullPath to HMAC secret; null = <data_dir>/replication_secret.hex
tls.enabledtrueEnable TLS on the replication channel
tls.server_certnullPrimary TLS cert PEM; null = auto-generate self-signed
tls.server_keynullPrimary TLS key PEM; null = auto-generate
tls.server_hostname"vledger-primary"SNI hostname used by replica to verify the primary's cert
tls.ca_certnullReplica: CA cert PEM to verify the primary. Required in production
tls.client_certnullReplica client cert PEM for mTLS (optional)
tls.client_keynullReplica client key PEM for mTLS (optional)
Operations

Replication Limitations

Failover promotion is manual. There is no automatic primary election. If the primary crashes, an operator must explicitly reconfigure a replica as the new primary. Automated promotion via consensus (Raft/Paxos) is planned but not yet implemented.
Split-brain prevention is network-layer only. VectorLedger relies on the operator's network segmentation (e.g. AWS security groups, private subnets) to prevent two nodes from simultaneously acting as primary. There is no fencing or STONITH mechanism in the replication layer itself.
Replica lag is observable but not bounded. Heartbeat ACKs carry the replica's last applied LSN, allowing the primary to compute lag. There is no automatic write-pause when lag exceeds a threshold.

These limitations do not affect the security properties of the WAL integrity or tamper-evidence guarantees.

Operations

Multi-Node Deployment (WAL Replication) Growth+

Growth or Enterprise license required. If replication.json exists in the data directory when vledger start is run, the license check for the Replication feature fires immediately — it will not start at all without a valid Growth+ license.

The primary runs a WAL shipper on port 5434; replicas connect and stream WAL records in real time. A shared 32-byte HMAC secret authenticates the channel inside TLS 1.3.

On the primary — create replication.json

# /opt/vledger-primary/replication.json { "role": "primary", "replication_addr": "0.0.0.0:5434", "ack_timeout_ms": 5000, "heartbeat_interval_ms": 1000, "tls": { "enabled": true, "server_hostname": "vledger-primary", "server_cert": "/etc/vledger/replication/server.pem", "server_key": "/etc/vledger/replication/server-key.pem", "ca_cert": "/etc/vledger/replication/ca.pem" } }

Start the primary

vledger start-primary --data-dir /opt/vledger-primary

On first run this auto-generates replication_secret.hex (mode 0o600). Copy that file to the replica before starting it:

scp /opt/vledger-primary/replication_secret.hex \ replica-host:/opt/vledger-replica/replication_secret.hex

On the replica — create replication.json

# /opt/vledger-replica/replication.json { "role": "replica", "replication_addr": "primary-host:5434", "ack_timeout_ms": 5000, "tls": { "enabled": true, "server_hostname": "vledger-primary", "ca_cert": "/etc/vledger/replication/ca.pem" } }

Start the replica

vledger start-replica --data-dir /opt/vledger-replica

The replica connects to the primary, performs a BLAKE3 HMAC challenge-response inside TLS, then streams WAL records continuously. It verifies the BLAKE3 hash of every record before writing and reconnects automatically with exponential back-off (500 ms → 30 s) on disconnection.

Missing secret = startup failure. If replication_secret.hex is absent on the replica, startup fails with a clear error. Copy the file from the primary before starting the replica.
What the replica verifiesHow
Channel authenticityBLAKE3-keyed HMAC challenge-response inside TLS 1.3
Per-record integrityBLAKE3 hash verified before each record is applied
Divergence detectionPeriodic DivergenceCheckpoint messages carry a rolling BLAKE3 WAL chain hash — mismatch triggers re-seed
Operations

Client SDKs

Native client libraries are included for three languages, all located under clients/ in the repository.

LanguageLocation
Pythonclients/python/
TypeScript / Node.jsclients/typescript/
Goclients/go/

Any standard PostgreSQL client library can also connect to the PostgreSQL wire-protocol listener on port 5432 when the server is started with --pgwire (Starter tier and above).

Operations

Licensing

VectorLedger uses a tiered license model. The binary enforces feature availability at startup by verifying a signed license.json file in your data directory. If no license file is present, the engine runs in Free tier mode.

Licenses are issued by VectorGuard Labs. Purchase at vledger.vectorguardlabs.com — your license.json is generated automatically and delivered to the email on your account.

Pricing

TierPriceBest for
Free$0 / monthDevelopment, evaluation, internal tools
Starter$499 / monthEarly-stage teams that need PostgreSQL client compatibility
Growth$2499 / monthProduction fintechs and SaaS companies under SOC 2 or PCI-DSS
EnterpriseContact SalesBanks, payment processors, PCI-DSS Level 1, hardware HSM requirements

Annual billing available on all paid tiers — pay for 10 months, get 12. Contact pavon@vectorguardlabs.com for multi-instance or custom pricing.

Installing a license

cp acme-license.json ./vledger-data/license.json vledger license --data-dir ./vledger-data

Feature availability by tier

FeatureFreeStarterGrowthEnterprise
Core ledger + SQL REPL✓✓✓✓
AES-256-GCM encryption at rest✓✓✓✓
BLAKE3 hash chain + Merkle proofs✓✓✓✓
Four-eyes dual-control workflow✓✓✓✓
WORM audit log + chain verification✓✓✓✓
Backup & restore✓✓✓✓
Audit log export (date range)30 days90 daysUnlimitedUnlimited
PostgreSQL wire protocol (--pgwire)✗✓✓✓
MCP server + AgenticAI✗10 queries/mo100 queries/moUnlimited
Natural-language queries (--ask)✗✓✓✓
WAL replication (hot standby)✗✗✓✓
Compliance reports (SOC 2 / PCI-DSS)✗✗✓✓
Hardware HSM PKCS#11 integration✗✗✗✓
Multi-node deployment✗✗✗✓

Annual billing available on all paid tiers pay for 10 months, get 12. Contact pavon@vectorguardlabs.com for multi-instance or custom pricing.

Operations

Production Deployment Checklist

Before putting VectorLedger in front of production traffic:

Core setup

  • PyHSM daemon running with a persistent, backed-up keystore (PYHSM_KEYSTORE_PATH points to a durable location)
  • vledger init completed with --key-source pyhsm (Model 1) or --key-source remote-pyhsm (Model 2)
  • key_source.json shows "backend": "py_hsm" or "backend": "remote_py_hsm" not "env" or "file"
  • keys/MASTER_KEY_PLACEHOLDER.txt deleted (its presence fails the PCI-DSS compliance check)
  • Admin credential file read, password changed, and catalog/.admin_initial_credentials deleted
  • Data directory permissions locked (chmod 700 on vledger-data/ and all subdirectories)
  • Volume encryption enabled on the disk hosting vledger-data/
  • Replace the self-signed TLS certificate with a CA-signed one pass --tls-cert-path and --tls-key-path at startup
  • Configure replication with a secondary node (replication.json) or document a backup-based HA strategy
  • Install a valid license.json for your paid tier (vledger license --data-dir ./vledger-data to confirm)
  • Test a full backup and restore drill: vledger backup → vledger restore → vledger verify
  • Schedule regular vledger backup runs (cron or your orchestrator)
  • Schedule regular vledger verify runs (recommended: after each backup)
  • Ship audit/audit.log to an append-only off-host destination in real time
  • Run compliance reports and confirm zero FAIL items: vledger compliance-report --standard pci-dss

Additional items for Model 2 (remote PyHSM)

  • PyHSM server has no public IP accessible only via private subnet
  • Security group / firewall allows VectorLedger → PyHSM (port 8443) only no other inbound
  • CA certificate, client certificate, and client key stored at paths that survive reboots and are mode 0600
  • PYHSM_CA_CERT, PYHSM_CLIENT_CERT, PYHSM_CLIENT_KEY environment variables set (or paths baked into key_source.json)
  • mTLS client certificate has a short validity period (90 days recommended) with a rotation schedule
  • PyHSM configured to validate requestId (reject duplicates within 5-minute window) and reject stale timestamp values
  • Verified that vledger start fails cleanly when PyHSM is unreachable never falls back silently
Operations

Testing & Verification

This section documents every testing mechanism available in VectorLedger. Run these commands on your own server to independently verify correctness, durability, and tamper-evidence before deploying to production.

Testing

Prerequisites

Start the server before running any tests that require a live connection:

export VectorLedger_MASTER_KEY=$(openssl rand -hex 32) ./target/release/vledger init --key-source env cat vledger-data/catalog/.admin_initial_credentials # note the generated password ./target/release/vledger start --max-connections 200 --pgwire & sleep 5
Testing

1. Benchmark Tests (TPS)

Measures transactions per second across INSERT, SELECT, and mixed workloads. Restart the server between each workload run to clear connection state.

# INSERT workload - write heavy cargo run --release --package vledger-bench -- \ --username admin --password <password> \ --clients 50 --transactions 5000 --workload insert
# Restart between runs pkill vledger && sleep 2 && ./target/release/vledger start --max-connections 200 --pgwire & sleep 5
# SELECT workload - read heavy cargo run --release --package vledger-bench -- \ --username admin --password <password> \ --clients 50 --transactions 5000 --workload select
# MIXED workload - 70% INSERT, 30% SELECT cargo run --release --package vledger-bench -- \ --username admin --password <password> \ --clients 50 --transactions 5000 --workload mixed
Recommended instance: AWS c7g.xlarge (Graviton3, 4 vCPU, 8 GB RAM). Avoid t3/t4g burstable instances - CPU credit throttling produces misleading results.
Testing

2. PostgreSQL Wire Protocol Compatibility

Verifies that VectorLedger accepts connections from standard PostgreSQL clients. Start the server with --pgwire and connect with psql:

psql "host=127.0.0.1 port=5432 user=admin dbname=vledger sslmode=require"
SELECT 1; SELECT version(); SELECT current_user(); SELECT current_database(); SHOW server_version; SHOW server_encoding; SHOW TimeZone; SELECT COUNT(*) FROM ledger; SELECT * FROM ledger LIMIT 5; SELECT * FROM accounts LIMIT 5; SELECT VERIFY_CHAIN(); BEGIN; SELECT COUNT(*) FROM ledger; COMMIT;
Testing

3. Concurrent Transaction Test

Runs the benchmark in one terminal while querying live from another to confirm no torn reads, duplicate sequences, or chain failures under concurrent load.

Terminal 1

cargo run --release --package vledger-bench -- \ --username admin --password <password> \ --clients 10 --transactions 1000 --workload mixed &

Terminal 2 (while benchmark runs)

psql "host=127.0.0.1 port=5432 user=admin dbname=vledger sslmode=require"
SELECT COUNT(*) FROM ledger; SELECT COUNT(DISTINCT sequence) FROM ledger; -- must equal COUNT(*) SELECT VERIFY_CHAIN(); -- must return OK
Testing

4. WAL Corruption Test

Confirms that VectorLedger detects and rejects corrupted WAL data during recovery.

pkill vledger && sleep 2 # Corrupt a byte in the last WAL segment python3 -c " import os wal_dir = 'vledger-data/wal' segments = sorted(os.listdir(wal_dir)) target = os.path.join(wal_dir, segments[-1]) size = os.path.getsize(target) mid = size // 2 with open(target, 'r+b') as f: f.seek(mid) b = f.read(1) f.seek(-1, 1) f.write(bytes([b[0] ^ 0xFF])) print('WAL corruption written at offset', mid) " # Attempt restart - server will reject or truncate at the corrupt record nohup ./target/release/vledger start --max-connections 200 --pgwire & sleep 10 cat nohup.out | tail -10 # Restore the WAL (XOR with 0xFF again to flip back) python3 -c " import os wal_dir = 'vledger-data/wal' segments = sorted(os.listdir(wal_dir)) target = os.path.join(wal_dir, segments[-1]) size = os.path.getsize(target) mid = size // 2 with open(target, 'r+b') as f: f.seek(mid) b = f.read(1) f.seek(-1, 1) f.write(bytes([b[0] ^ 0xFF])) print('WAL restored at offset', mid) " # Restart and verify full recovery pkill vledger && sleep 2 nohup ./target/release/vledger start --max-connections 200 --pgwire & sleep 120 # wait for WAL replay psql "host=127.0.0.1 port=5432 user=admin dbname=vledger sslmode=require"
SELECT COUNT(*) FROM ledger; SELECT VERIFY_CHAIN();
Testing

5. Logical Tampering Test

Confirms that the BLAKE3 hash chain detects in-memory data manipulation. TAMPER_ENTRY mutates an entry's description without updating its hash - simulating what a malicious actor would need to do to falsify a record.

-- Establish baseline SELECT VERIFY_CHAIN(); -- Tamper with a specific entry SELECT TAMPER_ENTRY(999999, 'THIS RECORD HAS BEEN FALSIFIED'); -- Hash chain must now detect the mutation SELECT VERIFY_CHAIN(); -- Expected: ERROR: INTEGRITY FAILURE: Hash chain broken at sequence 999999 -- Confirm the specific entry is marked corrupted SELECT VERIFY_ENTRY(999999); -- Expected: status = CORRUPTED
Testing

6. Crash / Restart Recovery Test

Confirms that committed transactions survive a hard kill mid-write and that uncommitted transactions are rolled back cleanly.

# Start benchmark in background cargo run --release --package vledger-bench -- \ --username admin --password <password> \ --clients 10 --transactions 10000 --workload insert & BENCH_PID=$! # Hard-kill the server while writes are in flight sleep 10 kill -9 $(pgrep -f "vledger start") echo "Server killed mid-write" wait $BENCH_PID # Restart - WAL replay recovers all committed transactions nohup ./target/release/vledger start --max-connections 200 --pgwire & sleep 120 psql "host=127.0.0.1 port=5432 user=admin dbname=vledger sslmode=require"
SELECT COUNT(*) FROM ledger; SELECT VERIFY_CHAIN(); -- Chain must be OK. Some in-flight transactions may be missing (expected). -- All committed transactions must be present and valid.
Testing

7. Integrity Self-Test Suite

The built-in self-test runs five automated phases against a completely isolated temporary database. Your production data is never touched.

# Quick smoke test - 1K entries, instant ./target/release/vledger verify --self-test --entries 1000 2>/dev/null # Dev run - 10K entries, ~5 seconds ./target/release/vledger verify --self-test --entries 10000 2>/dev/null # Standard - 100K entries, ~30-60 seconds (default) ./target/release/vledger verify --self-test 2>/dev/null # Enterprise stress test - 1M entries, ~10-15 minutes ./target/release/vledger verify --self-test --entries 1000000 2>/dev/null # Keep the test database for manual inspection ./target/release/vledger verify --self-test --entries 10000 --keep-data 2>/dev/null

What the self-test verifies

PhaseWhat it tests
A - BaselineInserts N deterministic entries with varied amounts, verifies the hash chain immediately
B - WAL IntegrityCorrupts a WAL byte, confirms server detects and rejects it, restores the byte
C - Crash RecoveryReopens the database, confirms 100% of entries recovered with chain intact
D - Logical IntegrityMutates an entry in memory without updating its hash, confirms VERIFY_CHAIN() detects it
E - Entry VerificationSpot-checks five entries spread across the ledger with VERIFY_ENTRY()

Inspecting the self-test database manually

# Run with --keep-data to retain the database after the test ./target/release/vledger verify --self-test --entries 10000 --keep-data 2>/dev/null # Note the directory printed at the end, then start a server against it cat /path/to/vledger-self-test-<timestamp>/catalog/.admin_initial_credentials ./target/release/vledger start \ --data-dir /path/to/vledger-self-test-<timestamp> \ --max-connections 10 --pgwire & sleep 5 psql "host=127.0.0.1 port=5432 user=admin dbname=vledger sslmode=require"
SELECT COUNT(*) FROM ledger; SELECT VERIFY_CHAIN(); SELECT VERIFY_ENTRY(1); SELECT VERIFY_ENTRY(5000); SELECT VERIFY_ENTRY(10000); \x SELECT * FROM ledger ORDER BY sequence LIMIT 10; SELECT * FROM ledger WHERE sequence = 5000; SELECT * FROM ledger_lines WHERE sequence = 5000;
Testing

8. Chain Range Verification

Verify a specific range of entries rather than the full chain:

-- Verify entries 1 through 100,000 SELECT VERIFY_CHAIN(1, 100000); -- Verify from 1,000,000 to end SELECT VERIFY_CHAIN(1000000); -- Verify the full chain SELECT VERIFY_CHAIN();
Testing

9. Direct SQL Queries (without psql)

Query the production database directly from the terminal without starting psql:

./target/release/vledger sql --query "SELECT COUNT(*) FROM ledger" ./target/release/vledger sql --query "SELECT VERIFY_CHAIN()" ./target/release/vledger sql --query "SELECT * FROM ledger WHERE sequence = 500000" ./target/release/vledger sql --query "SELECT VERIFY_ENTRY(500000)" ./target/release/vledger sql --query "SELECT VERIFY_CHAIN(1000000, 1100000)" ./target/release/vledger sql --query "SELECT * FROM ledger LIMIT 10" ./target/release/vledger sql --query "SELECT * FROM ledger_lines WHERE sequence = 500000"

Each vledger sql invocation opens a fresh TLS connection, authenticates, runs the query, and closes. There is no persistent session between calls, so credentials are prompted every time by default.

To avoid retyping credentials on every query, set environment variables:

export VLEDGER_CLI_USERNAME=admin export VLEDGER_CLI_PASSWORD=<your-password> # All subsequent queries run without prompting ./target/release/vledger sql --query "SELECT COUNT(*) FROM ledger" ./target/release/vledger sql --query "SELECT VERIFY_CHAIN()" ./target/release/vledger sql --query "SELECT VERIFY_ENTRY(500000)"

Alternatively, use psql for an interactive session - authenticate once and run as many queries as you want:

psql "host=127.0.0.1 port=5432 user=admin dbname=vledger sslmode=require" # Type \q to exit
Testing

10. Audit Package (Cryptographic Evidence Export)

Generate a portable audit evidence package and verify it independently — no database access required for verification.

# Generate commitment (fast — works at any scale) ./target/release/vledger audit-package \ --data-dir ./vledger-data \ --output audit-commitment.json # Prove a specific entry to an auditor ./target/release/vledger audit-proof \ --data-dir ./vledger-data \ --commitment audit-commitment.json \ --sequence 500000 \ --output entry-500000-proof.json # Verify — no database access required ./target/release/vledger verify-audit-package --file audit-commitment.json ./target/release/vledger verify-audit-package --file entry-500000-proof.json
Testing

Running the Test Suite

VectorLedger has 498 automated tests across 11 packages, all passing on every release.

# Run the full test suite (498 tests across 11 packages) cargo test --package vledger-ledger --package vledger-sql --package vledger-server \ --package vledger-audit --package vledger-replication --package vledger-compliance \ --package vledger-foureyes --package vledger-hsm --package vledger-license \ --package vledger-crypto --package vledger-wal # Run only the regression tests (reversal/correction workflow guarantees) cargo test --package vledger-ledger regression # Run only the SQL layer tests cargo test --package vledger-sql sql_tests # Run only the auth/user management tests cargo test --package vledger-server auth_tests # Run the built-in self-tests (end-to-end engine verification) vledger self-test vledger self-test-phase3

Test coverage by area

PackageTestsCoverage areas
vledger-ledger 100+ Financial invariants INV-1–INV-14, balance cache correctness, idempotency, monotonic sequences, hash chain validity, reversal nets to zero, overflow boundaries, currency mismatch rejection, exposure limits, four-eyes, legal holds, global ledger equation, WAL replay reconstruction, property-based tests, stress tests (5,000 concurrent clients), crash/fault injection, EntryDb account persistence, settlement lifecycle, 8 deterministic crash/recovery tests (v1.0.31)
vledger-sql 95 Adversarial inputs (malformed SQL, oversized queries, injection-style input, Unicode, binary garbage), SQL pipeline (CREATE ACCOUNT, INSERT, SELECT with all filter variants, BALANCE, VERIFY_CHAIN, MERKLE_ROOT, tamper detection), rejection of UPDATE/DELETE/DROP, aggregates, JOINs, idempotency via SQL
vledger-server 33+ UserStore bootstrap, create/list/delete users, set-role (changes role, unknown user fails, persists after reopen), set-enabled, set-password, authenticate (correct/wrong password, unknown/disabled user, correct role), validate_token, role capability matrix for all four roles, role string parsing
vledger-audit 17 Open, append, sequence incrementing, hash chain linkage, first event prev_hash = ZERO_HASH, self-verify, verify_chain (empty/single/20 events/all event kinds), chain tip tracking, persistence across reopen, hash uniqueness across events
vledger-replication 21 HMAC challenge-response, secret management, divergence detection, protocol encoding, config loading
vledger-compliance 45 SOC 2 / PCI-DSS report generation, all control evaluations, JSON/Markdown serialisation
vledger-foureyes 30 Submit, approve, reject, list_pending, get, persistence across reopen, idempotency guard, audit event emission
vledger-hsm 19 HsmTransport construction, key ID helpers, KeyPolicy, RemotePyHsmConfig, HsmError display, is_available without socket
vledger-license 21 Free tier, invalid JSON fallback, tampered signature, feature gating, LicenseTier/Feature parse round-trips, canonical payload
vledger-crypto 34 MasterKey determinism, all named derivation contexts pairwise distinct, row key isolation, DerivedKey conversions, edge cases
vledger-wal 29 WAL encryption migration path — plaintext, encrypted, mixed segments; derive_segment_key, encrypt/decrypt round-trip, segment-index AAD
Testing

Security Hardening Methods (v1.0.31–v1.0.34)

In addition to the automated test suite and fuzz targets, VectorLedger uses three industry-standard verification methods to harden the financial and cryptographic code paths.

Static Analysis — cargo clippy with custom deny rules

scripts/static-analysis.sh runs two-pass clippy across the workspace:

  • Pass 1 (financial and crypto packages): denies unwrap_used, expect_used, panic, cast_possible_truncation, cast_sign_loss, indexing_slicing, ptr_arg, todo, unimplemented. Every existing violation was either fixed or annotated with a #[allow] plus a // SAFETY: comment.
  • Pass 2 (all packages): denies correctness, suspicious, await_holding_lock, let_underscore_lock across all code including tests.
  • Amount arithmetic operators (Neg, Add, Sub) now use checked_neg / checked_add / checked_sub — a silent overflow in financial math is now a compile-time error.
./scripts/static-analysis.sh

Mutation Testing — cargo-mutants

scripts/mutation-test.sh runs cargo-mutants v27 scoped to 7 financial/crypto packages. Mutation testing injects plausible-but-wrong code changes and verifies the test suite catches each one.

Initial results on vledger-crypto: 57 mutants caught, 20 survived (74% mutation score). Surviving mutants are documented test gaps — the most significant are compute_chain_hash returning a default hash, and DerivedKey::into_signing_seed returning zeros.

./scripts/mutation-test.sh --package vledger-crypto ./scripts/mutation-test.sh # all 7 configured packages

Formal Verification — Kani proof harnesses

crates/vledger-kani/ contains 21 Kani proof harnesses that exhaustively verify properties for all possible inputs within bounded types — not just a sample.

ModuleHarnessesProperties proved
wal5MAX_RECORD_PAYLOAD cap rejects u32::MAX, usize::MAX, and all values ≥ 64 MiB; accepts all values < 64 MiB
amount7Amount::new(0) always returns None; checked arithmetic never panics for any i64; checked_add result equals x+y when no overflow
hash_chain5ZERO_HASH is exactly 32 zero bytes; Hash type is 32 bytes; merkle_root(&[]) returns ZERO_HASH
hmac4mac_eq is reflexive, symmetric, returns false when inputs differ, and implies byte-for-byte equality
cargo install kani-verifier && cargo kani setup cargo kani --package vledger-kani # all 21 harnesses cargo kani --package vledger-kani --harness wal_boundary_exact # single harness ./scripts/formal-verify.sh # via script

Fuzz Targets (12 total)

VectorLedger ships 12 fuzz targets covering the WAL, SQL planner, pgwire codec, backup/restore, auth, transaction bincode deserialization, replication, backup key sidecar, audit log, compliance report, CSV import, and multi-segment WAL recovery.

cargo install cargo-fuzz # Core targets (added in v1.0.31) cargo +nightly fuzz run fuzz_backup_restore -- -max_total_time=60 cargo +nightly fuzz run fuzz_auth -- -max_total_time=60 cargo +nightly fuzz run fuzz_transaction -- -max_total_time=60 # Extended targets (added in v1.0.33) cargo +nightly fuzz run fuzz_replication -- -max_total_time=60 cargo +nightly fuzz run fuzz_backup_keysidecar -- -max_total_time=60 cargo +nightly fuzz run fuzz_audit_log -- -max_total_time=60 cargo +nightly fuzz run fuzz_compliance_report -- -max_total_time=60 cargo +nightly fuzz run fuzz_csv_import -- -max_total_time=60 cargo +nightly fuzz run fuzz_wal_recovery_multisegment -- -max_total_time=60
Reference

WAL Sync Modes

VectorLedger ships with three WAL sync modes selectable at startup.

ModeDurabilityTypical Use
group_commitUp to one flush window of data loss on hard crashDefault recommended for most deployments
per_recordZero data loss every write fsynced immediatelyStrict regulatory environments
no_syncNoneDevelopment and CI only — compile-gated behind --features dev-no-sync; not available in release binaries
WalSyncMode::NoSync is a compile-time feature gate, not a runtime guard. The NoSync variant does not exist in the type system of a standard release build. It is only compiled in when --features dev-no-sync is explicitly passed at build time. The --wal-sync-mode=no_sync CLI flag returns a hard error in release binaries. It is structurally impossible to ship or misconfigure a production binary that skips fsyncs.
# Default group commit (2 ms flush interval) vledger start # Per-record fsync (safest, lower TPS) vledger start --wal-sync-mode per_record # Tune the flush interval vledger start --wal-sync-mode group_commit --group-commit-delay-ms 5 # no_sync is NOT available in release binaries — requires --features dev-no-sync at compile time
Reference

Connection Resource Controls

Per-listener resource controls to prevent resource exhaustion:

ControlNative (5433)PgWire (5432)
max_connections semaphore12864
Per-IP token bucket (burst=10, refill=2/s)YesYes
Auth timeout30 s30 s
Idle timeout5 min5 min
Request frame size limit4 MiB16 MiB
Graceful shutdown drainYesYes
Status & Performance

Production Status

This matrix reflects the current tested and verified state of each capability. Status labels are conservative a capability is only marked Production after it has been exercised under real or realistic load with the full mTLS / key pipeline in place.

Capability Status Notes
Core ledger (append, query, BLAKE3 chain) Production Exercised by full self-test suite and phase3 tests
WAL (group commit + per-record fsync) Production Both sync modes stable; no_sync is dev/CI only
Cryptographic integrity (AES-256-GCM, Merkle proofs) Production All proofs verified by vledger verify and self-test
Local PyHSM (Model 1 Unix socket) Production Default configuration; recommended for single-server deployments
Remote PyHSM (Model 2 TLS 1.3 + mTLS) Production Recommended for production. PyHSM runs on a dedicated server in the same-region private subnet. See Model 2 setup.
PostgreSQL wire protocol (--pgwire) Production Compatible with psql, standard PG drivers, and ORMs
MCP server + AgenticAI (15 tools) Production All 15 tools execute through the same RBAC pipeline as direct SQL. Requires AgenticAI license feature (Starter+). See MCP Server.
AWS KMS Supported GenerateDataKey + Decrypt with HMAC-protected local cache
HashiCorp Vault KV v2 Supported Token-based auth; TTL expiry warning at startup
WAL replication (hot standby) Beta Failover is manual; no automatic primary election. See Replication limitations.
Compliance evidence (SOC 2 / PCI-DSS) Technical evidence Machine-generated evidence supporting control assessments not a substitute for full audit. See scope note.
Status definitions: Production tested, stable, and safe to operate under production load. Beta functional but with known limitations or incomplete test coverage; treat with care. Supported the integration is implemented and working; operational maturity depends on your external service. Technical evidence output is real and machine-generated, but audit scope extends beyond the database engine.
Status & Performance

Performance Benchmarks

25,000,000 financial records imported and verified. VectorLedger has successfully processed and cryptographically verified 25 million financial records end-to-end, validating the hash chain, double-entry integrity, and audit log across the full dataset.

Memory allocator

VectorLedger uses jemalloc as its global allocator on Linux and macOS (via tikv-jemallocator). jemalloc aggressively returns freed memory to the OS after large working-set operations — in particular WAL recovery, where processing 25 M+ records with the default ptmalloc allocator causes ~7 GB of RSS to accumulate and never be returned. jemalloc eliminates this entirely. On Windows the default MSVC allocator is used unchanged.

No configuration is required. The allocator is compiled in automatically on non-MSVC targets.

Development baseline: benchmarked on Apple Silicon (MacBook, macOS) running in group_commit WAL mode with a mixed read/write workload (10 concurrent clients, 1,000 transactions each, 70% INSERT / 30% SELECT):

MetricValue
Throughput430 TPS
Total transactions10,000
Errors0 / 10,000
Min latency311 µs
p50 latency23 ms
p95 latency36 ms
p99 latency42 ms

Benchmark environment matrix

The table below will be populated as benchmarks are run on server-class hardware. Contributions and independent reproduction are welcome. All measurements use --wal-sync-mode group_commit (default) with a 70% INSERT / 30% SELECT mixed workload unless noted otherwise. per_record mode will show lower TPS and higher p99.

EnvironmentStorageConcurrencyTPSp50p95p99
Apple M-series (dev baseline)NVMe1043023 ms36 ms42 ms
AWS Graviton3 (c7g)EBS gp310————
AWS Graviton3 (c7g)EBS gp3100————
AWS x86 (c6i)EBS gp310————
AWS x86 (c6i)EBS gp3100————
AWS x86 (c6i)EBS gp31,000————
Bare metalNVMe100————
Bare metalNVMe1,000————
Methodology note. The 430 TPS figure is a conservative baseline measured on a single MacBook with 10 concurrent clients. It is not intended for production capacity planning. Server-class NVMe storage, higher concurrency, and network-attached clients will produce materially different numbers in both directions depending on workload shape. The primary bottleneck in the write path is fsync latency, which varies significantly between storage devices and operating systems. Production performance has not yet been independently characterized on server-class hardware — that work is in progress.

Want to run your own benchmarks? The exact test script and methodology will be linked here once published so you can reproduce the numbers on your own hardware.
Reference

Financial Invariants Enforced at the Engine Level

VectorLedger enforces 16 financial invariants in code — not by policy or documentation. Every invariant is verified by the automated test suite on every release.

Core double-entry rules

InvariantError returned
Debits must equal credits on every entryUnbalancedEntry — checked before any WAL write
Every entry must have at least 2 lines (one debit, one credit)TooFewLines
Every amount must be non-zeroZeroAmount — enforced at the Amount type level

Account validity

InvariantError returned
Every account referenced in an entry must existAccountNotFound
Closed accounts reject new entriesAccountClosed
Currency on each line must match the account's registered currencyCurrencyMismatch

Balance protection

InvariantError returned
Asset and Expense accounts enforce non-negative balance (configurable per account)InsufficientFunds
Exposure limits: aggregate debit against a single account in one entry cannot exceed the configured limitExposureLimitExceeded

Legal and compliance controls

InvariantError returned
Accounts under legal hold block all new entries, reversals, and settlement transitionsAccountUnderLegalHold
Entries against accounts requiring four-eyes approval must carry a second approverFourEyesRequired

Reversal rules

InvariantError returned
Only Posted entries can be reversedCannotReverse
An entry can only be reversed onceAlreadyReversed

Cryptographic and structural integrity

InvariantEnforcement
Sequence numbers are strictly monotonic with no gapsEnforced at WAL write time
BLAKE3 hash chain maintained on every entryVERIFY_CHAIN() detects any tampering
WAL commits are Ed25519-signedSignature verification on replay detects substitution

Asset precision

InvariantError returned
Amounts for known currencies cannot exceed the maximum minor unit value for that currency's precision (USD precision=2, BTC precision=8, ETH precision=18)PrecisionViolation

Idempotency & global equation

InvariantError / verification
Duplicate entries with the same idempotency key are detected and skippedIdempotencyConflict
Σ(Assets + Expenses) == Σ(Liabilities + Equity + Income)Verified by check_financial_invariants()
Reference

Changelog

v1.5.1 — Workspace-wide version bump

  • Workspace-wide version bump — all crates at 1.5.1

v1.5.0 — AgenticAI feature gate + monthly Agent Query limits

  • Bug fix: require_feature() was only checking the explicit features list, not the tier's default features — licenses on paid tiers were incorrectly blocked from tier-default features. Fixed by checking both the explicit list and tier default features. No security impact — this was an over-blocking bug, not an under-blocking one.
  • MCP server and vledger sql --ask gated behind Feature::AgenticAi (Starter, Growth, Enterprise)
  • Monthly Agent Query limits enforced: Starter 10, Growth 100, Enterprise unlimited

v1.4.5 — propose_correction and execute_correction MCP tools

  • Added propose_correction MCP tool — shows reversal+correction plan, writes nothing
  • Added execute_correction MCP tool — executes reversal + correction after mandatory human confirmation
  • Both tools post through the normal post_entry pipeline subject to the same RBAC and all 16 financial invariants; neither tool can modify or delete existing entries

v1.4.4 — resolve_account identity enforcement MCP tool

  • Added resolve_account MCP tool — mandatory identity gate resolving name/code/UUID to an authoritative account ID before any write
  • NOT_FOUND or MULTIPLE_FOUND results halt the operation; all lookups are subject to the caller's RBAC session

v1.4.3 — JSON-RPC notifications return HTTP 204 (Kiro V3 fix)

  • JSON-RPC notifications (no id field) now return HTTP 204 — fixes Kiro V3 stuck on loading

v1.4.2 — SSE endpoint event sends full absolute URL (Kiro CLI V3 fix)

  • SSE endpoint event now sends full absolute URL — fixes Kiro CLI V3

v1.4.1 — vledger mcp auto-detects running server

  • vledger mcp now auto-detects a running vledger start on port 5433 and switches to network mode automatically

v1.4.0 — Financial semantic layer: 5 new MCP reasoning tools + AGENT_SYSTEM_PROMPT

  • 5 new MCP reasoning tools: explain_balance, reconcile_account, find_policy_violations, summarize_period, audit_report
  • AGENT_SYSTEM_PROMPT constant exported from vledger-mcp and returned in MCP initialize response's instructions field
  • Richer --ask schema context with full financial domain knowledge

v1.0.39 — Merkle root display truncation fixed

  • Bug fix: Merkle root display was truncated to 32 hex characters (16 bytes) at 3 display sites due to &root_hex[..root_hex.len().min(32)]; now shows full 64-character BLAKE3 hash
  • Display-only bug — no data integrity issue; all three display sites now show the full hash

v1.0.38 — Column projection fix

  • Bug fix: Column projection (SELECT specific, columns FROM ledger) now works correctly for all 3 tables (ledger, ledger_lines, accounts)

v1.0.37 — MERKLE_ROOT() single-argument form

  • to_seq is now optional — MERKLE_ROOT(n) is equivalent to MERKLE_ROOT(n, n) and returns the Merkle root for that single entry
  • Previously passing a single argument returned an error requiring two arguments; both forms are now fully equivalent

v1.0.36 — MERKLE_ROOT(from_seq, to_seq) SQL function

  • Added MERKLE_ROOT(from_seq, to_seq) as a first-class SQL function available at all times — no --with-proofs flag required
  • Returns a single row with from_seq, to_seq, entry_count, and merkle_root (64-char BLAKE3 hex); empty range returns 0000…0000
  • Privilege: requires admin, operator, or auditor role (can_verify); the readonly role cannot call MERKLE_ROOT
  • Uses the same leaf inputs (content_hash per entry) as --with-proofs and vledger audit-package — roots from all three methods are directly comparable
  • Files changed: vledger-sql planner/executor/optimizer, vledger-ledger store, vledger-server auth, vledger-pgwire server

v1.0.35 — pgwire --with-proofs flag was silently ignored (fixed)

  • Fixed: --with-proofs --pgwire was silently ignored — Merkle root was computed but never sent to PostgreSQL wire-protocol clients
  • Root cause: _attach_proofs parameter (underscore prefix) was never consumed; executor always called Executor::new() regardless of the flag
  • Three fixes in crates/vledger-pgwire/src/server.rs: parameter renamed and consumed; read plans now route through ReadExecutor on a read lock; Merkle root emitted as a pgwire NoticeResponse after data rows when a proof is present
  • Secondary fix: read queries were unnecessarily acquiring the write lock, blocking concurrent reads
  • This was a correctness bug — no data was exposed, no integrity guarantee was weakened; the native JSON protocol (port 5433) was unaffected

v1.0.34 — Security & Reliability Hardening: static analysis, mutation testing, formal verification

  • Static Analysis (scripts/static-analysis.sh): two-pass cargo clippy — Pass 1 denies unwrap_used, expect_used, panic, cast_possible_truncation, cast_sign_loss, indexing_slicing, and more on financial/crypto packages; Pass 2 denies correctness, suspicious, await_holding_lock across all packages; Amount arithmetic now uses checked_neg/checked_add/checked_sub
  • Mutation Testing (scripts/mutation-test.sh): cargo-mutants v27 scoped to 7 financial/crypto packages; vledger-crypto: 57 mutants caught, 20 survived (74% score); surviving mutants documented as test gaps
  • Formal Verification (crates/vledger-kani/): 21 Kani proof harnesses exhaustively verifying properties across wal (5), amount (7), hash_chain (5), and hmac (4) modules for all possible inputs within bounded types
  • No on-disk format changes; existing databases and backups are fully compatible

v1.0.33 — Security & Reliability Hardening: 498 tests, 6 new fuzz targets, 4 new proptest properties

  • 498 tests passing across 11 packages — 245 new tests added across 9 packages (see test suite for full breakdown)
  • 4 new proptest properties: reversal+correction nets to correction amount, hash chain valid for random count and amounts, settlement events do not mutate original entry, balance equals sum of lines
  • 6 new fuzz targets (12 total): fuzz_replication, fuzz_backup_keysidecar, fuzz_audit_log, fuzz_compliance_report, fuzz_csv_import, fuzz_wal_recovery_multisegment
  • No on-disk format changes; existing databases and backups are fully compatible

v1.0.32 — Security & Reliability Hardening: 3 fuzz-found bugs fixed

  • WAL reader unbounded allocation (OOM) fixed: payload_len = 0xFFFFFFFF triggered a 4 GiB allocation and killed the process; fixed by capping both payload_len and ct_len at MAX_RECORD_PAYLOAD = 64 MiB before any allocation; found by fuzz_wal_recovery
  • SQL planner index-out-of-bounds panic fixed: INSERT with fewer values than columns caused a panic; fixed by replacing vals[idx] with vals.get(idx) returning SqlError::MissingField; found by fuzz_sql_parser
  • Fuzz harness bincode allocation cap added: raw arbitrary bytes fed to bincode::serde::decode_from_slice without a limit caused ~1.8 exabyte allocation attempt in the harness (production code path is safe)
  • All 6 fuzz targets ran clean after fixes; no on-disk format changes

v1.0.31 — Security & Reliability Hardening: WAL signing, NoSync compile gate, crash recovery tests, 3 new fuzz targets

  • SignedCommit::verify() removed: self-consistency method deleted from vledger-crypto; it proved self-consistency (key embedded in commit) but not authenticity; all callers already used the safe verify_against(trusted_key) form; the unsafe method is now a compile error
  • WalSyncMode::NoSync is now a compile-time feature gate: the variant does not exist in the type system of a standard release build; requires --features dev-no-sync at compile time; the CLI flag --wal-sync-mode=no_sync returns a hard error in release builds
  • 8 new deterministic crash/recovery tests in crates/vledger-ledger/src/deterministic_recovery_tests.rs covering: torn record discard, segment-boundary crash, checkpoint deletion/corruption/future-sequence, concurrent open rejection, and multi-entry batch spanning segment roll
  • 3 new fuzz targets: fuzz_backup_restore, fuzz_auth, fuzz_transaction
  • No on-disk format changes; existing databases and backups are fully compatible

v1.0.30 — FTS5 metadata index

  • Added SQLite FTS5 full-text index over all entry metadata (entries_fts virtual table, unicode61 tokenizer)
  • WHERE metadata LIKE '%value%' and WHERE metadata ILIKE '%value%' now use the FTS index — queries that previously took minutes on 17M+ entry ledgers now return in milliseconds
  • FTS index is populated at insert time for all new entries
  • On first startup after upgrade, existing databases are automatically backfilled (progress logged to nohup.out); subsequent startups skip the step
  • Index works with any metadata field name — future CSV imports with different column layouts are automatically searchable

v1.0.29 — Metadata scan OOM fix

  • Fixed server crash (OOM) introduced in v1.0.28: replaced entries_scan(entry_count()) with stream_entries(), which iterates SQLite rows one at a time in constant RAM

v1.0.28 — Metadata scan cap fix

  • Fixed WHERE metadata LIKE returning 0 rows on ledgers larger than 10,000 entries: scan was incorrectly capped at DEFAULT_SCAN_LIMIT regardless of total ledger size

v1.0.27 — Metadata WHERE filtering

  • Added WHERE metadata = 'value' and WHERE metadata LIKE '%value%' / ILIKE support on both ledger and ledger_lines
  • Previously metadata was readable in SELECT output but not filterable in WHERE clauses
Reference

Built With

ComponentLibrary
Async runtimetokio
Symmetric encryptionaes-gcm (AES-256-GCM)
Hashingblake3
Signinged25519-dalek
Key derivationhkdf
Password hashingargon2
TLSrustls
SQL parsingsqlparser
Query indexrusqlite (SQLite)
Secret managementreqwest (Vault / AWS KMS)