Self-hosted (open source)

Install the open-source VTA appliance, generate a signing identity, and connect PNM, all on infrastructure you control.

Run the VTA appliance on infrastructure you manage, such as your laptop for local development, or a server your organisation already operates. You install the vta binary, generate its signing identity with a single non-interactive setup file, and connect Personal Network Manager (PNM) to the instance you just started.

Install binariescargo install vta-service, pnm-cliGenerate Admin DIDpnm setupConfigure & start VTAvta setup --from setup.tomlMINTS THE VTA'S OWN did:webvhConnect PNMpnm setup continueYOUR MACHINE

Use this guide when:

  • You need infrastructure-level control over where the VTA runs and how it’s configured.
  • You’re doing local development or testing, and want a disposable instance.
  • Your organisation already operates the host you’ll deploy on.

For a fully managed option, use Deploy VTA appliance instead. Affinidi provisions and runs the infrastructure for you, hosted inside a Trusted Execution Environment.

Once your VTA is running, whichever path you took, continue to Create your first context.

Prerequisites

  • Install Rust version 1.95 or later (required for compiling the VTA and PNM CLI).
  • OS keyring support (macOS Keychain, GNOME Keyring, or Windows Credential Manager). This guide uses the keyring as the backend for the VTA’s master seed.
  • A host and port the VTA will listen on. Use http://localhost:8100 for local development, or your server’s public HTTPS URL if other machines need to reach it.

Step 1. Install the VTA and PNM binaries

This path uses two binaries:

  • vta: the appliance itself. It holds the master seed and serves signing and vault requests.
  • pnm: the operator CLI. You use it to configure and connect to the appliance remotely.

cargo install pulls both directly from the registry, so you don’t need to clone the source repository unless you want to build from source or inspect the code.

cargo install vta-service@0.42.0 --locked --registry crates-io
cargo install pnm-cli@0.23.1 --locked --registry crates-io

Confirm the installation

vta --help
pnm help

You should see vta’s subcommands (including setup and bootstrap-admin) and PNM’s subcommands (including setup, health, keys, and contexts).

Step 2. Generate your Administrator DID

pnm setup mints a did:key locally and stores its private key in your OS keyring. The DID starts in a pending state because no VTA exists yet to register it with. You generate it now so Step 3 can embed it directly into the VTA’s own setup file. That lets a single vta setup call both create the VTA and grant your DID admin access.

pnm setup --name "my-vta"

Sample output:

Pending VTA 'my-vta' created.
  Admin DID: did:key:z6Mk...

Next: set `admin_did = "did:key:z6Mk..."` in the VTA setup.toml, boot the VTA,
      then run: pnm setup continue my-vta --vta-did <did:...>
{"slug":"my-vta","admin_did":"did:key:z6Mk...","state":"pending"}

The summary lines go to stderr and the final JSON line to stdout, so a script can capture the JSON on its own. Copy the admin_did value. You need it in the next step.

Step 3. Write a setup file

vta setup --from <file> provisions the VTA from a single TOML file, with no interactive prompts. Passing admin_did seeds your Administrator DID and seals the VTA in this same step, so you don’t need a separate vta bootstrap-admin or vta import-did call. Each block in the file configures one part of that process:

  • config_path and data_dir are where the generated config.toml and the on-disk key store are written. Treat both as sensitive. config.toml holds the JWT signing key, and the store holds key records, imported keys, vault entries, and agent memory. Imported keys are always encrypted under a key derived from the master seed. Everything else is written in plaintext unless you turn on hardened mode with a [hardened] block (enabled = true), which encrypts the store and keeps the JWT signing key out of config.toml.

  • admin_did and admin_label seed your Administrator DID as super-admin and seal the VTA. While it is sealed, offline commands such as vta acl create, vta keys secrets, vta import-did, and vta export-admin exit with an error until you run vta unseal. Read-only commands such as vta acl list keep working. This suits a single-developer instance. For multi-operator setups, consider seeding the admin later instead.

  • [secrets] selects where the generated master seed is stored, and is required. keyring uses your OS credential manager, the recommended backend for a workstation.

  • [vta_did] sets the VTA’s own identity. Two kinds mint a new DID:

    • create_webvh (recommended): a did:webvh, a resolvable DID with a full, publicly verifiable history.
    • create_did_key: simpler, good for local development or a throwaway test instance, but has no hosted history to inspect.

    See VTA DID for why did:webvh is recommended.

# setup.toml
config_path = "./vta-data/config.toml"
data_dir    = "./vta-data/data"
vta_name    = "my-vta"
public_url  = "http://localhost:8100"

# Paste the admin_did printed in Step 2.
admin_did   = "did:key:z6Mk..."
admin_label = "ops"

[secrets]
backend = "keyring"
service = "my-vta"

[vta_did]
kind = "create_webvh"
url  = "http://localhost:8100"

Connect to a DIDComm mediator (optional)

The setup above configures REST only. If an integration guide you’re following needs DIDComm transport, for example retrieving credentials from the vault, add a [messaging] block naming an existing mediator DID. You need a mediator already running first: see DIDComm mediator deployment options.

[messaging]
kind = "existing"
did  = "did:webvh:...mediator-did..."

# Set true only if the mediator uses explicit_allow (closed) mode.
# Leave false for an explicit_deny (open) mediator.
setup_acl = false

# Turn TSP off for a DIDComm-only mediator, such as an Affinidi-hosted one.
[services]
tsp = false

services.didcomm defaults to true, so no extra flag is needed unless you disabled it. services.tsp also defaults to true, and setup then refuses a mediator whose DID document shows it does not carry Trust Spanning Protocol (TSP). If setup cannot check, it warns and continues. If your mediator does not carry TSP, add a [services] block with tsp = false. setup_acl = true provisions a per-DID allow-all ACL entry on the mediator once the DIDComm connection is established. This is required on a closed mediator. Without it, the VTA’s messages are rejected.

Step 4. Run setup

vta setup --from setup.toml

This generates a fresh 24-word BIP-39 mnemonic, stores it in your OS keyring, writes config.toml, mints the VTA’s did:webvh identity, and seeds your Administrator DID as super-admin. After progress lines for each of those, the output ends with a summary that includes the VTA’s own DID:

Setup complete.
  Config:   ./vta-data/config.toml
  Data dir: ./vta-data/data
  Name:     my-vta
  URL:      http://localhost:8100
  VTA DID:  did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:localhost%3A8100
  Admin:    did:key:z6Mk... (sealed)

  Mnemonic was generated and stored in the configured backend.
  Capture an encrypted backup after the first admin connects:
    pnm backup export --output vta-backup.vtabak

Copy this value. You need it in Step 6.

Step 5. Start the VTA

Setup only prepared the configuration and the key store. This step starts the daemon that actually serves signing, vault, and identity requests over REST using what Step 4 created.

vta --config ./vta-data/config.toml

Leave this running. Open a new terminal for the remaining steps.

Step 6. Finish connecting PNM

Your PNM session has been pending since Step 2, waiting to learn which VTA it belongs to. This step binds it to the VTA DID that vta setup minted in Step 4, moving the session from pending to complete.

pnm setup continue my-vta --vta-did <vta-did-from-step-4>

Sample output:

Bound VTA DID for 'my-vta': did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:localhost%3A8100
Ask the VTA admin to grant admin access:
  vta import-did --did did:key:z6Mk... --role admin
{"slug":"my-vta","admin_did":"did:key:z6Mk...","state":"complete"}

The vta import-did hint is standard output, and you can ignore it here: admin_did in setup.toml already granted this DID admin access.

Confirm

Test 1: PNM authenticates and the VTA reports healthy

pnm health

On this first authenticated call, PNM auto-rotates from the temporary did:key to a fresh, permanent one and removes the temporary DID from the VTA’s access control list. Expected output:

  VTA: my-vta
  DID: did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:localhost%3A8100

── VTA ───────────────────────────────────────────
  DID           did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:localhost%3A8100
                ✓ resolves (webvh)
  Mode          REST
  URL           http://localhost:8100 (from DID)
  Service       ✓ ok

── Authentication ────────────────────────────────
  Client DID    did:key:z6Mk...
  Token         ✓ valid (expires in 899s)

── Mediator ──────────────────────────────────────
  (not configured)

If you configured a mediator in Step 3, the Mediator section shows the mediator’s DID, a resolution check, and a trust-ping round-trip, followed by a VTA DIDComm section with a trust-ping to the VTA. A VTA TSP section is added when the VTA advertises TSP.

Troubleshooting

SymptomLikely causeFix
vta exits with vta_did is not configured[vta_did] was omitted or set to kind = "skip" in setup.tomlAdd a [vta_did] block with kind = "create_webvh", set overwrite_config = true and data_dir_exists = "delete" in setup.toml, and re-run vta setup --from setup.toml.
vta setup fails with config file … already exists or data directory … already holds a storeYou ran vta setup more than once with the same pathsSet both overwrite_config = true and data_dir_exists = "delete" in setup.toml for a clean re-run, or point config_path and data_dir at new paths.
pnm health shows ✗ unreachable on ServiceThe VTA process is not running, or public_url in setup.toml does not match where vta is actually listeningConfirm vta --config ./vta-data/config.toml is still running, and that the port matches public_url.
vta exits with server error: io error: Address already in use (os error 48)Another process (or a previous vta instance you forgot to stop) is already bound to the configured portStop the process holding the port (lsof -i :8100 on macOS/Linux), or edit [server].port in config.toml and the matching port in public_url, then restart vta.
vta setup fails with services.tsp = true, but mediator … advertises no TSPTransport serviceThe mediator does not route TSP, and TSP is on by defaultAdd [services] with tsp = false, or use a TSP-capable mediator, then set overwrite_config = true and data_dir_exists = "delete" and re-run vta setup --from setup.toml.
pnm commands fail to authenticateNo session stored in your OS keyring, or the DID was removed from the VTA’s ACLRun pnm auth status. If no session exists, run pnm vta delete my-vta, then pnm setup --name "my-vta", and repeat from Step 3 with overwrite_config = true and data_dir_exists = "delete" set in setup.toml.

Next steps

  Create your first context and signing key

  Back up and restore VTA state

  Glossary: the terms used across these pages and in pnm output.