The Universal Resolver turns a DID into a DID document over plain HTTP
Universal Resolver implementation and drivers.
At a glance
- What is it?
- The Universal Resolver is a Java implementation of a resolution service for Decentralized Identifiers, with one driver per DID method and a REST API for looking any of them up. The trade is that it is a resolver and not a wallet: it tells you where a DID document lives and what is in it, and it does not hold keys or prove control of an identity.
- Who is it for?
- The Universal Resolver suits a developer building against many DID methods at once, who wants one HTTP endpoint instead of one client library per method, and who is willing to run the service rather than call it. It does not suit a project that needs to resolve identifiers on a chain it controls, because the drivers reach out to the public ledgers and networks the identifiers live on.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Java, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One endpoint for identifiers that live in different places
The problem is that a Decentralized Identifier is a syntax, not a storage location. A did:web identifier resolves through HTTPS, a did:ethr identifier is an address on a blockchain, a did:sov identifier lives in a Sovrin network, and each of those needs a different client library, a different network configuration and a different failure mode. The Universal Resolver exists to collapse that. The README describes it as resolving Decentralized Identifiers across many different DID methods, based on the W3C DID Core 1.0 and the DID Resolution specifications, and it is a work item of the DIF Identifiers and Discovery Working Group. The intended user is an application developer who has to support several DID methods and does not want to embed a separate client for each. There is also a DIF-hosted instance at dev.uniresolver.io described as usable for testing purposes, and Docker Hub images for the service.
The API is a GET on the identifier, and the identifier is URL path data
The interface is a single HTTP GET, and the identifier goes straight into the path after a version prefix. The README gives a long list of examples, one per method, and the shape is consistent:@@C1@@The version segment, 1.0 in these examples,
curl -X GET http://localhost:8080/1.0/identifiers/did:web:did.actor:alice
curl -X GET http://localhost:8080/1.0/identifiers/did:web:did.actor:bob
curl -X GET http://localhost:8080/1.0/identifiers/did:web:did.actor:mikefollows the host and precedes the identifiers path, which is a practical way of letting the API change shape without breaking callers. The reason the curl list is long is the point of the project: the same call works across did:jwk, did:dyne, did:sov, did:btcr, did:v1, did:key, did:web, did:ethr, did:ens, did:peer, did:eosio, did:jolo, did:stack, did:hcr, did:neoid, did:elem, did:github, did:ccp, did:ont, did:kilt, did:factom, did:mpg, did:io, did:bba, did:schema, did:ion, did:gatc, did:icon, did:vaa, did:unisot, did:sol, did:lit, did:ebsi, did:emtrust, did:meta, did:tz, did:pkh, did:orb, did:oyd, did:moncon, did:dock and did:mydata. That is the concrete measure of the project's scope, and it is also the list of dependencies your deployment inherits.
Running it locally is a clone and two compose commands
The quick start is short and it assumes Docker Compose, and the README frames it as deploying the Universal Resolver on your local machine by cloning the repository and using docker-compose to build and run the resolver as well as its drivers:
git clone https://github.com/decentralized-identity/universal-resolver
cd universal-resolver/
docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml up@@C2@@The compose file pulls the images before starting them, which matters because the service is composed of several containers rather than one. Once it is up you resolve identifiers locally with the curl requests above. The compose file defines a network named universal-resolver and a service named uni-resolver-web that publishes port 8080, and the environment section of that service is where the driver topology becomes visible, with one uniresolver_web_driver_url_did_* variable per method.
Every method is a separate container behind a URL
The environment block in docker-compose.yml is the architecture of the project in one file. Each method is reached through its own driver URL, with a variable per method, and the compose file sets them for did:btcr, did:sov, did:indy, did:v1, did:stack, did:web, did:ethr, did:ens, did:peer, did:eosio, did:jolo, did:hcr, did:elem, did:github, did:ccp, did:ont, did:kilt, did:factom, did:io, did:bba, did:schema, did:ion, did:ace, did:gatc, did:icon, did:vaa, did:unisot, did:sol and did:aip. The first block of the service reads like this:
uni-resolver-web:
image: universalresolver/uni-resolver-web:latest
ports:
- "8080:8080"
environment:@@C3@@The comment above those variables explains the fallback: override the default driver URLs if they are set in .env variables, otherwise do not define the variables and let the uni-resolver-web container use its default set in application.yml. So a driver can be hosted anywhere, inside the compose network or on a separate host, and the .env file is where you point at it. The repository root holds that .env file alongside docker-compose.yml, and the module layout mirrors the design, with driver/ for the drivers, driver-http/ for the HTTP driver client, uni-resolver-core/ for the core API and resolution logic, uni-resolver-web/ for the web service, uni-resolver-local/ and uni-resolver-client/ for the other pieces, plus openapi/ and docs/ directories.
Maven modules, a Java core, and a JavaScript client
The build is Maven, with a pom.xml at the root and a second one under examples/, and the primary language in the repository is Java. The module names tell you the layering. uni-resolver-core holds the resolution logic and the API, uni-resolver-web is the service that exposes it, driver/ is where the per-method implementations live, and driver-http/ is the piece that talks to a driver over HTTP rather than in process, which is what makes the container-per-driver topology possible at all. uni-resolver-client is the client library for consuming the API, and uni-resolver-local/ is the variant that resolves within the same process without the HTTP hop. There is also an openapi/ directory, so the API is described as an OpenAPI document rather than only in prose, and a ci/ directory with the build pipeline alongside a .gitlab-ci.yml at the root. The examples/ directory includes a sovrin/ example, which suggests the driver API is meant to be copied as a starting point.
What a resolver is not, and why that matters for a wallet
The limitation is conceptual rather than technical, and it is the one to get right before building on this. A resolver answers where a DID document is and what it contains, and it does so by asking the network the identifier points at. It does not hold a key, it does not sign anything, and it does not prove that the caller controls the identity the DID describes. A wallet or an agent is the component that holds keys and produces signatures, and the resolver sits outside that boundary. That is the right division for a resolution service, and it is also why the resolver is safe to expose publicly, but it means a resolver on its own cannot answer the question an application usually has, which is whether this user is who they say they are. For a service that needs to verify a signature or authenticate a holder, the resolver gives you the document and the verification is still your problem. A related constraint is that resolution quality depends on the networks being reachable, so a resolver deployed in an environment without access to a particular chain or ledger will fail for the identifiers that depend on it.
Apache-2.0, an old release tag, and a recently moving repository
The licence is Apache-2.0 with the LICENSE file at the root, which is a permissive choice for an implementation that other identity projects are expected to embed or run. The version history is the part that needs a note. The recent releases are 0.5.0 from 2022-01-07, v0.4.0 from 2021-08-21 and v0.3.1 from 2020-11-17, so the newest tagged release predates the current state of the code by several years, while the last push to the repository was on 2026-10-20 and the default branch is main. The practical reading is that this is an actively changed repository whose release tags have not kept up, which is the same pattern to check for in any project you are going to depend on. The documentation is external rather than in the repository, with the project homepage at uniresolver.io and the DIF-hosted test instance at dev.uniresolver.io, and the README points to a blog post and a webinar for background on the resolution model.
Editorial conclusion
The Universal Resolver suits a developer building against many DID methods at once, who wants one HTTP endpoint instead of one client library per method, and who is willing to run the service rather than call it. It does not suit a project that needs to resolve identifiers on a chain it controls, because the drivers reach out to the public ledgers and networks the identifiers live on. Before adopting, run the docker compose stack and resolve one identifier per method you care about, confirm the driver list in docker-compose.yml still matches the methods your users hold, and note that the last push to the repository was on 2026-10-20 with 0.5.0 from 2022-01-07 as the newest release tag.
Frequently asked questions
How do I run the Universal Resolver locally?
Clone the repository, change into the universal-resolver directory, then run `docker compose -f docker-compose.yml pull` followed by `docker compose -f docker-compose.yml up`. The README says this builds and runs the Universal Resolver as well as its drivers, and you then resolve identifiers with curl requests against port 8080.
What is the Universal Resolver API endpoint format?
A GET request to a version-prefixed path of the form http://localhost:8080/1.0/identifiers/<did>, with the identifier placed directly in the path. The README lists examples for methods including did:jwk, did:web, did:ethr, did:sov, did:key and did:peer.
How are the DID method drivers configured in docker-compose.yml?
The uni-resolver-web service has one uniresolver_web_driver_url_did_* environment variable per method. The compose file's comment says to override the default driver URLs if they are set in .env variables, and otherwise to leave the variables undefined so the container uses the defaults set in application.yml.
Can the Universal Resolver verify that I control a DID?
No. It resolves a DID to its DID document, telling you where the document is and what it contains. It does not hold keys, sign anything or prove control of an identity, so verifying a signature or authenticating a holder is still the caller's responsibility.
What licence is the Universal Resolver released under?
Apache-2.0, with the licence text in the LICENSE file at the root of the repository. The newest release tag is 0.5.0 from 2022-01-07, while the last push to the repository was on 2026-10-20.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/decentralized-identity-universal-resolver)Was this useful?