Self-hosted (open source)
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.
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:8100for 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-ioConfirm the installation
vta --help
pnm helpYou 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_pathanddata_dirare where the generatedconfig.tomland the on-disk key store are written. Treat both as sensitive.config.tomlholds 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 ofconfig.toml.admin_didandadmin_labelseed your Administrator DID as super-admin and seal the VTA. While it is sealed, offline commands such asvta acl create,vta keys secrets,vta import-did, andvta export-adminexit with an error until you runvta unseal. Read-only commands such asvta acl listkeep 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.keyringuses 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): adid: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:webvhis 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 = falseservices.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.
The master seed generated in the next step is the root of every key this VTA will ever derive. Never commit setup.toml, config.toml, or the data_dir to version control. vta setup --from stores the generated mnemonic without displaying it, so back the VTA up with pnm backup export once you are connected.
Step 4. Run setup
vta setup --from setup.tomlThis 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.vtabakCopy 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.tomlLeave 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 healthOn 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
| Symptom | Likely cause | Fix |
|---|---|---|
vta exits with vta_did is not configured | [vta_did] was omitted or set to kind = "skip" in setup.toml | Add 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 store | You ran vta setup more than once with the same paths | Set 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 Service | The VTA process is not running, or public_url in setup.toml does not match where vta is actually listening | Confirm 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 port | Stop 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 service | The mediator does not route TSP, and TSP is on by default | Add [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 authenticate | No session stored in your OS keyring, or the DID was removed from the VTA’s ACL | Run 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
Glad to hear it! Please tell us how we can improve more.
Sorry to hear that. Please tell us how we can improve.
Thank you for sharing your feedback so we can improve your experience.