Table of Contents
- Overview
- Prerequisites
- Installation
- Quick Start
- Reset / Starting Over
- CLI: vledger init
- CLI: vledger start
- CLI: vledger sql
- CLI: vledger audit-package
- CLI: vledger audit-proof
- CLI: vledger verify-audit-package
- CLI: vledger start-primary
- CLI: vledger start-replica
- CLI: vledger user
- CLI: vledger seed
- CLI: vledger migrate-to-sqlite
- CLI: vledger import
- CLI: vledger reconcile
- CLI: vledger settle
- CLI: vledger hold
- CLI: vledger retention
- CLI: vledger rules
- CLI: vledger backup-verify
- MCP Server Overview
- CLI: vledger mcp
- MCP Client Configuration
- MCP Agent Query Billing
- Natural-Language Queries
- PyHSM Key Backend
- Model 2 Setup Guide
- SQL Reference
- Metadata Search
- Multiple Point Lookups (IN)
- Compliance Reporting
- Cryptographic Audit Package
- Replication
- Replication Setup
- Multi-Node Deployment
- Client SDKs
- Licensing
- Production Checklist
- Testing & Verification
- Production Status
- Benchmarks
- Financial Invariants
- Changelog
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.
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.
Prerequisites
| Requirement | Minimum Version | Notes |
|---|---|---|
| Rust toolchain | 1.80 | Install via rustup.rs |
| macOS, Linux, or Windows | macOS and Linux are fully supported; Windows 10/11 and Windows Server 2019/2022 (x86_64 and ARM64) are supported | |
| Git | Any recent | To clone the repository |
No other runtime dependencies are required. All cryptographic libraries are statically linked via Cargo.
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
| Variable | Default | Description |
|---|---|---|
VLEDGER_VERSION | latest | Release tag to install, e.g. v1.0.18 |
VLEDGER_INSTALL_DIR | /usr/local/bin | Directory to place the vledger binary |
VLEDGER_NO_MODIFY_PATH | 0 | Set 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
| Parameter | Default | Description |
|---|---|---|
-Version | latest | Release tag, e.g. v1.0.18. Also reads $env:VLEDGER_VERSION. |
-InstallDir | %LOCALAPPDATA%\vledger\bin | Directory to install vledger.exe |
-NoPathUpdate | off | Skip adding the install dir to your user PATH |
$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
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
git clone https://github.com/pavondunbar/VectorLedger.git
cd VectorLedger
# 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
./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.
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.
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
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
vledger start is called, startup will fail with a clear error.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"
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.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/
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.
cat vledger-data/catalog/.admin_initial_credentials
vledger user set-password --username admin --data-dir ./vledger-data
rm vledger-data/catalog/.admin_initial_credentials
vledger verify --data-dir ./vledger-data
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
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"
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
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.
# 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.
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)
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)
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.
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.
--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:
| Endpoint | Description |
|---|---|
GET /metrics | Prometheus text-format scrape target |
GET /health | Liveness 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
| Metric | Type | Description |
|---|---|---|
vledger_build_info | gauge | Build version and Rust version |
vledger_uptime_seconds | gauge | Server uptime in seconds |
vledger_ledger_entries_total | counter | Total journal entries posted |
vledger_ledger_accounts_total | gauge | Total accounts in chart of accounts |
vledger_wal_segments_total | gauge | Number of WAL segment files |
vledger_wal_sync_lag_ms | gauge | Milliseconds since last WAL fsync |
vledger_connections_active | gauge | Currently open client connections |
vledger_connections_total | counter | Total connections accepted since start |
vledger_auth_successes_total | counter | Successful authentications |
vledger_auth_failures_total | counter | Failed authentication attempts |
vledger_queries_total | counter | SQL queries executed |
vledger_query_duration_ms_sum | counter | Cumulative query execution time (ms) |
vledger_chain_verification_failures_total | counter | Hash chain integrity failures |
vledger_backup_age_seconds | gauge | Seconds since the last backup was created |
vledger_replica_lag_lsn | gauge | Replication lag in WAL records |
vledger_foureyes_pending_total | gauge | Four-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 asINTEGRITY_ALERT. - 15-minute invariant monitor — checks that
Σ(Assets+Expenses) == Σ(Liabilities+Equity+Income)every 15 minutes. Violations are logged asINVARIANT_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
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
REPL meta-commands
| Command | Description |
|---|---|
\x | Toggle expanded (vertical) display. Useful for wide rows. |
\q or exit | Quit the REPL. |
\? or \help | Show available meta-commands. |
The prompt changes to vledger (expanded)> while expanded mode is active. Use \x again to toggle back.
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
vledger status
Show database version, WAL segment count, and active segment.
vledger status --data-dir <PATH>
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>]
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]
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]
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)
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:
| Tier | Date range allowed |
|---|---|
| Free | Last 30 days |
| Starter | Last 90 days |
| Growth / Enterprise | Unlimited |
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"
}
}
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
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 type | What is verified |
|---|---|
commitment | Ed25519 root signature |
entry_proof | Content hash + chain hash + Merkle inclusion proof |
full | All 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
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
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)
vledger start-primary
Start a WAL replication primary listener. Requires a Growth or Enterprise license.
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.
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.
vledger user
Manage user accounts. All subcommands read the user store directly from the data directory the server does not need to be running.
| Subcommand | Description |
|---|---|
set-password | Change a user's password (revokes all active sessions) |
set-role | Change a user's role (revokes all active sessions immediately) |
create | Create a new user account |
list | List all accounts |
set-enabled | Enable or disable an account |
delete | Delete 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
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.
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.
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
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.
| Situation | Action |
|---|---|
SQLite index (vledger.db) is empty after a large vledger import | Run migrate-to-sqlite once after import completes |
| Upgrading from a version that did not maintain the SQLite index | Run migrate-to-sqlite once after upgrading the binary |
| Normal operation | Not needed — the index is maintained automatically |
After migration completes, run vledger verify --data-dir ./vledger-data to confirm WAL and chain integrity.
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.
| Situation | What 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 startups | Rebuild 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.
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
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.
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)
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>
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>
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
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
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 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).
curl http://127.0.0.1:3000/healthResponse:
{"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
| Tool | Category | Description |
|---|---|---|
query_ledger | Query | Run any SELECT, BALANCE(), VERIFY_CHAIN(), MERKLE_ROOT() |
post_entry | Write | Record a new double-entry journal entry |
get_balance | Query | Current balance of any account |
list_accounts | Query | All accounts with balances (optional domain/currency filter, default limit 500) |
query_ledger_lines | Query | Individual debit/credit lines |
verify_chain | Integrity | Verify the BLAKE3 hash chain |
merkle_root | Integrity | BLAKE3 Merkle commitment over a sequence range |
explain_balance | Reasoning | Why is an account at its current balance? Chains metadata → debits → credits → narrative |
reconcile_account | Reasoning | Does stored balance match sum(posted lines)? Returns BALANCED or exact discrepancy |
find_policy_violations | Reasoning | Large transactions, pending-too-long, missing external refs, failed entries |
summarize_period | Reasoning | Natural-language summary: counts by status, volume, Merkle commitment, chain integrity |
audit_report | Reasoning | Full cryptographic audit evidence report for a period |
resolve_account | Identity | Resolve name/code/UUID → authoritative account ID (mandatory before writes) |
propose_correction | Correction | Show reversal+correction plan (writes nothing) |
execute_correction | Correction | Execute 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 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],
)
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.
| Tier | Agent Queries / Month |
|---|---|
| Free | Disabled |
| Starter | 10 |
| Growth | 100 |
| Enterprise | Unlimited |
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.
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 Variable | Default | Purpose |
|---|---|---|
OPENAI_API_KEY | — | Required. Your API key. |
OPENAI_MODEL | gpt-4o | Model to use for translation. |
OPENAI_BASE_URL | https://api.openai.com/v1 | Override for Ollama, Groq, etc. |
OPENAI_BASE_URL to your Ollama or Groq endpoint to avoid sending queries to OpenAI.
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
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
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
File (development only)
Generates a random key and writes it to vledger-data/keys/master_key.hex with mode 0o600.
vledger init --key-source file
Key Backend Summary
| Backend | --key-source | Key never on disk | External dependency |
|---|---|---|---|
| PyHSM local Model 1 | pyhsm | ✓ | PyHSM daemon on same host |
| PyHSM remote Model 2 | remote-pyhsm | ✓ | PyHSM daemon on private subnet + TLS certs |
| Environment variable | env | ✗ (in env) | None |
| Disk file | file | ✗ | None |
| HashiCorp Vault | vault | ✓ | Vault server + token |
| AWS KMS | aws_kms | ✓ | AWS credentials + KMS key |
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
PyHSM environment variables
| Variable | Default | Description |
|---|---|---|
PYHSM_MASTER_PASSWORD | Required. Password that unlocks the PyHSM keystore. | |
PYHSM_KEYSTORE_PATH | ./pyhsm-keystore.enc | Path where the encrypted keystore is stored. Set this to a persistent, backed-up location. |
PYHSM_AUDIT_LOG_PATH | <keystore>.audit.jsonl | Path for PyHSM's own tamper-evident audit log. |
PYHSM_SOCKET_PATH | /tmp/pyhsm.sock | Unix socket path the daemon listens on (Model 1). |
PYHSM_CALLER_SECRET | Optional shared secret for IPC caller authentication. | |
PYHSM_RATE_LIMIT | 100 | Max operations per rate window. |
PYHSM_RATE_WINDOW_MS | 60000 | Rate window in milliseconds. |
How VectorLedger uses PyHSM across restarts
| Event | What happens |
|---|---|
First vledger init --key-source pyhsm | VectorLedger 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 start | HMAC 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 startup | vledger start fails immediately with a clear error no data is touched. |
| Cache file tampered | HMAC 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. |
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
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
| Variable | Description |
|---|---|
PYHSM_ENDPOINT | HTTPS endpoint of the remote PyHSM daemon |
PYHSM_CA_CERT | Path to the CA certificate PEM |
PYHSM_CLIENT_CERT | Path to the mTLS client certificate PEM |
PYHSM_CLIENT_KEY | Path to the mTLS client private key PEM |
PYHSM_TIMEOUT_MS | Per-request timeout in milliseconds |
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.
openssl is available on whichever machine you generate certificates on.# 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
# 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
| Field | Value |
|---|---|
| Type | Custom TCP |
| Port | 8443 |
| Source | Server 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.
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
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.
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.
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)
Replay-Attack Prevention (Model 2)
Every request sent to a remote PyHSM over TLS includes two additional fields that PyHSM should validate:
| Field | Value | Purpose |
|---|---|---|
requestId | UUID v4 | PyHSM rejects duplicate IDs within its replay window (recommended: 5 min) |
timestamp | RFC 3339 UTC | PyHSM 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.
requestId uniqueness within a 5-minute window and reject stale timestamp values. This is also a requirement in the Production Deployment Checklist.
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
Hardware HSM PKCS#11 Integration Enterprise
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:
| Hardware | Bridge sidecar |
|---|---|
| AWS CloudHSM | vledger-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.
Compliance Reporting
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
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
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 pattern | Behaviour |
|---|---|
SELECT * FROM ledger WHERE sequence = N | No cap — returns exactly 1 entry |
SELECT * FROM ledger WHERE external_ref = 'X' | No cap — point lookup |
SELECT * FROM ledger LIMIT 500 | Exactly 500 rows — explicit limit honoured |
SELECT * FROM ledger | Capped 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;
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';
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');
amountis in minor units (cents for USD — 100000 = $1,000.00)debit_accountandcredit_accountaccept either accountcodeor UUID- Optional fields:
external_ref,idempotency_key - Entries are append-only —
UPDATEandDELETEare 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.
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);
--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.
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;
Joins
Supports INNER JOIN and LEFT OUTER JOIN.
SELECT * FROM ledger JOIN accounts ON ledger.domain = accounts.domain;
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.
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
ledgerandledger_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. LIKEperforms case-sensitive matching;ILIKEis case-insensitive.
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-...');
WHERE sequence < x OR sequence > y range approach.
Compatibility Queries
These queries are supported for ORM and connection pooler health checks:
SELECT 1;
SELECT version();
SELECT current_user();
SELECT current_database();
What Is Not Supported
| Operation | Why |
|---|---|
UPDATE | Append-only — entries are permanent |
DELETE | Append-only — entries are permanent |
CREATE TABLE / DROP TABLE | Schema is fixed |
pg_catalog.* system tables | Not PostgreSQL internally |
\l, \dt, \du psql meta-commands | Rely on pg_catalog |
| Multiple databases or schemas | Single-database engine |
| Stored procedures, triggers, sequences | Not implemented |
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.
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 mode0o600. - Divergence detection via periodic
DivergenceCheckpointmessages carrying a rolling BLAKE3 WAL chain hash — a mismatch means the replica must be re-seeded.
Replication Setup
# 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.
scp vledger-data/replication_secret.hex replica-host:/path/to/vledger-data/replication_secret.hex
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"
}
}
"ca_cert": null and build with the dev-insecure-replication Cargo feature. Do not do this in production.# 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.
replication.json Reference
| Field | Default | Description |
|---|---|---|
role | "primary" | "primary" or "replica" |
replication_addr | "127.0.0.1:5434" | Primary: bind address. Replica: primary's host:port |
ack_timeout_ms | 5000 | How long the primary waits for a replica ACK (ms) |
heartbeat_interval_ms | 1000 | Heartbeat frequency (ms) |
send_buffer_bytes | 67108864 | Max bytes buffered per replica connection (64 MiB) |
secret_path | null | Path to HMAC secret; null = <data_dir>/replication_secret.hex |
tls.enabled | true | Enable TLS on the replication channel |
tls.server_cert | null | Primary TLS cert PEM; null = auto-generate self-signed |
tls.server_key | null | Primary TLS key PEM; null = auto-generate |
tls.server_hostname | "vledger-primary" | SNI hostname used by replica to verify the primary's cert |
tls.ca_cert | null | Replica: CA cert PEM to verify the primary. Required in production |
tls.client_cert | null | Replica client cert PEM for mTLS (optional) |
tls.client_key | null | Replica client key PEM for mTLS (optional) |
Replication Limitations
These limitations do not affect the security properties of the WAL integrity or tamper-evidence guarantees.
Multi-Node Deployment (WAL Replication) Growth+
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.
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 verifies | How |
|---|---|
| Channel authenticity | BLAKE3-keyed HMAC challenge-response inside TLS 1.3 |
| Per-record integrity | BLAKE3 hash verified before each record is applied |
| Divergence detection | Periodic DivergenceCheckpoint messages carry a rolling BLAKE3 WAL chain hash — mismatch triggers re-seed |
Client SDKs
Native client libraries are included for three languages, all located under clients/ in the repository.
| Language | Location |
|---|---|
| Python | clients/python/ |
| TypeScript / Node.js | clients/typescript/ |
| Go | clients/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).
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
| Tier | Price | Best for |
|---|---|---|
| Free | $0 / month | Development, evaluation, internal tools |
| Starter | $499 / month | Early-stage teams that need PostgreSQL client compatibility |
| Growth | $2499 / month | Production fintechs and SaaS companies under SOC 2 or PCI-DSS |
| Enterprise | Contact Sales | Banks, 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
| Feature | Free | Starter | Growth | Enterprise |
|---|---|---|---|---|
| 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 days | 90 days | Unlimited | Unlimited |
PostgreSQL wire protocol (--pgwire) | ✗ | ✓ | ✓ | ✓ |
| MCP server + AgenticAI | ✗ | 10 queries/mo | 100 queries/mo | Unlimited |
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.
Production Deployment Checklist
Before putting VectorLedger in front of production traffic:
Core setup
- PyHSM daemon running with a persistent, backed-up keystore (
PYHSM_KEYSTORE_PATHpoints to a durable location) vledger initcompleted with--key-source pyhsm(Model 1) or--key-source remote-pyhsm(Model 2)key_source.jsonshows"backend": "py_hsm"or"backend": "remote_py_hsm"not"env"or"file"keys/MASTER_KEY_PLACEHOLDER.txtdeleted (its presence fails the PCI-DSS compliance check)- Admin credential file read, password changed, and
catalog/.admin_initial_credentialsdeleted - Data directory permissions locked (
chmod 700onvledger-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-pathand--tls-key-pathat startup - Configure replication with a secondary node (
replication.json) or document a backup-based HA strategy - Install a valid
license.jsonfor your paid tier (vledger license --data-dir ./vledger-datato confirm) - Test a full backup and restore drill:
vledger backup→vledger restore→vledger verify - Schedule regular
vledger backupruns (cron or your orchestrator) - Schedule regular
vledger verifyruns (recommended: after each backup) - Ship
audit/audit.logto 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_KEYenvironment variables set (or paths baked intokey_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 staletimestampvalues - Verified that
vledger startfails cleanly when PyHSM is unreachable never falls back silently
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.
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
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
c7g.xlarge (Graviton3, 4 vCPU, 8 GB RAM). Avoid t3/t4g burstable instances - CPU credit throttling produces misleading results.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;
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
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();
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
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.
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
| Phase | What it tests |
|---|---|
| A - Baseline | Inserts N deterministic entries with varied amounts, verifies the hash chain immediately |
| B - WAL Integrity | Corrupts a WAL byte, confirms server detects and rejects it, restores the byte |
| C - Crash Recovery | Reopens the database, confirms 100% of entries recovered with chain intact |
| D - Logical Integrity | Mutates an entry in memory without updating its hash, confirms VERIFY_CHAIN() detects it |
| E - Entry Verification | Spot-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;
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();
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
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
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
| Package | Tests | Coverage 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 |
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_lockacross all code including tests. Amountarithmetic operators (Neg,Add,Sub) now usechecked_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.
| Module | Harnesses | Properties proved |
|---|---|---|
wal | 5 | MAX_RECORD_PAYLOAD cap rejects u32::MAX, usize::MAX, and all values ≥ 64 MiB; accepts all values < 64 MiB |
amount | 7 | Amount::new(0) always returns None; checked arithmetic never panics for any i64; checked_add result equals x+y when no overflow |
hash_chain | 5 | ZERO_HASH is exactly 32 zero bytes; Hash type is 32 bytes; merkle_root(&[]) returns ZERO_HASH |
hmac | 4 | mac_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
WAL Sync Modes
VectorLedger ships with three WAL sync modes selectable at startup.
| Mode | Durability | Typical Use |
|---|---|---|
group_commit | Up to one flush window of data loss on hard crash | Default recommended for most deployments |
per_record | Zero data loss every write fsynced immediately | Strict regulatory environments |
no_sync | None | Development 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
Connection Resource Controls
Per-listener resource controls to prevent resource exhaustion:
| Control | Native (5433) | PgWire (5432) |
|---|---|---|
max_connections semaphore | 128 | 64 |
| Per-IP token bucket (burst=10, refill=2/s) | Yes | Yes |
| Auth timeout | 30 s | 30 s |
| Idle timeout | 5 min | 5 min |
| Request frame size limit | 4 MiB | 16 MiB |
| Graceful shutdown drain | Yes | Yes |
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. |
Performance Benchmarks
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):
| Metric | Value |
|---|---|
| Throughput | 430 TPS |
| Total transactions | 10,000 |
| Errors | 0 / 10,000 |
| Min latency | 311 µs |
| p50 latency | 23 ms |
| p95 latency | 36 ms |
| p99 latency | 42 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.
| Environment | Storage | Concurrency | TPS | p50 | p95 | p99 |
|---|---|---|---|---|---|---|
| Apple M-series (dev baseline) | NVMe | 10 | 430 | 23 ms | 36 ms | 42 ms |
| AWS Graviton3 (c7g) | EBS gp3 | 10 | — | — | — | — |
| AWS Graviton3 (c7g) | EBS gp3 | 100 | — | — | — | — |
| AWS x86 (c6i) | EBS gp3 | 10 | — | — | — | — |
| AWS x86 (c6i) | EBS gp3 | 100 | — | — | — | — |
| AWS x86 (c6i) | EBS gp3 | 1,000 | — | — | — | — |
| Bare metal | NVMe | 100 | — | — | — | — |
| Bare metal | NVMe | 1,000 | — | — | — | — |
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.
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
| Invariant | Error returned |
|---|---|
| Debits must equal credits on every entry | UnbalancedEntry — checked before any WAL write |
| Every entry must have at least 2 lines (one debit, one credit) | TooFewLines |
| Every amount must be non-zero | ZeroAmount — enforced at the Amount type level |
Account validity
| Invariant | Error returned |
|---|---|
| Every account referenced in an entry must exist | AccountNotFound |
| Closed accounts reject new entries | AccountClosed |
| Currency on each line must match the account's registered currency | CurrencyMismatch |
Balance protection
| Invariant | Error 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 limit | ExposureLimitExceeded |
Legal and compliance controls
| Invariant | Error returned |
|---|---|
| Accounts under legal hold block all new entries, reversals, and settlement transitions | AccountUnderLegalHold |
| Entries against accounts requiring four-eyes approval must carry a second approver | FourEyesRequired |
Reversal rules
| Invariant | Error returned |
|---|---|
Only Posted entries can be reversed | CannotReverse |
| An entry can only be reversed once | AlreadyReversed |
Cryptographic and structural integrity
| Invariant | Enforcement |
|---|---|
| Sequence numbers are strictly monotonic with no gaps | Enforced at WAL write time |
| BLAKE3 hash chain maintained on every entry | VERIFY_CHAIN() detects any tampering |
| WAL commits are Ed25519-signed | Signature verification on replay detects substitution |
Asset precision
| Invariant | Error 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
| Invariant | Error / verification |
|---|---|
| Duplicate entries with the same idempotency key are detected and skipped | IdempotencyConflict |
Σ(Assets + Expenses) == Σ(Liabilities + Equity + Income) | Verified by check_financial_invariants() |
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 explicitfeatureslist, 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 --askgated behindFeature::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_correctionMCP tool — shows reversal+correction plan, writes nothing - Added
execute_correctionMCP tool — executes reversal + correction after mandatory human confirmation - Both tools post through the normal
post_entrypipeline 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_accountMCP tool — mandatory identity gate resolving name/code/UUID to an authoritative account ID before any write NOT_FOUNDorMULTIPLE_FOUNDresults 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
idfield) 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
endpointevent now sends full absolute URL — fixes Kiro CLI V3
v1.4.1 — vledger mcp auto-detects running server
vledger mcpnow auto-detects a runningvledger starton 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_PROMPTconstant exported fromvledger-mcpand returned in MCPinitializeresponse'sinstructionsfield- Richer
--askschema 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_seqis now optional —MERKLE_ROOT(n)is equivalent toMERKLE_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-proofsflag required - Returns a single row with
from_seq,to_seq,entry_count, andmerkle_root(64-char BLAKE3 hex); empty range returns0000…0000 - Privilege: requires
admin,operator, orauditorrole (can_verify); thereadonlyrole cannot callMERKLE_ROOT - Uses the same leaf inputs (
content_hashper entry) as--with-proofsandvledger audit-package— roots from all three methods are directly comparable - Files changed:
vledger-sqlplanner/executor/optimizer,vledger-ledgerstore,vledger-serverauth,vledger-pgwireserver
v1.0.35 — pgwire --with-proofs flag was silently ignored (fixed)
- Fixed:
--with-proofs --pgwirewas silently ignored — Merkle root was computed but never sent to PostgreSQL wire-protocol clients - Root cause:
_attach_proofsparameter (underscore prefix) was never consumed; executor always calledExecutor::new()regardless of the flag - Three fixes in
crates/vledger-pgwire/src/server.rs: parameter renamed and consumed; read plans now route throughReadExecutoron a read lock; Merkle root emitted as a pgwireNoticeResponseafter 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-passcargo clippy— Pass 1 deniesunwrap_used,expect_used,panic,cast_possible_truncation,cast_sign_loss,indexing_slicing, and more on financial/crypto packages; Pass 2 deniescorrectness,suspicious,await_holding_lockacross all packages;Amountarithmetic now useschecked_neg/checked_add/checked_sub - Mutation Testing (
scripts/mutation-test.sh):cargo-mutantsv27 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 acrosswal(5),amount(7),hash_chain(5), andhmac(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 = 0xFFFFFFFFtriggered a 4 GiB allocation and killed the process; fixed by capping bothpayload_lenandct_lenatMAX_RECORD_PAYLOAD = 64 MiBbefore any allocation; found byfuzz_wal_recovery - SQL planner index-out-of-bounds panic fixed:
INSERTwith fewer values than columns caused a panic; fixed by replacingvals[idx]withvals.get(idx)returningSqlError::MissingField; found byfuzz_sql_parser - Fuzz harness bincode allocation cap added: raw arbitrary bytes fed to
bincode::serde::decode_from_slicewithout 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 fromvledger-crypto; it proved self-consistency (key embedded in commit) but not authenticity; all callers already used the safeverify_against(trusted_key)form; the unsafe method is now a compile errorWalSyncMode::NoSyncis now a compile-time feature gate: the variant does not exist in the type system of a standard release build; requires--features dev-no-syncat compile time; the CLI flag--wal-sync-mode=no_syncreturns a hard error in release builds- 8 new deterministic crash/recovery tests in
crates/vledger-ledger/src/deterministic_recovery_tests.rscovering: 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_ftsvirtual table,unicode61tokenizer) WHERE metadata LIKE '%value%'andWHERE 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())withstream_entries(), which iterates SQLite rows one at a time in constant RAM
v1.0.28 — Metadata scan cap fix
- Fixed
WHERE metadata LIKEreturning 0 rows on ledgers larger than 10,000 entries: scan was incorrectly capped atDEFAULT_SCAN_LIMITregardless of total ledger size
v1.0.27 — Metadata WHERE filtering
- Added
WHERE metadata = 'value'andWHERE metadata LIKE '%value%'/ILIKEsupport on bothledgerandledger_lines - Previously metadata was readable in SELECT output but not filterable in WHERE clauses