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, in Isolated mode, which runs in an AWS Nitro Enclave, a Trusted Execution Environment (TEE), or in Shared mode, which runs on a shared network for evaluation and development purposes only.
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. Clone the source repository only if you want to build from source or inspect the code.
cargo install vta-service@0.65.0 --locked --registry crates-io
cargo install pnm-cli@0.45.0 --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 one TOML file, with no prompts. Create setup.toml:
# 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"| Setting | What it does |
|---|---|
config_path, data_dir | Where setup writes config.toml and the on-disk key store. Treat both as sensitive: config.toml holds the JWT signing key. |
vta_name | A human-readable name for this VTA, shown by vta config show and pnm setup. |
public_url | The VTA’s REST address, advertised in its DID document. For a DIDComm-only VTA, leave it out and set rest = false in a [services] block. |
admin_did, admin_label | Your Administrator DID from Step 2, and a label for it. Setup seeds it as super-admin and seals the VTA. |
[secrets] | Where the master seed is stored. Required. keyring uses your OS credential manager, under the service name, which should be unique for each VTA on the host. |
[vta_did] | The VTA’s own DID. create_webvh, the recommended kind, mints a did:webvh hosted at url, with a publicly verifiable history. By default it also lists its did:scid:vh:1 alias in alsoKnownAs; to leave the alias out, add a [dids] block with scid_also_known_as = false. create_did_key mints a did:key, for local development. See VTA DID. |
Two consequences of these settings:
- Sealed VTA: while the VTA is sealed, offline commands such as
vta 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. For a multi-operator setup, consider seeding the admin later instead. - Plaintext store: the key store is written in plaintext, apart from imported keys, which are always encrypted. To encrypt the store and keep the JWT signing key out of
config.toml, add a[hardened]block withenabled = true.
Connect to a DIDComm mediator (optional)
The file above configures REST only. Add a [messaging] block to use DIDComm, which you need to retrieve vault credentials and to back up the VTA with pnm backup export. The mediator must already be running: 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| Setting | What it does |
|---|---|
kind, did | existing connects to a running mediator, identified by its DID. |
setup_acl | true for a closed (explicit_allow) mediator: the VTA adds an allow-all ACL entry for its own DID on the mediator when it connects at startup, and without it the mediator rejects the VTA’s messages. false for an open (explicit_deny) mediator. |
[services] tsp | TSP is on by default, and setup refuses a mediator whose DID document shows no TSP. If setup cannot check, it warns and continues. Set false for a DIDComm-only mediator, such as an Affinidi-hosted one. |
Keep setup.toml, config.toml, and the data_dir out of version control. vta setup --from stores the master seed’s mnemonic without displaying it, so an encrypted backup is your recovery path: add the [messaging] block before this VTA holds keys you cannot recreate.
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 resolves the VTA DID from the host in its did:webvh identifier, and connects to the VTA to check your admin access. By default it accepts only public hosts:
Local development (
public_urlonlocalhost): allow private hosts in the terminal you runpnmfrom, before you run the command below.export VTA_ALLOW_PRIVATE_ENDPOINTS=1Public HTTPS URL: no extra setting is needed.
pnm setup continue my-vta --vta-did <vta-did-from-step-4>Sample output:
Checking whether did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:localhost%3A8100 accepts did:key:z6MkjLfAT61ZbMfUTytxjp6KnDh7hy9jE5YkQJNuouJEC4cU...
Bound VTA DID for 'my-vta': did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:localhost%3A8100
did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:localhost%3A8100 accepted did:key:z6MkjLfAT61ZbMfUTytxjp6KnDh7hy9jE5YkQJNuouJEC4cU: admin access is in place. PNM rotated to a fresh long-lived did:key (did:key:z6Mk...) and dropped the temp DID from the ACL.
Ready: try `pnm health`.
{"slug":"my-vta","admin_did":"did:key:z6Mk...","state":"complete"}admin_did in setup.toml already granted the temporary DID admin access, so PNM confirms it and replaces it with a fresh, permanent did:key.
Confirm
Test 1: PNM authenticates and the VTA reports healthy
pnm healthClient DID shows the permanent did:key PNM rotated to in Step 6. Expected output:
VTA: my-vta
DID: did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:localhost%3A8100
── VTA ───────────────────────────────────────────
DID did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:localhost%3A8100
✓ resolves (webvh)
claims (unverified): did:scid:vh:1:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp?src=localhost%3A8100
Mode REST
URL http://localhost:8100 (from DID)
Service ✓ ok
── Authentication ────────────────────────────────
Client DID did:key:z6Mk...
Token ✓ valid (expires in 899s)
── Mediator ──────────────────────────────────────
(not configured)The output above is abridged. Between Authentication and Mediator, pnm health also prints a Deployment section with these rows:
- Sealed: whether the VTA is sealed.
- Storage: whether the VTA’s storage is encrypted at rest.
- TSP: whether TSP is enabled.
- TEE: the TEE type and whether it is detected, on a VTA that runs in a TEE.
- Version: the VTA version, shown to administrators only.
- Restored: whether the VTA’s state was restored from a backup, shown to administrators only.
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 fails with a BlockedHost error naming localhost | The VTA DID is hosted on localhost, and pnm resolves DIDs from public hosts only by default | Run export VTA_ALLOW_PRIVATE_ENDPOINTS=1 in the terminal you run pnm from, as in Step 6. |
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 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 panics with failed to open store: Store(Locked) | A previous vta instance using the same data_dir is still running | Stop that instance, 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. Thanks for letting us know.
Sorry to hear that. Thanks for letting us know.