Skip to content

ENS Omnigraph API

The ENS Omnigraph API is the world’s first and only API providing unified access to the full state of ENSv1 and ENSv2. It’s a single, polymorphic GraphQL API over both ENSv1 and ENSv2 — write your query once and get correct, typed results regardless of which protocol version a given Domain lives in.

ENS Omnigraph diagram

The Omnigraph is delivered by ENSApi on top of the indexed Unigraph in ENSDb. It follows the Relay specification, abstracts away the most common ENS-protocol footguns, and exposes enough of the underlying protocol for builders who need to go deep.

When ENSv2 launches in Summer 2026, the two protocol versions coexist — and the Omnigraph keeps your app working against both, at the same time, with no code changes. Both ENSv1 and ENSv2 Domains are indexed concurrently and exposed through a unified schema.

Addressing a Domain by name (domain(by: { name: "vitalik.eth" })) returns an ENSv2 Domain if the domain has been optionally upgraded by its owner to ENSv2, and returns an ENSv1 Domain otherwise. Set-returning queries (domains(where: { name: ... })) return both the ENSv1 and ENSv2 onchain representations of the same domain, unless explicitly filtered with version: ENSv1 or version: ENSv2.

A Namegraph is the native onchain data model of ENSv2: it represents names not as a flat mapping of namehashes in a Nametable (as in ENSv1), but as a graph of Registry → Domain → Registry → Domain → …. This graph may be cyclic, and within the ENSv2 protocol an unbounded number of disjoint (not connected) Namegraphs will exist. Within the unified ENS protocol (v1+v2) there will exist many Namegraphs, at the very least those headed by the ENSv1 Root Registry, Basenames, Lineanames, 3DNS, and the ENSv2 Root Registry.

The Unigraph is the entire collection of these disjoint ENSv2 Namegraphs and multiple ENSv1 Nametables, combined together into a single unified data model using ENS Resolution semantics. Navigating the Unigraph from "eth" down to "vitalik.eth" and beyond looks identical regardless of whether the underlying entities are ENSv1 or ENSv2.

The unigraph plugin in ENSIndexer is what builds this unified model. It’s also where multichain coverage lives: Basenames (.base.eth), Lineanames (.linea.eth), and 3DNS names (.box) are all materialized into the same Unigraph as mainnet .eth, so a single query covers all indexable names in one shot.

Given that a Domain entity (say, the sub in sub.example.eth) can be reached by infinitely many aliases (for example, sub.other.eth), it becomes important to determine a canonical reference to the Domain — this is the Canonical Name. Canonicality is also connected to nameability; if an ENSv2 Domain exists on-chain but isn’t eventually connected to the Root Registry somehow, it doesn’t have a name!

Within the Omnigraph API the complexity of this is reduced, and all Canonical Domains (those with names) are queryable, searchable, and addressable by said name. Domains that are not canonical are still referenceable by id (eg. domain(by: { id: DomainId! })).

Canonical Domains have a Domain.canonical field hosting the canonicality-derived fields such as name, node, depth (i.e. 2 for vitalik.eth), and path from the ENS root (i.e. [Domain("eth"), Domain("vitalik.eth")]).

Every entity in the Omnigraph has an id — a nominally-typed, stable reference to a specific on-chain entity (DomainId, RegistryId, RegistrationId, etc.). When you already know the entity you mean, address it by id: domain(by: { id: "..." }) always returns the same on-chain Domain, even if its canonical name changes across time.

Addressing a Domain by name is a different operation. It’s Unigraph forward traversal: domain(by: { name: "vitalik.eth" }) walks from the ENSv2 Root Registry → "eth" in that Registry → the Registry that "eth" points at (the EthRegistry) → "vitalik" in that Registry. The Domain returned is whichever on-chain entity (if any) that path currently resolves to.

These two views are not interchangeable:

  • The id you receive from a name lookup is the stable reference to the on-chain entity that the Namegraph currently resolves "vitalik.eth" to.
  • But "vitalik.eth" is not a stable reference to that entity. The Namegraph can be re-parented or re-aliased — and tomorrow, "vitalik.eth" may resolve to an entirely different on-chain Domain.

Rule of thumb: address by name when you’re answering “which Domain would records come from if resolved right now”; address by id when you’re answering “what’s the latest state of this specific on-chain entity?”.

Domain, Registry, and Registration are GraphQL interfaces, with concrete types implementing each:

  • DomainENSv1Domain, ENSv2Domain
  • RegistryENSv1Registry, ENSv1VirtualRegistry, ENSv2Registry
  • RegistrationBaseRegistrarRegistration, NameWrapperRegistration, ThreeDNSRegistration, ENSv2RegistryRegistration, ENSv2RegistryReservation

Shared fields are available unconditionally on the interface. Protocol- or implementation-specific fields are reached via typed inline fragments — ... on ENSv1Domain { rootRegistryOwner }, ... on ENSv2Domain { tokenId }, ... on BaseRegistrarRegistration { wrapped { fuses } }, ... on NameWrapperRegistration { fuses }. The result is a single query that compiles, type-checks, and returns the right fields for whichever concrete type each record turns out to be.

Every name and label crossing the Omnigraph surface is an Interpreted Name (or Interpreted Label). Each label in an Interpreted Name is either a normalized literal label or an Encoded LabelHash ([abc123…]) when the literal isn’t known or is unnormalized. This eliminates one of the most common ENS UI footguns — unnormalized labels, unhealed hashes, and rendering surprises — at the schema layer, making UI rendering trivial. See terminology for the full definition.

Collection and paginated relationship fields in the schema follow the Relay-spec Connection pattern with edges, pageInfo, and totalCount. Cursor-based pagination is idiomatic in urql, Apollo, Relay, and most modern GraphQL clients — infinite scroll and stable pagination work out of the box, with no per-endpoint plumbing.

The Omnigraph indexes every onchain Event relevant to ENS and exposes it from the entities each Event relates to:

  • Domain.events — every Event for a specific Domain
  • Resolver.events — every Event emitted by a specific Resolver
  • Account.events — every Event for which an Account is the HCA-aware sender
  • Permissions.events, PermissionsUser.events — Permission grant and revocation history

Each Event carries chain, block, transaction, and log metadata, plus an HCA-aware sender field distinct from the raw tx.from for HCA-mediated transactions.

Permissions are modeled as top-level entities. Permissions represents a contract that manages role grants; PermissionsResource is an addressable resource within that contract; PermissionsUser is a specific user’s role bitmap on a specific resource.

Registries, Resolvers, and ENSv2 Domains all expose their Permissions directly (Registry.permissions, Resolver.permissions, ENSv2Domain.permissions), and an Account can be queried for every Permission it’s been granted (Account.permissions, Account.registryPermissions, Account.resolverPermissions). Access-aware UIs — “which Domains can this address manage?”, “who can update this Registry?” — become a single query.

The polymorphism, Namegraph traversal, Canonical Name, Events, and version-specific fragments — all in one query:

query EthAndSubdomains {
domains(where: { name: { eq: "eth" } }) {
edges {
node {
__typename
id
canonical {
name { interpreted }
depth
}
events(first: 5) {
totalCount
edges {
node {
timestamp
transactionHash
}
}
}
subdomains(first: 10, order: { by: NAME }) {
totalCount
edges {
node {
__typename
id
canonical { name { interpreted } }
... on ENSv1Domain {
rootRegistryOwner { address }
}
... on ENSv2Domain {
tokenId
}
}
}
}
... on ENSv1Domain {
rootRegistryOwner { address }
}
... on ENSv2Domain {
tokenId
}
}
}
}
}