Affinidi DID Hosting Daemon
The DID Hosting Daemon is a unified binary that embeds all four services — server, witness, watcher, and control plane — behind a single TCP listener. This is the easiest way to get started with a complete DID Hosting deployment.
Each service is mounted at a path prefix:
| Service | Path prefix | Description |
|---|---|---|
| Server | / |
Public DID hosting and resolution |
| Witness | /witness |
Witness proof signing |
| Watcher | /watcher |
Read-only DID mirror |
| Control | / |
DID management as Trust Tasks (TSP, DIDComm, POST /api/trust-tasks), management UI |
IMPORTANT: did-hosting-service crates are provided "as is" without any warranties or guarantees, and by using this framework, users agree to assume all risks associated with its deployment and use including implementing security, and privacy measures in their applications. Affinidi assumes no liability for any issues arising from the use or modification of the project.
Getting Started
Prerequisites
- Rust 1.94.0+ (2024 Edition)
- Node.js 20+ (only if building with the management UI)
1. Build
# Without UI
# With embedded management UI
&& && &&
The binary is produced at target/release/did-hosting-daemon.
2. Configure
The fastest way to produce a working config.toml is to run
did-hosting-daemon setup, which prompts for the values below and emits a
file you can hand-edit later. A worked example follows for reference:
= "did:webvh:example.com"
= "did:webvh:mediator.example.com"
= "https://example.com"
# Identity mode. Default: "vta". Set to "self-managed" to skip the
# VTA round-trip and have the daemon self-host its own did:webvh
# identifier. See docs/self-managed-mode-spec.md for the trade-offs.
[]
= "vta"
[]
= "0.0.0.0"
= 8534
[]
= "info"
[]
= 900
= 86400
# Secret backend. The daemon's default features include `keyring`; other
# backends require recompiling with `--features aws-secrets|gcp-secrets|
# azure-secrets|vault-secrets|k8s-secrets` and setting the matching
# `[secrets]` fields (the configured backend is selected by priority at
# runtime). HashiCorp Vault authenticates via the pod's Kubernetes
# ServiceAccount by default.
[]
= "did-hosting-daemon"
# vault_addr = "https://vault.example.com:8200" # --features vault-secrets
# vault_secret_path = "webvh/daemon-secrets"
# vault_k8s_role = "did-hosting-daemon"
# k8s_secret_name = "did-hosting-daemon-secrets" # --features k8s-secrets
# Main store (server, watcher, control share this)
[]
= "data/daemon/store"
# Witness store (separate to avoid keyspace collisions)
[]
= "data/daemon/witness"
# Which services to enable (all default to true except watcher)
[]
= true
= true
= false
= true
For self-managed mode (no VTA), set [identity] mode = "self-managed"
and omit the [vta] section entirely. The daemon will mint its own
Ed25519 + X25519 keys at first run, host its own did:webvh
identifier on public_url, and seed an empty ACL — admin enrolment is
done through did-hosting-daemon invite --did <DID> --role admin and the
passkey enrolment flow. See docs/self-managed-mode-spec.md
for the full walkthrough.
Non-interactive setup (CI / scripted)
did-hosting-daemon setup --from <recipe.toml> drives the wizard with no
TTY. Recipe is declarative TOML; no secrets inside. Example:
# VTA-managed (after `--setup-key-out` enrolment elsewhere):
# Self-managed (no VTA, no phase-1 needed):
# → recipe has [deployment].vta_mode = "self-managed"
# Air-gapped (no VTA network access from CI):
# (operator ferries to VTA admin, gets sealed bundle back)
Other flags: --force-reprovision rotates an existing install (backs
up config.toml first), did-hosting-daemon uninstall tears down (clears
managed secrets + removes config). See
docs/bootstrap_startup.md
for the recipe schema and exit codes.
3. Start the daemon
The daemon starts all enabled services on a single port and logs which
services are active. The control plane and did-hosting-server (public DID
resolver) both merge at root; webvh-witness and webvh-watcher nest
under their own prefixes:
server (/)
witness (/witness)
control (/)
daemon listening on 0.0.0.0:8534
Configuration
The daemon is configured via a TOML file. By default it looks
for config.toml in the current directory. You can specify a
different path with the --config flag or the
DAEMON_CONFIG_PATH environment variable.
Shared vs Per-Service Settings
Settings like server_did, mediator_did, public_url,
[auth], [secrets], and [log] are shared across all
services. Each service gets its own store directory to avoid
keyspace name collisions (both server and witness use sessions
and acl keyspaces).
Service-Specific Settings
| Section | Service | Description |
|---|---|---|
[limits] |
Server | Upload body limit, per-account quotas |
[vta] |
Witness | VTA remote key management |
[watcher_sync] |
Watcher | Push tokens and source servers |
[registry] |
Control | Service instance registry |
A default daemon has an empty registry and does not use the reverse proxy, so no extra configuration is needed. If you populate
[[registry.instances]](or acceptMSG_SERVER_REGISTER) and proxy to remote instances, you must setregistry.url_allowlist— the proxy is fail-closed without it. See "Reverse proxy & the registry allowlist" in the control plane README.
Enable/Disable Services
The [enable] section controls which services start:
[]
= true # DID hosting (default: true)
= true # Witness proofs (default: true)
= false # DID mirror (default: false)
= true # Management UI (default: true)
Environment Variable Overrides
Environment variables use the DAEMON_ prefix:
| Variable | Description |
|---|---|
DAEMON_CONFIG_PATH |
Path to config file |
DAEMON_SERVER_DID |
Server DID identifier |
DAEMON_MEDIATOR_DID |
Mediator DID identifier |
DAEMON_PUBLIC_URL |
Public URL |
DAEMON_SERVER_HOST |
Bind host |
DAEMON_SERVER_PORT |
Bind port |
DAEMON_LOG_LEVEL |
Log level |
CLI Commands
did-hosting-daemon # Run daemon (default)
did-hosting-daemon setup # Interactive config wizard
did-hosting-daemon health # Run health check diagnostics
did-hosting-daemon add-acl # Add ACL entry
did-hosting-daemon list-acl # List ACL entries
did-hosting-daemon remove-acl # Remove ACL entry
did-hosting-daemon invite # Create passkey enrollment invite
did-hosting-daemon list-dids # List all DIDs in the store
did-hosting-daemon remove-did # Remove a DID from the store
did-hosting-daemon load-did # Load a DID from existing files
did-hosting-daemon bootstrap-did # Bootstrap a DID (defaults to .well-known)
did-hosting-daemon recreate-did # Recreate a DID at a given path
did-hosting-daemon recover-did # Recover a soft-deleted DID
did-hosting-daemon import-secrets # Import secrets from VTA bundle or keys
did-hosting-daemon backup # Export data to backup file
did-hosting-daemon restore # Restore data from backup file
API Path Mapping
When all services are enabled, the daemon exposes endpoints
at the following paths. did-hosting-server (the public-DID-resolver edge
node) and did-hosting-control (the Trust Task listener + UI) both merge at
root — the daemon is a unified front door rather than a multiplexer.
Witness and watcher are nested under their own prefixes because their
APIs are operator-facing and benefit from a clean URL boundary.
Server + Control plane (root, merged)
| Path | Description | Source |
|---|---|---|
/api/health |
Combined health endpoint | server / control |
/api/trust-tasks |
Every management operation, as a signed Trust Task (the HTTPS binding) | control plane |
/api/sign-in/readiness |
Whether the service DID lists the services wallet sign-in needs (read by the login page) | control plane |
/api/auth/challenge, /api/auth/, /api/auth/refresh |
Browser SIOPv2 sign-in and console session refresh | control plane |
/{mnemonic}/did.jsonl |
Public DID resolution | server |
/.well-known/did.jsonl |
Service-DID resolution | server |
/, /assets/* |
Management UI (when ui feature enabled) |
control plane |
Witness (nested at /witness)
| Path | Description |
|---|---|
/witness/api/health |
Witness health |
/witness/api/trust-tasks |
The witness's Trust Tasks (webvh/witness/*, acl/*), HTTPS binding |
Watcher (nested at /watcher)
| Path | Description |
|---|---|
/watcher/api/health |
Watcher health |
/watcher/{mnemonic}/did.jsonl |
Mirrored public DID resolution |
Daemon
| Path | Description |
|---|---|
/health |
Daemon-level health |
Graceful Shutdown
The daemon handles SIGINT (Ctrl+C) and SIGTERM for graceful
shutdown. On shutdown, all service stores are persisted before
the process exits.
Feature Flags
Features propagate to the underlying service crates:
| Feature | Description |
|---|---|
ui |
Embed the management UI in did-hosting-control |
keyring |
OS keyring secrets backend |
store-fjall |
Fjall embedded storage backend |
did-methods |
Enable did:webvh + did:web resolution (route + dispatcher) |
Default features: keyring, store-fjall, ui, did-methods.
Passkey/WebAuthn auth is always compiled in (via the embedded control
plane) and is not a build feature. It activates at runtime when
public_url is set; otherwise login uses DID challenge-response.
Support & feedback
If you face any issues or have suggestions, please don't hesitate to contact us using this link.
Reporting technical issues
If you have a technical issue with the Affinidi DID Hosting Service codebase, you can also create an issue directly in GitHub.
-
Ensure the bug was not already reported by searching on GitHub under Issues.
-
If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behaviour that is not occurring.
Contributing
Want to contribute? Head over to our CONTRIBUTING guidelines.