Affinidi DID Hosting Control Plane
The DID Hosting Control Plane is the authoritative source of truth for all DID management. It handles DID lifecycle operations (create, publish, delete) via DIDComm and optional REST API, and pushes updates to server edge nodes via DIDComm through a mediator. It also hosts an optional web-based management UI, maintains a service registry, acts as a reverse proxy to backend service instances, and supports DIDComm v2 and passkey (WebAuthn) authentication.
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-control.
2. Run the setup wizard
For non-interactive runs see
docs/bootstrap_startup.md
and the example recipe at examples/did-hosting-control-build.toml. The
recipe handles online VTA flow (with --setup-key-file), the
air-gapped offline-prepare / offline-complete pair, env-var
overlays, and reprovision safety.
The interactive wizard configures:
- Configuration file path — where to write
config.toml - Server DID identity — for DIDComm authentication
- Public URL — for passkey (WebAuthn) RP origin
- Host / port — listen address (default:
0.0.0.0:8532) - Log level / format — logging configuration
- Data directory — persistent storage path
- Secrets backend — where to store private key material
- Admin bootstrap — create an initial admin ACL entry
3. Start the control plane
If built with --features ui, browse to
http://localhost:8532/ to access the management UI.
Configuration
The control plane 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
CONTROL_CONFIG_PATH environment variable.
Example config.toml
= "did:webvh:control.example.com"
= "did:webvh:mediator.example.com"
= "https://control.example.com"
[]
= true
[]
= "0.0.0.0"
= 8532
[]
= "info"
[]
= "data/did-hosting-control"
[]
= 900
= 86400
= 300
= 86400
[]
= "did-hosting-control"
# Service registry — register backend instances
# [[registry.instances]]
# label = "Primary Server"
# service_type = "server"
# url = "http://localhost:8530"
#
# [[registry.instances]]
# label = "Witness"
# service_type = "witness"
# url = "http://localhost:8531"
[]
= 60 # seconds
Service Registry
The [registry] section configures backend service instances
that the control plane manages and proxies requests to.
Static instances can be defined in config.toml:
[[]]
= "Primary Server"
= "server" # server, witness, or watcher
= "http://localhost:8530"
[[]]
= "Witness Node"
= "witness"
= "http://localhost:8531"
[[]]
= "EU Watcher"
= "watcher"
= "http://watcher-eu:8533"
Instances can also be managed dynamically via the registry API.
Environment Variable Overrides
Environment variables use the CONTROL_ prefix:
| Variable | Description |
|---|---|
CONTROL_CONFIG_PATH |
Path to config file |
CONTROL_SERVER_DID |
Control plane DID |
CONTROL_MEDIATOR_DID |
Mediator DID |
CONTROL_PUBLIC_URL |
Public URL (passkey origin) |
CONTROL_SERVER_HOST |
Bind host |
CONTROL_SERVER_PORT |
Bind port |
CONTROL_LOG_LEVEL |
Log level |
CONTROL_REGISTRY_HEALTH_CHECK_INTERVAL |
Health check interval (sec) |
CLI Commands
did-hosting-control # Run control plane (default)
did-hosting-control setup # Interactive config wizard
did-hosting-control add-acl --did <DID> [--role admin|owner] [--label <name>] # Add ACL entry
did-hosting-control list-acl # List ACL entries
Features
Management UI
When built with the ui feature, the control plane embeds a
web-based management interface using rust-embed. The UI is
served as a fallback for any non-API GET requests — no separate
web server needed.
The UI provides:
- Server health and DID counts
- DID creation, upload, and deletion
- Witness proof management
- Access control management (admin only)
- Service instance overview
Passkey Authentication
Passkey/WebAuthn support is always compiled into the control plane.
It is activated at runtime when a public_url is configured: the
control plane then supports WebAuthn passkey enrollment and login,
providing browser-based passwordless authentication alongside DIDComm
challenge-response auth. Without a public_url, WebAuthn is not
initialised and authentication falls back to DID challenge-response.
Reverse Proxy
The control plane proxies requests to registered backend service instances. This allows the UI to communicate with all services through a single origin (no CORS issues):
UI → /api/server/{instance_id}/dids → did-hosting-server
UI → /api/witness/{instance_id}/witnesses → webvh-witness
Health Checking
Registered service instances are periodically health-checked.
The health check interval is configurable via
registry.health_check_interval (default: 60 seconds).
API Endpoints
All API endpoints are under the /api prefix.
Authentication
| Method | Path | Description |
|---|---|---|
POST |
/api/auth/challenge |
Request challenge |
POST |
/api/auth/ |
Submit DIDComm auth |
POST |
/api/auth/refresh |
Refresh token |
POST |
/api/auth/passkey/enroll/start |
Start passkey enroll |
POST |
/api/auth/passkey/enroll/finish |
Finish passkey enroll |
POST |
/api/auth/passkey/login/start |
Start passkey login |
POST |
/api/auth/passkey/login/finish |
Finish passkey login |
Access Control (admin only)
| Method | Path | Description |
|---|---|---|
GET |
/api/acl |
List ACL entries |
POST |
/api/acl |
Create ACL entry |
PUT |
/api/acl/{did} |
Update ACL entry |
DELETE |
/api/acl/{did} |
Remove ACL entry |
DID Management
All routes require Bearer-token authentication; ownership and admin gating is enforced per-handler.
| Method | Path | Description |
|---|---|---|
GET |
/api/dids |
List DIDs (owners see their own; admins see all, or filter by ?owner=did:...). |
POST |
/api/dids |
Reserve a DID slot (mnemonic + URL). Body: { "path"?: string, "force"?: bool }. |
POST |
/api/dids/check |
Check whether a custom path is available. Body: { "path": string }. |
POST |
/api/dids/register |
Atomic claim-and-publish (closes the resolvability gap of POST /api/dids + PUT /api/dids/{m}). Body: { "path": string, "did_log": string, "force"?: bool }. |
GET |
/api/dids/{*mnemonic} |
Get DID record + log metadata. |
PUT |
/api/dids/{*mnemonic} |
Publish a signed did.jsonl log. Body: text/plain JSONL. |
DELETE |
/api/dids/{*mnemonic} |
Delete a DID and its associated content. |
PUT |
/api/owner/{*mnemonic} |
Transfer ownership. Body: { "new_owner": string }. New owner must be in the ACL. |
PUT |
/api/disable/{*mnemonic} |
Toggle disabled = true on the record (resolvers serve gone). |
PUT |
/api/enable/{*mnemonic} |
Toggle disabled = false. |
POST |
/api/rollback/{*mnemonic} |
Remove the last log entry (decrements version_count). |
GET |
/api/log/{*mnemonic} |
Parsed log entries as structured JSON. |
GET |
/api/raw/{*mnemonic} |
Raw did.jsonl content as text/plain. |
PUT |
/api/witness/{*mnemonic} |
Upload a witness proof file. Body: application/json. |
Statistics & Time-series
| Method | Path | Description |
|---|---|---|
GET |
/api/stats |
Aggregate stats across the control plane. |
GET |
/api/stats/{*mnemonic} |
Per-DID stats. |
GET |
/api/timeseries |
Server-wide time-series buckets. Query: ?range=1h|24h|7d|30d (default 24h). |
GET |
/api/timeseries/{*mnemonic} |
Per-DID time-series. Same range query. |
Service Topology & Configuration
| Method | Path | Description |
|---|---|---|
GET |
/api/services/overview |
Full topology: control plane info + every registered service + aggregate stats. |
GET |
/api/config |
Non-sensitive control-plane configuration (DIDs, URLs, feature flags). |
Service Registry (admin only)
| Method | Path | Description |
|---|---|---|
GET |
/api/control/registry |
List instances |
POST |
/api/control/registry |
Register instance |
GET |
/api/control/registry/{instance_id} |
Get instance |
DELETE |
/api/control/registry/{instance_id} |
Deregister instance |
POST |
/api/control/registry/{instance_id}/health |
Trigger health check |
Reverse Proxy
| Method | Path | Description |
|---|---|---|
* |
/api/server/{instance_id}/{path} |
Proxy to server instance |
* |
/api/witness/{instance_id}/{path} |
Proxy to witness instance |
Health
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check |
Library Usage
The did-hosting-control crate can be used as a library (e.g., by the did-hosting-daemon). It exposes:
did_hosting_control::config::AppConfig— configurationdid_hosting_control::server::AppState— application statedid_hosting_control::routes::router()— Axum routerdid_hosting_control::server::run()— standalone entry point
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.