# Self-hosted (open source): unified daemon

> Build and configure the did-hosting-daemon binary to run the WebVH Hosting control plane, edge server, and witness on a single port, using either VTA-managed or self-managed identity mode.

By the end of this guide, you will have a running did-hosting-daemon instance that resolves did:webvh identifiers publicly and accepts signed DID logs from your VTA and other authenticated clients.

The did-hosting-daemon embeds the control plane, edge server, and witness behind a single TCP listener, with the watcher available as an optional fourth service. It is the recommended starting point for new deployments and for production deployments that run on a single host. See the [Overview](/products/affinidi-elements/webvh-hosting/overview.md#how-it-works) for what the control plane and edge server do.

When to use this guide:

- You are deploying for the first time.

- You want a single binary, single port deployment.

- You are running development, staging, or a production deployment that runs on a single host.

To register a deployment with your VTA and create your first DID on it, see [Create your first hosted DID](/products/affinidi-elements/webvh-hosting/get-started/create-your-first-did.md).

## Prerequisites

- [Rust 1.95.0 or later](https://www.rust-lang.org/tools/install) installed on the build host.

- Node.js 24.3 or later and npm. The management UI is part of the default build, and the build script compiles it automatically.

- A public HTTPS URL at which the daemon will serve DIDs (for example, https://did.example.com). The URL must be reachable by DID resolvers. The daemon itself listens over plain HTTP, so terminate TLS at a reverse proxy or load balancer in front of it.

- For VTA-managed mode, one of the following:

- Online: the DID of a running [VTA](/products/affinidi-elements/vta.md), and a workstation with PNM authenticated to that VTA, so you can run the context command the wizard prints.

- Offline: access to a VTA operator who can process a sealed-bundle bootstrap request, for hosts that cannot reach the VTA.

- For self-managed mode, nothing extra. The daemon generates its own keys.

## Step 1. Clone and build the daemon

```bash
git clone https://github.com/affinidi/did-hosting-service.git
cd did-hosting-service
cargo build -p did-hosting-daemon --release --locked
```

The binary is produced at target/release/did-hosting-daemon. The default build includes the management UI, the OS keyring secrets backend, and the embedded store.

To build without the management UI, and so without Node.js:

```bash
cargo build -p did-hosting-daemon --release --locked \
  --no-default-features --features keyring,store-fjall,did-methods
```

## Step 2. Run the setup wizard

The wizard creates a configuration file and stores private keys in the secrets backend you choose.

```bash
./target/release/did-hosting-daemon setup
```

The wizard first asks How will the daemon obtain its identity?:

- Online — VTA reachable from this host: VTA-managed mode, provisioned directly from your VTA.

- Offline — start a new sealed-bundle bootstrap (phase 1) and Offline — complete a pending sealed-bundle bootstrap (phase 2): VTA-managed mode for a host that cannot reach the VTA.

- Self-managed (no VTA — daemon manages its own DID): the daemon is its own trust root.

For the online and self-managed modes, it then asks for the Configuration file path (default config.toml) and Which services should the daemon run?, where the control plane, server, and witness are selected by default and the watcher is not.

### VTA-managed mode, online

The wizard asks for the following, in this order:

| Prompt | What to enter |
| VTA DID | The DID of your VTA, for example did:webvh:vta.example.com. |
| Context ID | The VTA context for this daemon. Default: webvh. |
| Has the context been created? | The wizard prints a pnm command that creates the context and grants a temporary setup DID admin access to it. Run that command on your PNM workstation, then confirm. |
| DIDComm mediator | Use the VTA’s mediator, enter a different mediator DID, or continue with no mediator. If the VTA advertises no mediator, enter a mediator DID or leave it empty. |
| Messaging transport | Both DIDComm and TSP (recommended), DIDComm only, or TSP only. Shown only when a mediator is selected. |
| Public URL | The externally reachable HTTPS URL, for example https://did.example.com. |
| Where should the daemon DID be published? | A hosting server registered with your VTA, or Serverless — self-host did.jsonl on this daemon. Shown only when your VTA has hosting servers registered, otherwise the wizard uses serverless. |
| DID path on the server | Serverless only. Path for the daemon’s own DID. Default: the path part of the public URL, or .well-known if it has none. |
| Listen host and Listen port | Defaults: 0.0.0.0 and 8534. |
| Log level and log format | Defaults: info and text. |
| Data directory root | Where to store DID records. Default: data/daemon. |
| Secrets storage backend | Where to store private keys. See [secrets backends](#secrets-backends). |
| Admin ACL entry | Enter an existing DID (for example, your operator DID), generate a new did:key for the operator (the private key is printed once, so save it immediately), or skip and add one later with did-hosting-daemon add-acl. |

The wizard also adds your VTA’s DID to the ACL, so the VTA can provision DIDs on the daemon. The Admin DID printed at the end of setup is a separate, VTA-minted identity, and needs no input from you.

### VTA-managed mode, offline

For a host that cannot reach the VTA, run the wizard twice:

- Run setup and select Offline — start a new sealed-bundle bootstrap (phase 1). The wizard writes a bootstrap request file and a pending state file.

- Send the request file to your VTA operator.

- Receive the ASCII-armoured sealed bundle and its SHA-256 digest from the operator.

- Run setup again and select Offline — complete a pending sealed-bundle bootstrap (phase 2).

- Supply the bundle, the digest, and the pending state file from phase 1.

The same flow is available non-interactively through the setup-offline-prepare and setup-offline-complete subcommands.

### Self-managed mode

Select Self-managed (no VTA — daemon manages its own DID). The daemon generates its own Ed25519 and X25519 keys locally and self-hosts its did:webvh identifier. The wizard asks for the public URL, DID path, mediator and transport, listen address, logging, data directory, and secrets backend, but not for an admin.
Warning

Self-managed mode is available only on the unified daemon, and a self-managed deployment cannot be migrated to VTA-managed mode later.

The ACL starts empty. You enrol the first admin with a passkey invite after starting the daemon in Step 4.

## Step 3. Review the configuration

The wizard writes the configuration file at the path you entered. The abridged example below shows the main sections. The wizard also writes sections for features, hosting, the registry, limits, watchers, watcher sync, and, in VTA-managed mode, the VTA.

```toml
server_did = "did:webvh:example.com"
mediator_did = "did:webvh:mediator.example.com"
public_url = "https://did.example.com"

[identity]
mode = "vta"            # or "self-managed"

[server]
host = "0.0.0.0"
port = 8534

[log]
level = "info"

[auth]
access_token_expiry = 900
refresh_token_expiry = 86400

[secrets]
keyring_service = "webvh"

[store]
data_dir = "data/daemon/store"

[witness_store]
data_dir = "data/daemon/witness"

[enable]
server  = true
witness = true
watcher = false
control = true
```

The [enable] block turns individual services on or off:

- Witness (witness, on by default): holds witness signing identities and signs a proof for a DID log version when an admin in its own ACL requests one. No other service requests proofs automatically, including your VTA when it publishes a DID.

- Watcher (watcher, off by default): a read-only mirror served under /watcher. In the daemon it reads the daemon’s own store on the same host and port, so it adds no resolution redundancy.

If the daemon runs behind a reverse proxy, set trusted_proxy_cidrs under [server] to the proxy’s address range so the daemon honours its forwarded host headers.

### Secrets backends

The default build includes the OS keyring backend. Additional backends require rebuilding with the matching Cargo feature, for example cargo build -p did-hosting-daemon --release --locked --features aws-secrets.

| Backend | Feature flag | Settings in [secrets] |
| OS keyring | (default) | keyring_service. Suitable for single-host deployments. |
| AWS Secrets Manager | aws-secrets | aws_secret_name, and optionally aws_region. |
| GCP Secret Manager | gcp-secrets | gcp_project and gcp_secret_name. |
| Azure Key Vault | azure-secrets | azure_vault_url and azure_secret_name. |
| HashiCorp Vault | vault-secrets | vault_addr and vault_secret_path. vault_auth_method is kubernetes (default, with vault_k8s_role), token, or approle. vault_kv_mount defaults to secret. |
| Kubernetes Secrets | k8s-secrets | k8s_secret_name, and optionally k8s_namespace. |

Note

With the OS keyring or any cloud, Vault, or Kubernetes backend, private keys stay in the secrets backend, separate from the configuration file. If the daemon is built without any secure backend, the wizard offers to store secrets in plaintext in the configuration file instead. Accept that only for local testing, and rebuild with a secure backend for any shared or production host.

## Step 4. Start the daemon

```bash
./target/release/did-hosting-daemon --config config.toml
```

The daemon logs which services are active on startup:

```text
--- daemon services ---
  server (/)
  witness (/witness)
  control (/)
daemon listening on 0.0.0.0:8534
```

With the default build, the management UI is served at http://localhost:8534/.

Self-managed mode only: enrol your first admin now. Replace <admin-did> with the DID the admin will authenticate as, for example a did:key from a wallet you control:

```bash
./target/release/did-hosting-daemon invite --did  --role admin --config config.toml
```

Open the printed enrolment URL in a browser to register a passkey. Redeeming the invite adds the DID to the ACL as an admin. The invite command requires public_url to be set in the configuration.

## Confirm

### Test 1: DID resolution returns the daemon DID

If you chose Serverless publication with the default .well-known DID path, request the daemon’s root DID:

```bash
curl -s https://did.example.com/.well-known/did.jsonl
```

Expected output: the daemon’s own did:webvh log in JSONL format. If you chose a different DID path, request https://did.example.com/<did-path>/did.jsonl instead. If you published the daemon DID on a separate hosting server, request it from that server.

### Test 2: Health endpoint returns ok

```bash
curl -s https://did.example.com/api/health
```

Expected output:

```json
{"status":"ok","service":"did-hosting-control","version":""}
```

## Troubleshooting

| Symptom | Likely cause | Fix |
| Error: no secrets found — run service setup first on startup | Setup was not completed, or the secrets backend is unreachable. | Re-run did-hosting-daemon setup, or check that the secrets backend (keyring, Vault, and so on) is accessible from this host. |
| 404 on /.well-known/did.jsonl | The daemon DID was published at a different path or on a separate hosting server, or the request’s Host does not match the host in the DID. | Request the DID at the path you chose in setup. Behind a reverse proxy, confirm it forwards the original host and that trusted_proxy_cidrs includes the proxy. |
| Management UI returns 404 | The daemon was built with --no-default-features, or from a source tree without the did-hosting-ui workspace and without a pre-built UI bundle. | Rebuild from a full repository checkout with the default features. |
| Build fails with failed to invoke node --version | Node.js is missing on the build host. | Install Node.js 24.3 or later and rebuild. |

## Next steps

  [Create your first hosted DID](/products/affinidi-elements/webvh-hosting/get-started/create-your-first-did.md), for a daemon in VTA-managed mode

  [Create and manage DIDs](/products/affinidi-elements/webvh-hosting/webvh-management/did-management.md)
