RootResolver is a minimal, public, unauthenticated HTTP service that resolves a RecordWeb namespace or a full did:rwp to the resolverEndpoint responsible for it, by querying the global-namespace-registry.
It has no UI and no client-facing authentication. Any application that only needs "which resolver is responsible for this namespace" can call it directly, without ever holding a Fabric identity of its own.
Querying a Fabric peer requires a signed, TLS-authenticated identity. That is not something a public-facing application should ever ask an anonymous caller for, and it is not something most callers should need to care about at all. RootResolver holds exactly one, operator-configured Fabric identity and exposes a single read-only lookup on top of it. This mirrors how a public DNS resolver works: callers send a plain query and get back an answer, with all cryptographic and network complexity contained on the server side.
id is either:
- a raw Global Namespace Identifier (canonical UUIDv4, per RWP-23), or
- a full
did:rwp:<namespace>:...identifier, from which the namespace is extracted automatically.
Success — 200 OK
{
"namespace": "a3f9e21c-...",
"resolverEndpoint": "https://records.recordweb.org/api/v1/records",
"cacheAge": 0
}cacheAge is the number of seconds since this namespace was last resolved against the chaincode; 0 means the answer came fresh from the network just now.
Errors
| Status | Meaning |
|---|---|
400 |
id is missing or is neither a valid namespace nor a did:rwp:... identifier. |
404 |
The namespace is syntactically valid but not registered in the namespace registry. |
502 |
The Fabric network or chaincode could not be reached or returned an error. |
By design, the response contains only namespace, resolverEndpoint, and cacheAge — no registration metadata (registeredBy, registeredAt, txId, endorsements, etc.). RootResolver answers "where do I ask about this namespace", nothing more; anyone needing full registry metadata should use an admin-facing tool such as rw-gnr-admin instead.
Returns 200 OK with { "status": "ok", "cache": { "ttlSeconds": ..., "entries": ... } }. Does not touch the Fabric network — safe to use as a lightweight container/orchestrator liveness probe.
Resolved namespaces are kept in a per-process, in-memory Map with a configurable TTL (CACHE_TTL_SECONDS, default 60). There is no external cache store (e.g. Redis): a namespace-registry lookup is cheap enough that per-instance caching is sufficient to absorb repeated queries for the same namespace, and it keeps the service genuinely minimal and stateless-per-restart. Set CACHE_TTL_SECONDS=0 to disable caching entirely and always hit the chaincode.
If you run multiple replicas of RootResolver behind a load balancer, each replica maintains its own cache independently — this is an intentional trade-off for simplicity over perfectly synchronized caching, appropriate for routing data that changes rarely.
Caller
│ GET /resolve?id=did:rwp:<namespace>:... (or ?id=<namespace>)
▼
server.js ──▶ cache.js (namespace → resolverEndpoint, TTL)
│ │ miss
│ ▼
└────────▶ fabricConnect.js ──▶ Fabric peer (gRPC/mTLS) ──▶ namespace-registry chaincode
server.js– Express app; the only HTTP surface. Validatesid, checks the cache, falls through to the chaincode on a miss, and strips the response down tonamespace/resolverEndpoint/cacheAge.fabricConnect.js– Fabric Gateway client. Unlike RecordFinder, the gRPC client, identity, and signer are built once and reused across requests (no per-request peer selection here), keeping the hot path to a singleevaluateTransactioncall.cache.js– The in-memory TTL cache described above.
There is deliberately no frontend/ directory and no static assets — this service is meant to be called by other services and applications, not opened in a browser.
- Access to a peer of the RootResolver network and a Fabric identity (certificate + private key) with read access to the relevant channel.
- Node.js 20+ if running without Docker.
All values are set via environment variables, see .env.example:
| Variable | Meaning | Default |
|---|---|---|
PORT |
HTTP port of the service | 3000 |
CRYPTO_CONFIG_HOST_PATH |
Path to the local crypto material (outside the container) | /opt/rw-rrn/crypto-config |
CRYPTO_DIR |
Base directory of the MSP/TLS crypto material inside the container | /crypto/peerOrganizations/tws.rwrrn.recordweb.dev |
MSP_ID |
MSP ID of the service's Fabric identity | TWSOrgMSP |
ORG_DOMAIN |
Organisation domain, used to derive the default admin identity name | org.recordweb.dev |
ADMIN_IDENTITY |
Name of the admin identity as registered/enrolled with the Fabric CA (live-CA networks typically use a custom name, e.g. tws-org-admin, rather than the cryptogen-style Admin@<org-domain>) |
Admin@${ORG_DOMAIN} |
ADMIN_CERT_FILENAME |
Filename of the signing certificate under users/<ADMIN_IDENTITY>/msp/signcerts/ (fabric-ca-client enroll typically produces a plain cert.pem) |
${ADMIN_IDENTITY}-cert.pem |
CHANNEL_NAME |
Fabric channel of the RootResolver network | rw-gnr-test |
CHAINCODE_NAME |
Name of the namespace-registry chaincode | namespace-registry |
PEER_ENDPOINT |
The single peer this instance talks to (host:port) |
peer0.tws.rwrrn.recordweb.dev:7051 |
PEER_HOST_ALIAS |
TLS SAN hostname of that peer | peer0.tws.rwrrn.recordweb.dev |
CACHE_TTL_SECONDS |
In-memory cache TTL per namespace, in seconds (0 disables caching) |
60 |
npm install
cp .env.example .env # adjust values
node server.jscurl "http://localhost:3000/resolve?id=did:rwp:a3f9e21c-....:xyz123"
curl "http://localhost:3000/health"cp .env.example .env # adjust values, including CRYPTO_CONFIG_HOST_PATH
docker compose up -d --builddocker-compose.yml mounts the crypto material read-only into the container at /crypto, sourced from the host path configured via CRYPTO_CONFIG_HOST_PATH in .env (this variable must live in the same .env file as docker-compose.yml, since Compose only interpolates ${...} placeholders from a root-level .env, not from variables passed into the container via env_file). It also joins the external tws-proxy Docker network so a shared nginx reverse proxy can reach the container by its Docker DNS name (rootresolver).
Deployment to a VPS is automated via .github/workflows/deploy.yml (SSH deploy on push to main), provided the secrets VPS_HOST, VPS_USER, and VPS_SSH_KEY are configured in the repository and /opt/rootresolver exists on the VPS with a checked-out copy of this repository.
- The service performs read-only operations exclusively (
evaluateTransaction, neversubmitTransaction); it cannot modify namespace registrations. - It exposes no Fabric identity, MSP, channel, chaincode, or peer selection to its callers — the entire API surface is the two GET endpoints described above.
- Crypto material (private keys, admin certificates) never leaves the container filesystem and should be protected on the host with restrictive file permissions or a secrets manager.
- Because
/resolveis unauthenticated by design, consider fronting it with basic rate limiting at the reverse-proxy level if it is exposed to the public internet at scale.
RootResolver implements the read side of the namespace-resolution model specified in RWP: given a namespace, find the resolver responsible for it, without ever exposing DID documents, records, or registry metadata beyond the routing answer itself. Details on network setup, governance, and the rollout plan for the RootResolver network are available in the RootResolver Network Operating Handbook maintained by the RecordWeb organisation.