From 140748c1a4edbc190f160d0604f79c7fc13335d0 Mon Sep 17 00:00:00 2001
From: shrugs
Date: Wed, 13 May 2026 11:40:08 -0500
Subject: [PATCH 01/17] checkpoint: quickstart
---
.../src/content/docs/docs/integrate/index.mdx | 162 +++++++++++++++++-
1 file changed, 159 insertions(+), 3 deletions(-)
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
index c9aed63f8c..3366c0dbd5 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
@@ -6,11 +6,168 @@ sidebar:
order: 1
---
+import { LinkCard } from '@astrojs/starlight/components';
-:::caution[Coming Soon]
-We're actively working on this page right now. Check back by May 18th for full content!
+
+:::tip[Prepare for ENSv2]
+The ENSv2 upgrade to the ENS protocol is comming **Summer 2026**! Your app, regardless of how it interacts with names, needs to be updated to avoid being left behind.
+
+
+ Learn more about ENSv2 Readiness
+
:::
+ENSNode fully supports the ENSv2 upgrade via the [Omnigraph API](/docs/integrate/omnigraph), a _unified_ API over **both** ENSv1 and ENSv2. ENSNode takes the guesswork out of ENS integrations, whether you need to resolve up to date records, search all Domains, or see which Domains a user owns (and much, much more).
+
+TODO: insert the Omnigraph ENSv1 + ENSv2 asset here
+
+You can integrate with ENSNode in many different ways, whether you're using React, any Javascript runtime, or raw GraphQL.
+
+
+
+## 1. `enskit` + Omnigraph
+
+With `enskit`, leverage ENSNode and the Omnigraph to power your React components using `useOmnigraphQuery`. `enskit` comes with built-in type-safety, Omnigraph-specific cache directives, easy infinite pagination, and much much more.
+
+
+```tsx
+// this is fully typechecked and supports editor autocomplete!
+const DomainFragment = graphql(`
+ fragment DomainFragment on Domain {
+ __typename
+ id
+ name
+ owner { id address }
+ }
+`);
+
+// this is fully typechecked and supports editor autocomplete!
+const DomainByNameQuery = graphql(`
+ query DomainByNameQuery($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ ...DomainFragment
+ subdomains {
+ edges { node { ...DomainFragment } }
+ }
+ }
+ }
+`,
+ [DomainFragment],
+);
+
+function RenderDomainFragment({ data }: { data: FragmentOf }) {
+ // type-safe access to fragment data!
+ const domain = readFragment(DomainFragment, data);
+
+ return (
+ <>
+ Name: {domain.canonical ? beautifyInterpretedName(domain.canonical.name) : 'Unnamed Domain'}
+ Protocol Version: {domain.__typename === 'ENSv1Domain' ? 'ENSv1' : 'ENSv2'}
+ Owner: {domain.owner ? domain.owner.address : 'Unowned'}
+ >
+ );
+}
+
+export function RenderDomainAndSubdomains({ name }: { name: InterpretedName }) {
+ // `result` is fully typed!
+ const [result] = useOmnigraphQuery({ query: DomainByNameQuery, variables: { name } });
+
+ // some loading/error handling
+ if (!data && fetching) return
Loading...
;
+ if (error) return
Error: {error.message}
;
+ if (!data?.domain) return
No domain was found with name '{name}'.
;
+
+ // now we have type-safe access to Domain!
+ const domain = readFragment(DomainFragment, data.domain);
+ const { subdomains } = data.domain;
+
+ return (
+
+ );
+}
+```
+
+
+
+
+
+## 2. `enssdk` + Omnigraph
+
+With `enssdk`, leverage ENSNode and the Omnigraph from any Javascript runtime to power your frontend or backend apps. `enssdk` comes with built-in type-safety and editor autocomplete for Omnigraph queries.
+
+
+```ts
+// create and extend an EnsNodeClient with Omnigraph API support
+const client = createEnsNodeClient({ url: process.env.ENSNODE_URL! }).extend(omnigraph);
+
+// this is fully typechecked and supports editor autocomplete!
+const HelloWorldQuery = graphql(`
+ query HelloWorld {
+ domain(by: { name: "eth" }) {
+ id
+ name
+ owner { address }
+ }
+ }
+`);
+
+// `result` is fully typed!
+const result = await client.omnigraph.query({ query: HelloWorldQuery });
+```
+
+
+
+## 3. Omnigraph GraphQL API
+
+The Omnigraph API is a GraphQL API following the Relay specification, so you get built-in support for efficient infinite pagination and idomatic access to all of the ENS protocol within a _unified_ ENSv1 + ENSv2 datamodel.
+
+```gql
+query MyDomains($address: Address!) {
+ account(by: { address: $address }) {
+ domains {
+ edges {
+ node {
+ label { interpreted }
+ canonical { name }
+ }
+ }
+ }
+ }
+}
+```
+
+
+
This page will be a 60-second quickstart for querying ENSv2 data through ENSNode — including:
- A **React example** with [enskit](/docs/integrate/integration-options/enskit) using the `useOmnigraphQuery` hook.
@@ -18,4 +175,3 @@ This page will be a 60-second quickstart for querying ENSv2 data through ENSNode
- Links to [hosted instances](/docs/integrate/hosted-instances) so you can start querying immediately with zero setup.
- [ENSv2 Readiness](/docs/integrate/ensv2-readiness) — how building with ENSNode today prepares your app for ENSv2.
- [Integration Options](/docs/integrate/integration-options) — choose the right path: enskit, enssdk, raw GraphQL, or ENSDb.
-
From dec0361bd1d619e87e0afaf8e45715ac0d7b565d Mon Sep 17 00:00:00 2001
From: shrugs
Date: Wed, 13 May 2026 13:26:59 -0500
Subject: [PATCH 02/17] checkpoint: enskit
---
.../src/content/docs/docs/integrate/index.mdx | 8 +-
.../integrate/integration-options/enskit.mdx | 397 +++++++++++++++++-
.../integrate/integration-options/index.mdx | 35 +-
examples/enskit-react-example/tsconfig.json | 5 -
4 files changed, 429 insertions(+), 16 deletions(-)
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
index 3366c0dbd5..e929e93ab6 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
@@ -21,7 +21,7 @@ ENSNode fully supports the ENSv2 upgrade via the [Omnigraph API](/docs/integrate
TODO: insert the Omnigraph ENSv1 + ENSv2 asset here
-You can integrate with ENSNode in many different ways, whether you're using React, any Javascript runtime, or raw GraphQL.
+You can integrate with ENSNode in many different ways, whether you're using React, any JavaScript runtime, or raw GraphQL.
Subdomains:
@@ -117,7 +117,7 @@ export function RenderDomainAndSubdomains({ name }: { name: InterpretedName }) {
## 2. `enssdk` + Omnigraph
-With `enssdk`, leverage ENSNode and the Omnigraph from any Javascript runtime to power your frontend or backend apps. `enssdk` comes with built-in type-safety and editor autocomplete for Omnigraph queries.
+With `enssdk`, leverage ENSNode and the Omnigraph from any JavaScript runtime to power your frontend or backend apps. `enssdk` comes with built-in type-safety and editor autocomplete for Omnigraph queries.
```ts
@@ -164,7 +164,7 @@ query MyDomains($address: Address!) {
```
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
index 9a9d98c8d7..1d86c05562 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
@@ -3,8 +3,399 @@ title: enskit
description: React toolkit for ENSv2 development, includes fully typed providers for the ENS Omnigraph API.
---
-:::caution[Coming Soon]
-We're actively working on this page right now. Check back by May 18th for full content!
+import { LinkCard, Steps } from '@astrojs/starlight/components';
+
+`enskit` is the React toolkit for ENSv2 development. It provides a fully typed Omnigraph API client (powered by [`urql`](https://nearform.com/open-source/urql/) and [`gql.tada`](https://gql-tada.0no.co/)), the `OmnigraphProvider`, and the `useOmnigraphQuery` hook for writing type-safe ENS queries with editor autocomplete, Relay-style pagination, and Omnigraph-specific cache directives.
+
+This guide walks you from an empty directory to a working React component that renders an [ENS Domain](/docs/concepts/the-ens-protocol) and a paginated list of its subdomains — the same flow as the [`DomainView`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in our example app.
+
+:::tip[No backend required]
+You don't need to run your own ENSNode to follow this guide — the steps below default to a NameHash-hosted instance. Browse the available deployments below.
:::
-This page will cover enskit — the React toolkit for ENSv2 development. Includes fully typed providers for the ENS Omnigraph API (such as the useOmnigraphQuery hook), and working examples from the enskit-react-example app.
+
+
+## 1. Scaffold a React app
+
+If you already have a React + TypeScript app, skip ahead to [Install `enskit` and `enssdk`](#2-install-enskit-and-enssdk).
+
+Otherwise, the fastest way to get going is [Vite](https://vite.dev):
+
+```sh
+npm create vite@latest my-ens-app -- --template react-ts
+cd my-ens-app
+npm install
+```
+
+## 2. Install `enskit` and `enssdk`
+
+```sh
+npm install enskit@1.13.1 enssdk@1.13.1
+```
+
+:::tip[Pin exact versions]
+Always pin **exact** versions (no `^` or `~`) of `enskit` and `enssdk`, and keep them on the same version. The Omnigraph GraphQL schema is bundled inside `enssdk` and consumed by the `gql.tada` TypeScript plugin to type your queries — a minor or patch bump can change the schema and silently drift your generated types away from your queries. Locking exact versions keeps types and runtime in sync.
+:::
+
+## 3. Configure the `gql.tada` TypeScript plugin
+
+`gql.tada` is what gives your `graphql(...)` query strings end-to-end type safety. It reads the Omnigraph schema from `enssdk` at typecheck time.
+
+Add the plugin to `tsconfig.json`:
+
+```json title="tsconfig.json" {6-13}
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "plugins": [
+ {
+ "name": "gql.tada/ts-plugin",
+ "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql",
+ "tadaOutputLocation": "./src/generated/graphql-env.d.ts"
+ }
+ ]
+ },
+ "include": ["src"]
+}
+```
+
+If you're using VS Code, make sure your workspace is using the workspace TypeScript version so the plugin loads. Add this to `.vscode/settings.json`:
+
+```json title=".vscode/settings.json"
+{
+ "js/ts.tsdk.path": "node_modules/typescript/lib",
+ "js/ts.tsdk.promptToUseWorkspaceVersion": true
+}
+```
+
+## 4. Mount the `OmnigraphProvider`
+
+`OmnigraphProvider` is what `useOmnigraphQuery` reads from. Construct an `EnsNodeClient`, extend it with the `omnigraph` module, and wrap your app:
+
+```tsx title="src/App.tsx"
+import { OmnigraphProvider } from "enskit/react/omnigraph";
+import { createEnsNodeClient } from "enssdk/core";
+import { omnigraph } from "enssdk/omnigraph";
+import { StrictMode } from "react";
+
+import { DomainView } from "./DomainView";
+
+// use a local ENSNode instance or a NameHash Hosted instance URL
+const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL!
+
+// create and extend an EnsNodeClient with Omnigraph support
+const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph);
+
+export function App() {
+ return (
+
+
+
My ENS App
+
+
+
+ );
+}
+```
+
+## 5. Hello world
+
+Create `src/DomainView.tsx`. We'll start with the simplest possible query — look up the `eth` Domain and render its owner and protocol version.
+
+:::tip[InterpretedName]
+An **InterpretedName** is a Name whose labels are each either normalized or represented as an [encoded labelhash](/docs/reference/terminology#encoded-labelhash) (e.g. `[abcd...].eth`) — the canonical, lossless form ENSNode uses to identify a Name. `asInterpretedName("eth")` brands a known-safe string as one; for user input, validate first. See [Interpreted Name](/docs/reference/terminology#interpreted-name) in the terminology reference.
+:::
+
+```tsx title="src/DomainView.tsx"
+import { graphql, useOmnigraphQuery } from "enskit/react/omnigraph";
+import { asInterpretedName, beautifyInterpretedName } from "enssdk";
+
+const DomainByNameQuery = graphql(`
+ query DomainByName($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ __typename
+ canonical { name }
+ owner { address }
+ }
+ }
+`);
+
+export function DomainView() {
+ const name = asInterpretedName("eth");
+
+ const [result] = useOmnigraphQuery({
+ query: DomainByNameQuery,
+ variables: { name },
+ });
+
+ const { data, fetching, error } = result;
+
+ if (!data && fetching) return
+ );
+}
+```
+
+A few things to notice:
+
+- `graphql(...)` parses your query at typecheck time. Hover over `result.data` and you'll see it's typed exactly to your selection set — try removing `owner { address }` from the query and watch the access below become a type error.
+- `domain` is a union of `ENSv1Domain | ENSv2Domain` (both implement the `Domain` interface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query — `__typename` tells you which one you got.
+- `canonical` may be `null` for non-canonical names (e.g. Domains whose name cannot be inferred). **Always** guard the access; TypeScript will help you.
+
+## 6. List subdomains
+
+Expand the query to also fetch the Domain's subdomains. `subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm), so the shape is `{ edges: [{ node }] }`.
+
+```tsx title="src/DomainView.tsx" ins={7-13,40-54}
+const DomainByNameQuery = graphql(`
+ query DomainByName($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ __typename
+ canonical { name }
+ owner { address }
+ subdomains {
+ edges {
+ node {
+ canonical { name }
+ owner { address }
+ }
+ }
+ }
+ }
+ }
+`);
+
+export function DomainView() {
+ const name = asInterpretedName("eth");
+
+ const [result] = useOmnigraphQuery({
+ query: DomainByNameQuery,
+ variables: { name },
+ });
+
+ const { data, fetching, error } = result;
+
+ if (!data && fetching) return
+ );
+}
+```
+
+## 9. Run it
+
+```sh
+npm run dev
+```
+
+Open the printed URL and you should see the `eth` Domain, its owner, and the first page of its subdomains. Clicking **Next page** advances the cursor.
+
+## Where to go next
+
+- Swap the hardcoded `"eth"` for a name from props or a router — see [`EnsureInterpretedName`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in the example app for safe handling of user-provided names.
+- Browse other queries: account-owned domains, events, registrar permissions, full-text search. See [Omnigraph API examples](/docs/integrate/integration-options/raw-graphql).
+- Need data outside React? Use [`enssdk`](/docs/integrate/integration-options/enssdk) directly with the same `graphql(...)` helper.
+
+
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx
index cf57e67e07..39addf40d5 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx
@@ -3,8 +3,35 @@ title: Integration Options
description: Integration options for building with ENSNode.
---
-:::caution[Coming Soon]
-We're actively working on this page right now. Check back by May 18th for full content!
-:::
+import { LinkCard } from '@astrojs/starlight/components';
-This page will describe the integration options available: enskit (React), enssdk (TypeScript/JS), raw GraphQL endpoint, and building custom ENS services on ENSDb for self-hosters.
+ENSNode takes the guesswork out of ENS integrations, whether you need to resolve up to date records, search all Domains, or see which Domains a user owns (and much, much more).
+
+There are a few different ways to integrate with ENSNode, depending on your app, runtime, and needs.
+
+## 1. `enskit`
+
+With `enskit`, leverage ENSNode and the Omnigraph to power your React components using `useOmnigraphQuery`. `enskit` comes with built-in type-safety, Omnigraph-specific cache directives, easy infinite pagination, and much much more.
+
+
+
+## 2. `enssdk`
+
+With `enssdk`, leverage ENSNode and the Omnigraph from any JavaScript runtime to power your frontend or backend apps. `enssdk` comes with built-in type-safety and editor autocomplete for Omnigraph queries.
+
+
+
+## 3. Omnigraph GraphQL API
+
+The Omnigraph API is a GraphQL API following the Relay specification, so you get built-in support for efficient infinite pagination and idomatic access to all of the ENS protocol within a _unified_ ENSv1 + ENSv2 datamodel.
+
+
diff --git a/examples/enskit-react-example/tsconfig.json b/examples/enskit-react-example/tsconfig.json
index c7397b1aa4..78e1c025c7 100644
--- a/examples/enskit-react-example/tsconfig.json
+++ b/examples/enskit-react-example/tsconfig.json
@@ -12,12 +12,7 @@
// here we enable the gql.tada typescript plugin for
{
"name": "gql.tada/ts-plugin",
-
- // because this example is in the ENSNode monorepo, it imports from src
"schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql",
- // but in a normal app you'd import it from the package like so:
- // "schema": "node_modules/enssdk/omnigraph/schema.graphql",
-
"tadaOutputLocation": "./src/generated/graphql-env.d.ts"
}
]
From 849aa61655080b7255b8ef9861f517c55f8b9f84 Mon Sep 17 00:00:00 2001
From: shrugs
Date: Wed, 13 May 2026 14:12:36 -0500
Subject: [PATCH 03/17] feat: enssdk example app and integration walkthrough
---
AGENTS.md | 10 +-
.../src/content/docs/docs/integrate/index.mdx | 6 +
.../integrate/integration-options/enskit.mdx | 2 +-
.../integrate/integration-options/enssdk.mdx | 315 +++++++++++++++++-
examples/enskit-react-example/package.json | 4 +-
examples/enskit-react-example/src/App.tsx | 8 +-
examples/enssdk-example/LICENSE | 21 ++
examples/enssdk-example/README.md | 25 ++
examples/enssdk-example/package.json | 21 ++
examples/enssdk-example/src/index.ts | 80 +++++
examples/enssdk-example/tsconfig.json | 25 ++
package.json | 5 +-
pnpm-lock.yaml | 24 +-
13 files changed, 526 insertions(+), 20 deletions(-)
create mode 100644 examples/enssdk-example/LICENSE
create mode 100644 examples/enssdk-example/README.md
create mode 100644 examples/enssdk-example/package.json
create mode 100644 examples/enssdk-example/src/index.ts
create mode 100644 examples/enssdk-example/tsconfig.json
diff --git a/AGENTS.md b/AGENTS.md
index fa468412af..cdf3da4fd7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -87,8 +87,8 @@ Fail fast and loudly on invalid inputs.
- Add a changeset when your PR includes a logical change that should bump versions or be communicated in release notes: https://ensnode.io/docs/contributing/prs#changesets
- Before declaring work complete, run validation in the affected project(s):
- 1. `pnpm -F typecheck`
- 2. `pnpm lint`
- 3. `pnpm test --project [--project ]`
- 4. If OpenAPI Specs were affected, run `pnpm generate:openapi`
- 5. If the Omnigraph GraphQL Schema was affected, run `pnpm generate:gqlschema`
+ 1. If OpenAPI Specs were affected, run `pnpm generate:openapi`
+ 2. If the Omnigraph GraphQL Schema was affected, run `pnpm generate:gqlschema`
+ 3. `pnpm -F typecheck`
+ 4. `pnpm lint`
+ 5. `pnpm test --project [--project ]`
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
index e929e93ab6..8565d03e93 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
@@ -144,6 +144,12 @@ const result = await client.omnigraph.query({ query: HelloWorldQuery });
href="/docs/integrate/integration-options/enssdk"
/>
+
+
## 3. Omnigraph GraphQL API
The Omnigraph API is a GraphQL API following the Relay specification, so you get built-in support for efficient infinite pagination and idomatic access to all of the ENS protocol within a _unified_ ENSv1 + ENSv2 datamodel.
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
index 1d86c05562..3f8d148081 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
@@ -85,7 +85,7 @@ import { StrictMode } from "react";
import { DomainView } from "./DomainView";
-// use a local ENSNode instance or a NameHash Hosted instance URL
+// you may use a NameHash Hosted ENSNode, learn more at https://ensnode.io/docs/integrate/hosted-instances
const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL!
// create and extend an EnsNodeClient with Omnigraph support
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
index 046360bb4d..1e0c912731 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
@@ -3,8 +3,317 @@ title: enssdk
description: SDK for ENSv2 development in TypeScript/JavaScript.
---
-:::caution[Coming Soon]
-We're actively working on this page right now. Check back by May 18th for full content!
+import { LinkCard } from '@astrojs/starlight/components';
+
+`enssdk` is the foundational TypeScript/JavaScript SDK for ENS development. It's a fully modular and tree-shakeable modern TypeScript/JavaScript package that provides ENS-specific types and helpers — usable from any JS runtime, browser or server.
+
+`enssdk` also dirctly integrates with ENSNode, providing an `EnsNodeClient` (via `createEnsNodeClient`) that can be extended with a fully-typed Omnigraph API client (powered by [`gql.tada`](https://gql-tada.0no.co/)).
+
+This guide walks you from an empty directory to a working TypeScript script that queries the `eth` Domain and queries its subdomains — the same flow as our [enssdk-example](https://github.com/namehash/ensnode/tree/main/examples/enssdk-example).
+
+:::tip[No backend required]
+You don't need to run your own ENSNode to follow this guide — the steps below default to a NameHash-hosted instance. Browse the available deployments below.
:::
-This page will cover enssdk — the TypeScript/JavaScript SDK for ENSv2. Includes createEnsNodeClient, fully typed ENS Omnigraph API queries with gql.tada, normalization utilities, and much more.
+
+
+## 1. Scaffold a TypeScript project
+
+If you already have a TypeScript project, skip ahead to [Install `enssdk`](#2-install-enssdk).
+
+Otherwise:
+
+```sh
+mkdir my-ens-script && cd my-ens-script
+npm init -y
+mkdir src
+```
+
+## 2. Install `enssdk`
+
+We'll use [`tsx`](https://tsx.is/) to run TypeScript directly without a bundler.
+
+```sh
+npm install enssdk@1.13.1
+npm install -D tsx typescript @types/node
+```
+
+:::tip[Pin exact versions]
+Always pin an **exact** version (no `^` or `~`) of `enssdk`. The Omnigraph GraphQL schema is bundled inside `enssdk` and consumed by the `gql.tada` TypeScript plugin to type your queries — a minor or patch bump can change the schema and silently drift your generated types away from your queries. Locking the exact version keeps types and runtime in sync.
+:::
+
+## 3. Configure the `gql.tada` TypeScript plugin
+
+`gql.tada` is what gives your `graphql(...)` query strings end-to-end type safety. It reads the Omnigraph schema from `enssdk` at typecheck time.
+
+Create `tsconfig.json`:
+
+```json title="tsconfig.json"
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "lib": ["ESNext"],
+ "plugins": [
+ {
+ "name": "gql.tada/ts-plugin",
+ "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql",
+ "tadaOutputLocation": "./src/generated/graphql-env.d.ts"
+ }
+ ]
+ },
+ "include": ["src"]
+}
+```
+
+If you're using VS Code, make sure your workspace is using the workspace TypeScript version so the plugin loads. Add this to `.vscode/settings.json`:
+
+```json title=".vscode/settings.json"
+{
+ "js/ts.tsdk.path": "node_modules/typescript/lib",
+ "js/ts.tsdk.promptToUseWorkspaceVersion": true
+}
+```
+
+Also add a `start` script to `package.json`:
+
+```json title="package.json" ins={3-5}
+{
+ "type": "module",
+ "scripts": {
+ "start": "tsx src/index.ts"
+ }
+}
+```
+
+## 4. Construct the client
+
+The `EnsNodeClient` is the entry point. Extend it with the `omnigraph` module to get the `client.omnigraph.query(...)` method.
+
+```ts title="src/index.ts"
+import { createEnsNodeClient } from "enssdk/core";
+import { omnigraph } from "enssdk/omnigraph";
+
+// you may use a NameHash Hosted ENSNode, learn more at https://ensnode.io/docs/integrate/hosted-instances
+const ENSNODE_URL = process.env.ENSNODE_URL!;
+
+// create and extend an EnsNodeClient with Omnigraph support
+const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph);
+```
+
+## 5. Hello world
+
+Add your first query — look up the `eth` Domain and print its owner and protocol version.
+
+:::tip[InterpretedName]
+An **InterpretedName** is a Name whose labels are each either normalized or represented as an [encoded labelhash](/docs/reference/terminology#encoded-labelhash) (e.g. `[abcd...].eth`) — the canonical, lossless form ENSNode uses to identify a Name. `asInterpretedName("eth")` brands a known-safe string as one; for user input, validate first. See [Interpreted Name](/docs/reference/terminology#interpreted-name) in the terminology reference.
+:::
+
+```ts title="src/index.ts"
+// existing imports...
+
+import { asInterpretedName, beautifyInterpretedName } from "enssdk";
+import { graphql, omnigraph } from "enssdk/omnigraph";
+
+// existing client...
+
+// this is typechecked and editor autocompleted with built-in docs!
+const HelloWorldQuery = graphql(`
+ query HelloWorld($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ __typename
+ name
+ owner { address }
+ }
+ }
+`);
+
+async function main() {
+ const name = asInterpretedName("eth");
+
+ const result = await client.omnigraph.query({
+ query: HelloWorldQuery,
+ variables: { name },
+ });
+
+ if (result.errors) throw new Error(JSON.stringify(result.errors));
+ if (!result.data?.domain) throw new Error(`Domain '${name}' not found`);
+
+ const { domain } = result.data;
+
+ console.log(`Name: ${domain.name ? beautifyInterpretedName(domain.name) : ""}`);
+ console.log(`Version: ${domain.__typename}`);
+ console.log(`Owner: ${domain.owner?.address ?? "0x0"}`);
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
+```
+
+A few things to notice:
+
+- `graphql(...)` parses your query at typecheck time. Hover over `result.data` and you'll see it's typed exactly to your selection set — try removing `owner { address }` from the query and watch the access below become a type error.
+- `domain` is a union of `ENSv1Domain | ENSv2Domain` (both implement the `Domain` interface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query — `__typename` tells you which one you got.
+- `name` is `null` for non-canonical Domains (e.g. Domains whose name cannot be inferred). **Always** guard the access; TypeScript will help you.
+
+## 6. List subdomains
+
+Expand the query to also fetch the Domain's subdomains. `subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm) — pass `first: 20` to cap the page, and select `totalCount` to learn how many subdomains exist in total.
+
+```ts title="src/index.ts" ins={7-12,34-40}
+const HelloWorldQuery = graphql(`
+ query HelloWorld($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ __typename
+ name
+ owner { address }
+ subdomains(first: 20) {
+ totalCount
+ edges {
+ node { name owner { address } }
+ }
+ }
+ }
+ }
+`);
+
+async function main() {
+ const name = asInterpretedName("eth");
+
+ const result = await client.omnigraph.query({
+ query: HelloWorldQuery,
+ variables: { name },
+ });
+
+ if (result.errors) throw new Error(JSON.stringify(result.errors));
+ if (!result.data?.domain) throw new Error(`Domain '${name}' not found`);
+
+ const { domain } = result.data;
+
+ console.log(`Name: ${domain.name ? beautifyInterpretedName(domain.name) : ""}`);
+ console.log(`Version: ${domain.__typename}`);
+ console.log(`Owner: ${domain.owner?.address ?? "0x0"}`);
+
+ console.log(`\nSubdomains (showing 20 of ${domain.subdomains?.totalCount ?? 0}):`);
+ console.table(
+ (domain.subdomains?.edges ?? []).map(({ node }) => ({
+ name: node.name ? beautifyInterpretedName(node.name) : "",
+ owner: node.owner?.address ?? "0x0",
+ })),
+ );
+}
+```
+
+`console.table` renders the rows as a nicely-aligned table in your terminal.
+
+To page beyond the first 20, the connection also exposes `pageInfo { hasNextPage endCursor }` and accepts an `after: String` cursor — see [Relay's connection spec](https://relay.dev/graphql/connections.htm).
+
+## 7. Extract a typed fragment
+
+Notice we're selecting the same fields (`name`, `owner { address }`) on the parent Domain _and_ on each subdomain, and rendering them the same way. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed function that formats a Domain.
+
+```ts title="src/index.ts" ins={3,10-16,18,22,25,30,33-39,53,55,58-65,67-69}
+import { asInterpretedName, beautifyInterpretedName } from "enssdk";
+import { createEnsNodeClient } from "enssdk/core";
+import { type FragmentOf, graphql, omnigraph, readFragment } from "enssdk/omnigraph";
+
+// you may use a NameHash Hosted ENSNode, learn more at https://ensnode.io/docs/integrate/hosted-instances
+const ENSNODE_URL = process.env.ENSNODE_URL!;
+
+const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph);
+
+const DomainFragment = graphql(`
+ fragment DomainFragment on Domain {
+ __typename
+ name
+ owner { address }
+ }
+`);
+
+const HelloWorldQuery = graphql(
+ `
+ query HelloWorld($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ ...DomainFragment
+ subdomains(first: 20) {
+ totalCount
+ edges { node { ...DomainFragment } }
+ }
+ }
+ }
+`,
+ [DomainFragment],
+);
+
+function formatDomain(data: FragmentOf): string {
+ // type-safe access to fragment data!
+ const domain = readFragment(DomainFragment, data);
+ const name = domain.name ? beautifyInterpretedName(domain.name) : "";
+ const owner = domain.owner?.address ?? "0x0";
+ return `${name} (${domain.__typename}) — Owner ${owner}`;
+}
+
+async function main() {
+ const name = asInterpretedName("eth");
+
+ const result = await client.omnigraph.query({
+ query: HelloWorldQuery,
+ variables: { name },
+ });
+
+ if (result.errors) throw new Error(JSON.stringify(result.errors));
+ if (!result.data?.domain) throw new Error(`Domain '${name}' not found`);
+
+ const { domain } = result.data;
+ const totalCount = domain.subdomains?.totalCount ?? 0;
+
+ console.log(formatDomain(domain));
+ console.log(`\nSubdomains (showing 20 of ${totalCount}):`);
+ console.table(
+ (domain.subdomains?.edges ?? []).map(({ node }) => {
+ const sub = readFragment(DomainFragment, node);
+ return {
+ version: sub.__typename,
+ owner: sub.owner?.address ?? "0x0",
+ name: sub.name ? beautifyInterpretedName(sub.name) : "",
+ };
+ }),
+ );
+ console.log(
+ "(if the table's rightmost edge seems malformatted it's because console.table doesn't handle unicode well)",
+ );
+}
+```
+
+`FragmentOf` is the opaque type for any selection that includes `...DomainFragment` — `formatDomain` accepts any of them. `readFragment(DomainFragment, data)` unwraps that opaque type to the typed fields you declared.
+
+## 8. Run it
+
+Point at a hosted ENSNode and go:
+
+```sh
+ENSNODE_URL=https://api.v2-sepolia.ensnode.io npm start
+```
+
+You should see the `eth` Domain, followed by its first 20 subdomains and the total subdomain count.
+
+## Where to go next
+
+- Move queries into their own modules. `enssdk/omnigraph` re-exports `FragmentOf`, `ResultOf`, and `VariablesOf` from `gql.tada` for typing arguments and return values.
+- Browse other queries: account-owned domains, events, registrar permissions, full-text search. See [Omnigraph API examples](/docs/integrate/integration-options/raw-graphql).
+- Building a React app? Use [`enskit`](/docs/integrate/integration-options/enskit) — same `graphql(...)` helper, with `useOmnigraphQuery` and a graphcache.
+
+
diff --git a/examples/enskit-react-example/package.json b/examples/enskit-react-example/package.json
index 78b609ffa0..08c1bc1c97 100644
--- a/examples/enskit-react-example/package.json
+++ b/examples/enskit-react-example/package.json
@@ -8,7 +8,8 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
- "typecheck": "tsgo --noEmit"
+ "typecheck": "tsgo --noEmit",
+ "generate:gqlschema": "gql.tada generate-output"
},
"dependencies": {
"enskit": "workspace:*",
@@ -21,6 +22,7 @@
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "^4.5.2",
+ "gql.tada": "^1.8.10",
"typescript": "catalog:",
"vite": "catalog:"
}
diff --git a/examples/enskit-react-example/src/App.tsx b/examples/enskit-react-example/src/App.tsx
index 12f59d3ec1..2112d1c04b 100644
--- a/examples/enskit-react-example/src/App.tsx
+++ b/examples/enskit-react-example/src/App.tsx
@@ -11,13 +11,7 @@ import { SearchView } from "./SearchView";
const EXAMPLE_ACCOUNT_ADDRESS = "0x2f8e8b1126e75fde0b7f731e7cb5847eba2d2574";
-/**
- * Gets the ENSNODE_URL from the environment, defaulting to the NameHash-hosted Sepolia-V2 Namespace
- * at https://api.v2-sepolia.ensnode.io
- *
- * To override, provide ENSNODE_URL in your environment like:
- * ENSNODE_URL=https://api.alpha.ensnode.io pnpm dev
- */
+// you may use a NameHash Hosted ENSNode, learn more at https://ensnode.io/docs/integrate/hosted-instances
const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL ?? "https://api.v2-sepolia.ensnode.io";
console.log(`Connecting to ENSNode at ${ENSNODE_URL}`);
diff --git a/examples/enssdk-example/LICENSE b/examples/enssdk-example/LICENSE
new file mode 100644
index 0000000000..24d66814d7
--- /dev/null
+++ b/examples/enssdk-example/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 NameHash
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/examples/enssdk-example/README.md b/examples/enssdk-example/README.md
new file mode 100644
index 0000000000..a1f82872da
--- /dev/null
+++ b/examples/enssdk-example/README.md
@@ -0,0 +1,25 @@
+# enssdk Example
+
+A minimal TypeScript script demonstrating how to use `enssdk` to query the ENS Omnigraph API.
+
+Companion to the [enssdk integration guide](https://ensnode.io/docs/integrate/integration-options/enssdk).
+
+## Usage (with NameHash Hosted Instance)
+
+```sh
+# from the ENSNode monorepo root
+pnpm install
+
+ENSNODE_URL=https://api.v2-sepolia.ensnode.io pnpm -F enssdk-example start
+```
+
+## Usage (with Local ENSNode)
+
+First, follow the [ENSNode Contributing Documentation](https://ensnode.io/docs/contributing) to get ENSNode running on your local machine. At the end of this, ENSApi should be available on port `:4334`.
+
+```sh
+# from the ENSNode monorepo root
+pnpm install
+
+ENSNODE_URL=http://localhost:4334 pnpm -F enssdk-example start
+```
diff --git a/examples/enssdk-example/package.json b/examples/enssdk-example/package.json
new file mode 100644
index 0000000000..72c39de7a7
--- /dev/null
+++ b/examples/enssdk-example/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@ensnode/enssdk-example",
+ "private": true,
+ "version": "0.0.1",
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "start": "tsx src/index.ts",
+ "typecheck": "tsgo --noEmit",
+ "generate:gqlschema": "gql.tada generate-output"
+ },
+ "dependencies": {
+ "enssdk": "workspace:*"
+ },
+ "devDependencies": {
+ "@types/node": "catalog:",
+ "gql.tada": "^1.8.10",
+ "tsx": "^4.7.1",
+ "typescript": "catalog:"
+ }
+}
diff --git a/examples/enssdk-example/src/index.ts b/examples/enssdk-example/src/index.ts
new file mode 100644
index 0000000000..2e1984ab26
--- /dev/null
+++ b/examples/enssdk-example/src/index.ts
@@ -0,0 +1,80 @@
+import { asInterpretedName, beautifyInterpretedName } from "enssdk";
+import { createEnsNodeClient } from "enssdk/core";
+import { type FragmentOf, graphql, omnigraph, readFragment } from "enssdk/omnigraph";
+
+// you may use a NameHash Hosted ENSNode, learn more at https://ensnode.io/docs/integrate/hosted-instances
+// biome-ignore lint/style/noNonNullAssertion: invariant
+const ENSNODE_URL = process.env.ENSNODE_URL!;
+
+// create and extend an EnsNodeClient with Omnigraph support
+const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph);
+
+const DomainFragment = graphql(`
+ fragment DomainFragment on Domain {
+ __typename
+ name
+ owner { address }
+ }
+`);
+
+const HelloWorldQuery = graphql(
+ `
+ query HelloWorld($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ ...DomainFragment
+ subdomains(first: 20) {
+ totalCount
+ edges { node { ...DomainFragment } }
+ }
+ }
+ }
+`,
+ [DomainFragment],
+);
+
+function formatDomain(data: FragmentOf): string {
+ // type-safe access to fragment data!
+ const domain = readFragment(DomainFragment, data);
+ const name = domain.name ? beautifyInterpretedName(domain.name) : "";
+ const owner = domain.owner?.address ?? "0x0";
+ return `${name} (${domain.__typename}) — Owner ${owner}`;
+}
+
+async function main() {
+ const name = asInterpretedName("eth");
+
+ const start = performance.now();
+ const result = await client.omnigraph.query({
+ query: HelloWorldQuery,
+ variables: { name },
+ });
+ const elapsed = performance.now() - start;
+
+ if (result.errors) throw new Error(JSON.stringify(result.errors));
+ if (!result.data?.domain) throw new Error(`Domain '${name}' not found`);
+
+ const { domain } = result.data;
+ const totalCount = domain.subdomains?.totalCount ?? 0;
+
+ console.log(`Query took ${(elapsed / 1000).toFixed(2)}s`);
+ console.log(formatDomain(domain));
+ console.log(`\nSubdomains (showing 20 of ${totalCount}):`);
+ console.table(
+ (domain.subdomains?.edges ?? []).map(({ node }) => {
+ const sub = readFragment(DomainFragment, node);
+ return {
+ version: sub.__typename,
+ owner: sub.owner?.address ?? "0x0",
+ name: sub.name ? beautifyInterpretedName(sub.name) : "",
+ };
+ }),
+ );
+ console.log(
+ "(if the table's rightmost edge seems malformatted it's because console.table doesn't handle unicode well)",
+ );
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/examples/enssdk-example/tsconfig.json b/examples/enssdk-example/tsconfig.json
new file mode 100644
index 0000000000..cdccbda879
--- /dev/null
+++ b/examples/enssdk-example/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "lib": ["ESNext"],
+ "plugins": [
+ // here we enable the gql.tada typescript plugin
+ {
+ "name": "gql.tada/ts-plugin",
+
+ // because this example is in the ENSNode monorepo, it imports from src
+ "schema": "../../packages/enssdk/src/omnigraph/generated/schema.graphql",
+ // but in a normal app you'd import it from the package like so:
+ // "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql",
+
+ "tadaOutputLocation": "./src/generated/graphql-env.d.ts"
+ }
+ ]
+ },
+ "include": ["src"]
+}
diff --git a/package.json b/package.json
index 73382a81eb..52f9f910e6 100644
--- a/package.json
+++ b/package.json
@@ -26,8 +26,9 @@
"docker:build:ensrainbow": "docker build -f apps/ensrainbow/Dockerfile -t ghcr.io/namehash/ensnode/ensrainbow:latest .",
"docker:build:ensapi": "docker build -f apps/ensapi/Dockerfile -t ghcr.io/namehash/ensnode/ensapi:latest .",
"otel-desktop-viewer": "docker run -p 8000:8000 -p 4317:4317 -p 4318:4318 davetron5000/otel-desktop-viewer:alpine-3",
- "generate:openapi": "pnpm -r --if-present generate:openapi",
- "generate:gqlschema": "pnpm -F ensapi generate:gqlschema && pnpm -F enssdk generate:gqlschema"
+ "generate": "pnpm run -w \"/^generate:.*/\"",
+ "generate:openapi": "pnpm -r generate:openapi",
+ "generate:gqlschema": "pnpm -r generate:gqlschema"
},
"devDependencies": {
"@biomejs/biome": "^2.3.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a6972ee1b6..599c905a7b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -839,6 +839,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^4.5.2
version: 4.7.0(vite@7.3.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3))
+ gql.tada:
+ specifier: ^1.8.10
+ version: 1.9.1(graphql@16.11.0)(typescript@5.9.3)
typescript:
specifier: 'catalog:'
version: 5.9.3
@@ -846,6 +849,25 @@ importers:
specifier: 'catalog:'
version: 7.3.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3)
+ examples/enssdk-example:
+ dependencies:
+ enssdk:
+ specifier: workspace:*
+ version: link:../../packages/enssdk
+ devDependencies:
+ '@types/node':
+ specifier: 'catalog:'
+ version: 24.10.9
+ gql.tada:
+ specifier: ^1.8.10
+ version: 1.9.1(graphql@16.11.0)(typescript@5.9.3)
+ tsx:
+ specifier: ^4.7.1
+ version: 4.21.0
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
packages/datasources:
dependencies:
'@ponder/utils':
@@ -20142,7 +20164,7 @@ snapshots:
tsx@4.21.0:
dependencies:
- esbuild: 0.27.2
+ esbuild: 0.27.4
get-tsconfig: 4.13.0
optionalDependencies:
fsevents: 2.3.3
From 92ff01b3eb8fa68efc29067b9e7576a7287ec06a Mon Sep 17 00:00:00 2001
From: shrugs
Date: Wed, 13 May 2026 14:26:13 -0500
Subject: [PATCH 04/17] fix: silence logOperations in CI
---
apps/ensapi/src/lib/resolution/operations.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/apps/ensapi/src/lib/resolution/operations.ts b/apps/ensapi/src/lib/resolution/operations.ts
index 8780edfd00..88e3f1dc27 100644
--- a/apps/ensapi/src/lib/resolution/operations.ts
+++ b/apps/ensapi/src/lib/resolution/operations.ts
@@ -110,6 +110,9 @@ export function logOperations(
operations: Operation[],
logger: { debug: (obj: unknown) => void },
): void {
+ // silence this noisy log in CI
+ if (process.env.CI) return;
+
if (process.env.NODE_ENV !== "production") {
console.table(tablifyOperations(operations));
} else {
From f3cb135b34144621679c53f0a7fa85633edb48fd Mon Sep 17 00:00:00 2001
From: shrugs
Date: Wed, 13 May 2026 14:26:25 -0500
Subject: [PATCH 05/17] fix: hook up enssdk-example to integration tests
---
examples/enssdk-example/package.json | 3 ++-
.../src/index.integration.test.ts | 23 +++++++++++++++++++
.../vitest.integration.config.ts | 8 +++++++
pnpm-lock.yaml | 13 ++++++++++-
4 files changed, 45 insertions(+), 2 deletions(-)
create mode 100644 examples/enssdk-example/src/index.integration.test.ts
create mode 100644 examples/enssdk-example/vitest.integration.config.ts
diff --git a/examples/enssdk-example/package.json b/examples/enssdk-example/package.json
index 72c39de7a7..d9837b4f30 100644
--- a/examples/enssdk-example/package.json
+++ b/examples/enssdk-example/package.json
@@ -16,6 +16,7 @@
"@types/node": "catalog:",
"gql.tada": "^1.8.10",
"tsx": "^4.7.1",
- "typescript": "catalog:"
+ "typescript": "catalog:",
+ "vitest": "catalog:"
}
}
diff --git a/examples/enssdk-example/src/index.integration.test.ts b/examples/enssdk-example/src/index.integration.test.ts
new file mode 100644
index 0000000000..fb0ead4710
--- /dev/null
+++ b/examples/enssdk-example/src/index.integration.test.ts
@@ -0,0 +1,23 @@
+import { spawnSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+import { describe, expect, it } from "vitest";
+
+const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
+
+describe("enssdk-example", () => {
+ it("smoke test: completes against the configured ENSNode with exit code 0", () => {
+ const result = spawnSync("pnpm", ["start"], {
+ cwd: PACKAGE_ROOT,
+ env: process.env,
+ encoding: "utf8",
+ timeout: 60_000,
+ });
+
+ // log into vitest's stdout capture so --silent passed-only hides it on success
+ if (result.stdout) console.log(result.stdout);
+ if (result.stderr) console.error(result.stderr);
+
+ expect(result.status).toBe(0);
+ });
+});
diff --git a/examples/enssdk-example/vitest.integration.config.ts b/examples/enssdk-example/vitest.integration.config.ts
new file mode 100644
index 0000000000..20213b523d
--- /dev/null
+++ b/examples/enssdk-example/vitest.integration.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ environment: "node",
+ include: ["**/*.integration.test.ts"],
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 599c905a7b..b3c6548223 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -867,6 +867,9 @@ importers:
typescript:
specifier: 'catalog:'
version: 5.9.3
+ vitest:
+ specifier: 'catalog:'
+ version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3)
packages/datasources:
dependencies:
@@ -14963,6 +14966,14 @@ snapshots:
chai: 6.2.0
tinyrainbow: 3.0.3
+ '@vitest/mocker@4.0.5(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3))':
+ dependencies:
+ '@vitest/spy': 4.0.5
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)
+
'@vitest/mocker@4.0.5(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
'@vitest/spy': 4.0.5
@@ -20493,7 +20504,7 @@ snapshots:
vitest@4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3):
dependencies:
'@vitest/expect': 4.0.5
- '@vitest/mocker': 4.0.5(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3))
+ '@vitest/mocker': 4.0.5(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3))
'@vitest/pretty-format': 4.0.5
'@vitest/runner': 4.0.5
'@vitest/snapshot': 4.0.5
From f05c04c00e852b44ff7983085e4a476ebe571aa6 Mon Sep 17 00:00:00 2001
From: shrugs
Date: Wed, 13 May 2026 14:38:07 -0500
Subject: [PATCH 06/17] chckpoint: enssdk walkthrough
---
.../integrate/integration-options/enskit.mdx | 18 ++++++-
.../integrate/integration-options/enssdk.mdx | 54 +++++++++----------
examples/enskit-react-example/src/App.tsx | 3 +-
examples/enssdk-example/src/index.ts | 19 ++-----
4 files changed, 48 insertions(+), 46 deletions(-)
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
index 3f8d148081..055fed8781 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
@@ -85,7 +85,8 @@ import { StrictMode } from "react";
import { DomainView } from "./DomainView";
-// you may use a NameHash Hosted ENSNode, learn more at https://ensnode.io/docs/integrate/hosted-instances
+// you may use a NameHash Hosted ENSNode instance
+// learn more at https://ensnode.io/docs/integrate/hosted-instances
const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL!
// create and extend an EnsNodeClient with Omnigraph support
@@ -391,7 +392,8 @@ Open the printed URL and you should see the `eth` Domain, its owner, and the fir
## Where to go next
- Swap the hardcoded `"eth"` for a name from props or a router — see [`EnsureInterpretedName`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in the example app for safe handling of user-provided names.
-- Browse other queries: account-owned domains, events, registrar permissions, full-text search. See [Omnigraph API examples](/docs/integrate/integration-options/raw-graphql).
+- See the [Omnigraph Cookbook](/docs/integrate/omnigraph/cookbook) for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more.
+- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query.
- Need data outside React? Use [`enssdk`](/docs/integrate/integration-options/enssdk) directly with the same `graphql(...)` helper.
+
+
+
+
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
index 1e0c912731..5a64d1dfd8 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
@@ -51,7 +51,7 @@ Always pin an **exact** version (no `^` or `~`) of `enssdk`. The Omnigraph Graph
Create `tsconfig.json`:
-```json title="tsconfig.json"
+```json title="tsconfig.json" ins={10-15}
{
"compilerOptions": {
"target": "ES2022",
@@ -101,7 +101,8 @@ The `EnsNodeClient` is the entry point. Extend it with the `omnigraph` module to
import { createEnsNodeClient } from "enssdk/core";
import { omnigraph } from "enssdk/omnigraph";
-// you may use a NameHash Hosted ENSNode, learn more at https://ensnode.io/docs/integrate/hosted-instances
+// you may use a NameHash Hosted ENSNode instance
+// learn more at https://ensnode.io/docs/integrate/hosted-instances
const ENSNODE_URL = process.env.ENSNODE_URL!;
// create and extend an EnsNodeClient with Omnigraph support
@@ -169,7 +170,7 @@ A few things to notice:
Expand the query to also fetch the Domain's subdomains. `subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm) — pass `first: 20` to cap the page, and select `totalCount` to learn how many subdomains exist in total.
-```ts title="src/index.ts" ins={7-12,34-40}
+```ts title="src/index.ts" ins={7-12,34-38}
const HelloWorldQuery = graphql(`
query HelloWorld($name: InterpretedName!) {
domain(by: { name: $name }) {
@@ -204,16 +205,14 @@ async function main() {
console.log(`Owner: ${domain.owner?.address ?? "0x0"}`);
console.log(`\nSubdomains (showing 20 of ${domain.subdomains?.totalCount ?? 0}):`);
- console.table(
- (domain.subdomains?.edges ?? []).map(({ node }) => ({
- name: node.name ? beautifyInterpretedName(node.name) : "",
- owner: node.owner?.address ?? "0x0",
- })),
- );
+ for (const { node } of domain.subdomains?.edges ?? []) {
+ const subName = node.name ? beautifyInterpretedName(node.name) : "";
+ console.log(` - ${subName} — Owner ${node.owner?.address ?? "0x0"}`);
+ }
}
```
-`console.table` renders the rows as a nicely-aligned table in your terminal.
+Notice we're now writing the same name/owner rendering twice — once for the parent Domain and once inside the subdomain loop. We'll fix that next.
To page beyond the first 20, the connection also exposes `pageInfo { hasNextPage endCursor }` and accepts an `after: String` cursor — see [Relay's connection spec](https://relay.dev/graphql/connections.htm).
@@ -221,12 +220,13 @@ To page beyond the first 20, the connection also exposes `pageInfo { hasNextPage
Notice we're selecting the same fields (`name`, `owner { address }`) on the parent Domain _and_ on each subdomain, and rendering them the same way. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed function that formats a Domain.
-```ts title="src/index.ts" ins={3,10-16,18,22,25,30,33-39,53,55,58-65,67-69}
+```ts title="src/index.ts" ins={3,11-17,23,26,31,34-40,56,59}
import { asInterpretedName, beautifyInterpretedName } from "enssdk";
import { createEnsNodeClient } from "enssdk/core";
import { type FragmentOf, graphql, omnigraph, readFragment } from "enssdk/omnigraph";
-// you may use a NameHash Hosted ENSNode, learn more at https://ensnode.io/docs/integrate/hosted-instances
+// you may use a NameHash Hosted ENSNode instance
+// learn more at https://ensnode.io/docs/integrate/hosted-instances
const ENSNODE_URL = process.env.ENSNODE_URL!;
const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph);
@@ -278,38 +278,28 @@ async function main() {
console.log(formatDomain(domain));
console.log(`\nSubdomains (showing 20 of ${totalCount}):`);
- console.table(
- (domain.subdomains?.edges ?? []).map(({ node }) => {
- const sub = readFragment(DomainFragment, node);
- return {
- version: sub.__typename,
- owner: sub.owner?.address ?? "0x0",
- name: sub.name ? beautifyInterpretedName(sub.name) : "",
- };
- }),
- );
- console.log(
- "(if the table's rightmost edge seems malformatted it's because console.table doesn't handle unicode well)",
- );
+ for (const { node } of domain.subdomains?.edges ?? []) {
+ console.log(` - ${formatDomain(node)}`);
+ }
}
```
-`FragmentOf` is the opaque type for any selection that includes `...DomainFragment` — `formatDomain` accepts any of them. `readFragment(DomainFragment, data)` unwraps that opaque type to the typed fields you declared.
+`FragmentOf` is the opaque type for any selection that includes `...DomainFragment` — `formatDomain` accepts any of them, including each `node` in the subdomain edges. `readFragment(DomainFragment, data)` unwraps that opaque type to the typed fields you declared.
## 8. Run it
Point at a hosted ENSNode and go:
```sh
-ENSNODE_URL=https://api.v2-sepolia.ensnode.io npm start
+ENSNODE_URL=https://api.alpha.green.ensnode.io npm start
```
You should see the `eth` Domain, followed by its first 20 subdomains and the total subdomain count.
## Where to go next
-- Move queries into their own modules. `enssdk/omnigraph` re-exports `FragmentOf`, `ResultOf`, and `VariablesOf` from `gql.tada` for typing arguments and return values.
-- Browse other queries: account-owned domains, events, registrar permissions, full-text search. See [Omnigraph API examples](/docs/integrate/integration-options/raw-graphql).
+- See the [Omnigraph Cookbook](/docs/integrate/omnigraph/cookbook) for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more.
+- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query.
- Building a React app? Use [`enskit`](/docs/integrate/integration-options/enskit) — same `graphql(...)` helper, with `useOmnigraphQuery` and a graphcache.
+
+
diff --git a/examples/enskit-react-example/src/App.tsx b/examples/enskit-react-example/src/App.tsx
index 2112d1c04b..644832aed4 100644
--- a/examples/enskit-react-example/src/App.tsx
+++ b/examples/enskit-react-example/src/App.tsx
@@ -11,7 +11,8 @@ import { SearchView } from "./SearchView";
const EXAMPLE_ACCOUNT_ADDRESS = "0x2f8e8b1126e75fde0b7f731e7cb5847eba2d2574";
-// you may use a NameHash Hosted ENSNode, learn more at https://ensnode.io/docs/integrate/hosted-instances
+// you may use a NameHash Hosted ENSNode instance
+// learn more at https://ensnode.io/docs/integrate/hosted-instances
const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL ?? "https://api.v2-sepolia.ensnode.io";
console.log(`Connecting to ENSNode at ${ENSNODE_URL}`);
diff --git a/examples/enssdk-example/src/index.ts b/examples/enssdk-example/src/index.ts
index 2e1984ab26..6c882c485f 100644
--- a/examples/enssdk-example/src/index.ts
+++ b/examples/enssdk-example/src/index.ts
@@ -2,7 +2,8 @@ import { asInterpretedName, beautifyInterpretedName } from "enssdk";
import { createEnsNodeClient } from "enssdk/core";
import { type FragmentOf, graphql, omnigraph, readFragment } from "enssdk/omnigraph";
-// you may use a NameHash Hosted ENSNode, learn more at https://ensnode.io/docs/integrate/hosted-instances
+// you may use a NameHash Hosted ENSNode instance
+// learn more at https://ensnode.io/docs/integrate/hosted-instances
// biome-ignore lint/style/noNonNullAssertion: invariant
const ENSNODE_URL = process.env.ENSNODE_URL!;
@@ -59,19 +60,9 @@ async function main() {
console.log(`Query took ${(elapsed / 1000).toFixed(2)}s`);
console.log(formatDomain(domain));
console.log(`\nSubdomains (showing 20 of ${totalCount}):`);
- console.table(
- (domain.subdomains?.edges ?? []).map(({ node }) => {
- const sub = readFragment(DomainFragment, node);
- return {
- version: sub.__typename,
- owner: sub.owner?.address ?? "0x0",
- name: sub.name ? beautifyInterpretedName(sub.name) : "",
- };
- }),
- );
- console.log(
- "(if the table's rightmost edge seems malformatted it's because console.table doesn't handle unicode well)",
- );
+ for (const { node } of domain.subdomains?.edges ?? []) {
+ console.log(` - ${formatDomain(node)}`);
+ }
}
main().catch((err) => {
From 08528911ac10899cbcb8ba9d87ebabb7ea373733 Mon Sep 17 00:00:00 2001
From: shrugs
Date: Wed, 13 May 2026 14:46:08 -0500
Subject: [PATCH 07/17] checkpoint: initial raw graphql example and walkthrough
---
.../integration-options/raw-graphql.mdx | 189 +++++++++++++++++-
examples/enssdk-example/package.json | 3 +-
examples/omnigraph-graphql-example/LICENSE | 21 ++
examples/omnigraph-graphql-example/README.md | 27 +++
.../omnigraph-graphql-example/package.json | 16 ++
.../omnigraph-graphql-example/src/index.ts | 77 +++++++
.../omnigraph-graphql-example/tsconfig.json | 12 ++
packages/integration-test-env/package.json | 3 +
.../src/enssdk-example.integration.test.ts | 11 +-
...igraph-graphql-example.integration.test.ts | 30 +++
.../vitest.integration.config.ts | 2 +-
pnpm-lock.yaml | 29 +--
12 files changed, 400 insertions(+), 20 deletions(-)
create mode 100644 examples/omnigraph-graphql-example/LICENSE
create mode 100644 examples/omnigraph-graphql-example/README.md
create mode 100644 examples/omnigraph-graphql-example/package.json
create mode 100644 examples/omnigraph-graphql-example/src/index.ts
create mode 100644 examples/omnigraph-graphql-example/tsconfig.json
rename examples/enssdk-example/src/index.integration.test.ts => packages/integration-test-env/src/enssdk-example.integration.test.ts (81%)
create mode 100644 packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts
rename {examples/enssdk-example => packages/integration-test-env}/vitest.integration.config.ts (72%)
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/raw-graphql.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/raw-graphql.mdx
index 97953a9e7d..0b387627fa 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/raw-graphql.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/raw-graphql.mdx
@@ -3,8 +3,191 @@ title: Raw GraphQL
description: Query the ENS Omnigraph API directly via HTTP from any language.
---
-:::caution[Coming Soon]
-We're actively working on this page right now. Check back by May 18th for full content!
+import { LinkCard } from '@astrojs/starlight/components';
+
+The Omnigraph is **just a GraphQL API** following the [Relay specification](https://relay.dev/graphql/connections.htm). There's no proprietary protocol or transport — any GraphQL client in any language works, from `curl` and `fetch` to [`urql`](https://nearform.com/open-source/urql/), [Apollo](https://www.apollographql.com/), [`graphql-request`](https://github.com/jasonkuhrt/graphql-request), and beyond.
+
+This guide walks you through the minimum: a single `fetch` call against `/api/omnigraph` — the same flow as our [omnigraph-graphql-example](https://github.com/namehash/ensnode/tree/main/examples/omnigraph-graphql-example).
+
+If you want end-to-end typed queries (via [`gql.tada`](https://gql-tada.0no.co/)) with editor autocomplete and a built-in client, use [`enssdk`](/docs/integrate/integration-options/enssdk) instead — but if you need to integrate from a language without first-class GraphQL tooling, or you're already in a stack with its own GraphQL client, this is the path.
+
+:::tip[No backend required]
+You don't need to run your own ENSNode to follow this guide — point at any NameHash-hosted instance to get started.
:::
-This page will show how to query the ENS Omnigraph API directly via HTTP from any language — using the /api/omnigraph endpoint with standard GraphQL POST requests.
+
+
+## 1. The endpoint
+
+The Omnigraph lives at:
+
+```
+POST {ENSNODE_URL}/api/omnigraph
+Content-Type: application/json
+
+{ "query": "...", "variables": { ... } }
+```
+
+It returns `{ "data": ..., "errors": [...] }` per the standard GraphQL response shape. Nothing more.
+
+A minimum-viable hello world over `curl`:
+
+```sh
+curl -sS -X POST \
+ -H 'Content-Type: application/json' \
+ -d '{"query":"{ domain(by: { name: \"eth\" }) { name owner { address } } }"}' \
+ https://api.v2-sepolia.ensnode.io/api/omnigraph
+```
+
+The rest of this guide builds the same thing in TypeScript using `fetch`, so you have something to extend.
+
+## 2. Scaffold a TypeScript project
+
+If you already have one, skip ahead to [Write the query](#3-write-the-query).
+
+```sh
+mkdir my-ens-script && cd my-ens-script
+npm init -y
+mkdir src
+```
+
+Install [`tsx`](https://tsx.is/) so you can run TypeScript directly:
+
+```sh
+npm install -D tsx typescript @types/node
+```
+
+Add a `start` script to `package.json`:
+
+```json title="package.json" ins={3-5}
+{
+ "type": "module",
+ "scripts": {
+ "start": "tsx src/index.ts"
+ }
+}
+```
+
+## 3. Write the query
+
+Create `src/index.ts`. The whole script is a single `fetch` against `/api/omnigraph`.
+
+```ts title="src/index.ts"
+// you may use a NameHash Hosted ENSNode instance
+// learn more at https://ensnode.io/docs/integrate/hosted-instances
+const ENSNODE_URL = process.env.ENSNODE_URL!;
+
+const HELLO_WORLD_QUERY = /* GraphQL */ `
+ query HelloWorld($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ __typename
+ name
+ owner { address }
+ subdomains(first: 20) {
+ totalCount
+ edges { node { __typename name owner { address } } }
+ }
+ }
+ }
+`;
+
+interface Domain {
+ __typename: "ENSv1Domain" | "ENSv2Domain";
+ name: string | null;
+ owner: { address: string } | null;
+}
+
+interface QueryResult {
+ data?: {
+ domain: (Domain & {
+ subdomains: {
+ totalCount: number;
+ edges: { node: Domain }[];
+ } | null;
+ }) | null;
+ } | null;
+ errors?: { message: string }[];
+}
+
+function formatDomain(domain: Domain): string {
+ const name = domain.name ?? "";
+ const owner = domain.owner?.address ?? "0x0";
+ return `${name} (${domain.__typename}) — Owner ${owner}`;
+}
+
+async function main() {
+ const response = await fetch(new URL("/api/omnigraph", ENSNODE_URL), {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ query: HELLO_WORLD_QUERY,
+ variables: { name: "eth" },
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
+ }
+
+ const { data, errors } = (await response.json()) as QueryResult;
+
+ if (errors) throw new Error(JSON.stringify(errors));
+ if (!data?.domain) throw new Error("Domain 'eth' not found");
+
+ const { domain } = data;
+ const totalCount = domain.subdomains?.totalCount ?? 0;
+
+ console.log(formatDomain(domain));
+ console.log(`\nSubdomains (showing 20 of ${totalCount}):`);
+ for (const { node } of domain.subdomains?.edges ?? []) {
+ console.log(` - ${formatDomain(node)}`);
+ }
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
+```
+
+A few things to notice:
+
+- **`InterpretedName` is a scalar.** From the wire's perspective it's just a string — the server validates the format. Pass `"eth"` as a plain string in `variables`.
+- **`subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm).** Cursor through with `first`, `after`, `pageInfo { hasNextPage endCursor }` — same shape as any Relay-style API.
+- **Hand-written types.** We're maintaining `interface Domain` and `interface QueryResult` ourselves here. If you want these generated and kept in sync with your queries automatically, use [`enssdk`](/docs/integrate/integration-options/enssdk).
+
+## 4. Run it
+
+```sh
+ENSNODE_URL=https://api.v2-sepolia.ensnode.io npm start
+```
+
+You should see the `eth` Domain, its owner, and the first 20 of its subdomains.
+
+## Where to go next
+
+- Want typed queries with editor autocomplete and a real GraphQL client? Use [`enssdk`](/docs/integrate/integration-options/enssdk) — same API, with `gql.tada` types and an `EnsNodeClient`.
+- Building a React app? Use [`enskit`](/docs/integrate/integration-options/enskit) — same `graphql(...)` helper plus `useOmnigraphQuery` and a graphcache.
+- See the [Omnigraph Cookbook](/docs/integrate/omnigraph/cookbook) for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more.
+- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query.
+
+
+
+
+
+
diff --git a/examples/enssdk-example/package.json b/examples/enssdk-example/package.json
index d9837b4f30..72c39de7a7 100644
--- a/examples/enssdk-example/package.json
+++ b/examples/enssdk-example/package.json
@@ -16,7 +16,6 @@
"@types/node": "catalog:",
"gql.tada": "^1.8.10",
"tsx": "^4.7.1",
- "typescript": "catalog:",
- "vitest": "catalog:"
+ "typescript": "catalog:"
}
}
diff --git a/examples/omnigraph-graphql-example/LICENSE b/examples/omnigraph-graphql-example/LICENSE
new file mode 100644
index 0000000000..24d66814d7
--- /dev/null
+++ b/examples/omnigraph-graphql-example/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 NameHash
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/examples/omnigraph-graphql-example/README.md b/examples/omnigraph-graphql-example/README.md
new file mode 100644
index 0000000000..adac84db94
--- /dev/null
+++ b/examples/omnigraph-graphql-example/README.md
@@ -0,0 +1,27 @@
+# Omnigraph GraphQL Example
+
+A minimal TypeScript script demonstrating how to query the ENS Omnigraph API directly over HTTP — no SDK, just `fetch`.
+
+The Omnigraph is a standard GraphQL API following the Relay spec, so any GraphQL client (or plain `fetch`) works.
+
+Companion to the [Raw GraphQL integration guide](https://ensnode.io/docs/integrate/integration-options/raw-graphql).
+
+## Usage (with NameHash Hosted Instance)
+
+```sh
+# from the ENSNode monorepo root
+pnpm install
+
+ENSNODE_URL=https://api.v2-sepolia.ensnode.io pnpm -F omnigraph-graphql-example start
+```
+
+## Usage (with Local ENSNode)
+
+First, follow the [ENSNode Contributing Documentation](https://ensnode.io/docs/contributing) to get ENSNode running on your local machine. At the end of this, ENSApi should be available on port `:4334`.
+
+```sh
+# from the ENSNode monorepo root
+pnpm install
+
+ENSNODE_URL=http://localhost:4334 pnpm -F omnigraph-graphql-example start
+```
diff --git a/examples/omnigraph-graphql-example/package.json b/examples/omnigraph-graphql-example/package.json
new file mode 100644
index 0000000000..56bd2e594d
--- /dev/null
+++ b/examples/omnigraph-graphql-example/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@ensnode/omnigraph-graphql-example",
+ "private": true,
+ "version": "0.0.1",
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "start": "tsx src/index.ts",
+ "typecheck": "tsgo --noEmit"
+ },
+ "devDependencies": {
+ "@types/node": "catalog:",
+ "tsx": "^4.7.1",
+ "typescript": "catalog:"
+ }
+}
diff --git a/examples/omnigraph-graphql-example/src/index.ts b/examples/omnigraph-graphql-example/src/index.ts
new file mode 100644
index 0000000000..2844643e7e
--- /dev/null
+++ b/examples/omnigraph-graphql-example/src/index.ts
@@ -0,0 +1,77 @@
+// you may use a NameHash Hosted ENSNode instance
+// learn more at https://ensnode.io/docs/integrate/hosted-instances
+const ENSNODE_URL = process.env.ENSNODE_URL!;
+
+// The Omnigraph is a standard GraphQL API following the Relay spec.
+// You can use any GraphQL client — here we just use `fetch`.
+const HELLO_WORLD_QUERY = /* GraphQL */ `
+ query HelloWorld($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ __typename
+ name
+ owner { address }
+ subdomains(first: 20) {
+ totalCount
+ edges { node { __typename name owner { address } } }
+ }
+ }
+ }
+`;
+
+interface Domain {
+ __typename: "ENSv1Domain" | "ENSv2Domain";
+ name: string | null;
+ owner: { address: string } | null;
+}
+
+interface QueryResult {
+ data?: {
+ domain: (Domain & {
+ subdomains: {
+ totalCount: number;
+ edges: { node: Domain }[];
+ } | null;
+ }) | null;
+ } | null;
+ errors?: { message: string }[];
+}
+
+function formatDomain(domain: Domain): string {
+ const name = domain.name ?? "";
+ const owner = domain.owner?.address ?? "0x0";
+ return `${name} (${domain.__typename}) — Owner ${owner}`;
+}
+
+async function main() {
+ const response = await fetch(new URL("/api/omnigraph", ENSNODE_URL), {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ query: HELLO_WORLD_QUERY,
+ variables: { name: "eth" },
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
+ }
+
+ const { data, errors } = (await response.json()) as QueryResult;
+
+ if (errors) throw new Error(JSON.stringify(errors));
+ if (!data?.domain) throw new Error("Domain 'eth' not found");
+
+ const { domain } = data;
+ const totalCount = domain.subdomains?.totalCount ?? 0;
+
+ console.log(formatDomain(domain));
+ console.log(`\nSubdomains (showing 20 of ${totalCount}):`);
+ for (const { node } of domain.subdomains?.edges ?? []) {
+ console.log(` - ${formatDomain(node)}`);
+ }
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/examples/omnigraph-graphql-example/tsconfig.json b/examples/omnigraph-graphql-example/tsconfig.json
new file mode 100644
index 0000000000..96b780876d
--- /dev/null
+++ b/examples/omnigraph-graphql-example/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "lib": ["ESNext"]
+ },
+ "include": ["src"]
+}
diff --git a/packages/integration-test-env/package.json b/packages/integration-test-env/package.json
index c574af820c..4f29a0f295 100644
--- a/packages/integration-test-env/package.json
+++ b/packages/integration-test-env/package.json
@@ -18,5 +18,8 @@
"testcontainers": "^11.14.0",
"tsx": "^4.7.1",
"viem": "catalog:"
+ },
+ "devDependencies": {
+ "vitest": "catalog:"
}
}
diff --git a/examples/enssdk-example/src/index.integration.test.ts b/packages/integration-test-env/src/enssdk-example.integration.test.ts
similarity index 81%
rename from examples/enssdk-example/src/index.integration.test.ts
rename to packages/integration-test-env/src/enssdk-example.integration.test.ts
index fb0ead4710..8c9d00afb1 100644
--- a/examples/enssdk-example/src/index.integration.test.ts
+++ b/packages/integration-test-env/src/enssdk-example.integration.test.ts
@@ -3,12 +3,19 @@ import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { describe, expect, it } from "vitest";
-const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
+const EXAMPLE_DIR = join(
+ dirname(fileURLToPath(import.meta.url)),
+ "..",
+ "..",
+ "..",
+ "examples",
+ "enssdk-example",
+);
describe("enssdk-example", () => {
it("smoke test: completes against the configured ENSNode with exit code 0", () => {
const result = spawnSync("pnpm", ["start"], {
- cwd: PACKAGE_ROOT,
+ cwd: EXAMPLE_DIR,
env: process.env,
encoding: "utf8",
timeout: 60_000,
diff --git a/packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts b/packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts
new file mode 100644
index 0000000000..ea96148160
--- /dev/null
+++ b/packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts
@@ -0,0 +1,30 @@
+import { spawnSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+import { describe, expect, it } from "vitest";
+
+const EXAMPLE_DIR = join(
+ dirname(fileURLToPath(import.meta.url)),
+ "..",
+ "..",
+ "..",
+ "examples",
+ "omnigraph-graphql-example",
+);
+
+describe("omnigraph-graphql-example", () => {
+ it("smoke test: completes against the configured ENSNode with exit code 0", () => {
+ const result = spawnSync("pnpm", ["start"], {
+ cwd: EXAMPLE_DIR,
+ env: process.env,
+ encoding: "utf8",
+ timeout: 60_000,
+ });
+
+ // log into vitest's stdout capture so --silent passed-only hides it on success
+ if (result.stdout) console.log(result.stdout);
+ if (result.stderr) console.error(result.stderr);
+
+ expect(result.status).toBe(0);
+ });
+});
diff --git a/examples/enssdk-example/vitest.integration.config.ts b/packages/integration-test-env/vitest.integration.config.ts
similarity index 72%
rename from examples/enssdk-example/vitest.integration.config.ts
rename to packages/integration-test-env/vitest.integration.config.ts
index 20213b523d..c9333f2085 100644
--- a/examples/enssdk-example/vitest.integration.config.ts
+++ b/packages/integration-test-env/vitest.integration.config.ts
@@ -3,6 +3,6 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
- include: ["**/*.integration.test.ts"],
+ include: ["src/**/*.integration.test.ts"],
},
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b3c6548223..8b1fb2b038 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -867,9 +867,18 @@ importers:
typescript:
specifier: 'catalog:'
version: 5.9.3
- vitest:
+
+ examples/omnigraph-graphql-example:
+ devDependencies:
+ '@types/node':
specifier: 'catalog:'
- version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3)
+ version: 24.10.9
+ tsx:
+ specifier: ^4.7.1
+ version: 4.21.0
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
packages/datasources:
dependencies:
@@ -1184,6 +1193,10 @@ importers:
viem:
specifier: 'catalog:'
version: 2.38.5(typescript@5.9.3)(zod@4.3.6)
+ devDependencies:
+ vitest:
+ specifier: 'catalog:'
+ version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)
packages/namehash-ui:
dependencies:
@@ -11542,7 +11555,7 @@ snapshots:
'@esbuild-kit/core-utils@3.3.2':
dependencies:
- esbuild: 0.27.2
+ esbuild: 0.27.4
source-map-support: 0.5.21
'@esbuild-kit/esm-loader@2.6.5':
@@ -14966,14 +14979,6 @@ snapshots:
chai: 6.2.0
tinyrainbow: 3.0.3
- '@vitest/mocker@4.0.5(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3))':
- dependencies:
- '@vitest/spy': 4.0.5
- estree-walker: 3.0.3
- magic-string: 0.30.21
- optionalDependencies:
- vite: 6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)
-
'@vitest/mocker@4.0.5(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
'@vitest/spy': 4.0.5
@@ -20504,7 +20509,7 @@ snapshots:
vitest@4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3):
dependencies:
'@vitest/expect': 4.0.5
- '@vitest/mocker': 4.0.5(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3))
+ '@vitest/mocker': 4.0.5(vite@6.4.2(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3))
'@vitest/pretty-format': 4.0.5
'@vitest/runner': 4.0.5
'@vitest/snapshot': 4.0.5
From 2def6e2889ea3ebaf6fa2e51601279792383e3f3 Mon Sep 17 00:00:00 2001
From: shrugs
Date: Wed, 13 May 2026 14:52:25 -0500
Subject: [PATCH 08/17] rename to omnigraph-graphql-api
---
.../integrations/starlight/sidebar-topics/integrate.ts | 4 ++--
docs/ensnode.io/src/content/docs/docs/integrate/index.mdx | 2 +-
.../docs/docs/integrate/integration-options/index.mdx | 2 +-
.../{raw-graphql.mdx => omnigraph-graphql-api.mdx} | 6 +++---
examples/enssdk-example/src/index.ts | 3 ---
5 files changed, 7 insertions(+), 10 deletions(-)
rename docs/ensnode.io/src/content/docs/docs/integrate/integration-options/{raw-graphql.mdx => omnigraph-graphql-api.mdx} (93%)
diff --git a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts
index d74b6bd31e..ca7b2364e8 100644
--- a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts
+++ b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts
@@ -54,8 +54,8 @@ export const integrateSidebarTopic = {
link: "/docs/integrate/integration-options/enssdk",
},
{
- label: "Raw GraphQL",
- link: "/docs/integrate/integration-options/raw-graphql",
+ label: "Omnigraph GraphQL API",
+ link: "/docs/integrate/integration-options/omnigraph-graphql-api",
},
{
label: "ENSDb (PostgreSQL)",
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
index 8565d03e93..2d7f83678f 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
@@ -171,7 +171,7 @@ query MyDomains($address: Address!) {
This page will be a 60-second quickstart for querying ENSv2 data through ENSNode — including:
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx
index 39addf40d5..6db6d75794 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx
@@ -33,5 +33,5 @@ The Omnigraph API is a GraphQL API following the Relay specification, so you get
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/raw-graphql.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx
similarity index 93%
rename from docs/ensnode.io/src/content/docs/docs/integrate/integration-options/raw-graphql.mdx
rename to docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx
index 0b387627fa..5498ec6341 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/raw-graphql.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx
@@ -1,11 +1,11 @@
---
-title: Raw GraphQL
+title: Omnigraph GraphQL API
description: Query the ENS Omnigraph API directly via HTTP from any language.
---
import { LinkCard } from '@astrojs/starlight/components';
-The Omnigraph is **just a GraphQL API** following the [Relay specification](https://relay.dev/graphql/connections.htm). There's no proprietary protocol or transport — any GraphQL client in any language works, from `curl` and `fetch` to [`urql`](https://nearform.com/open-source/urql/), [Apollo](https://www.apollographql.com/), [`graphql-request`](https://github.com/jasonkuhrt/graphql-request), and beyond.
+The Omnigraph is **GraphQL API** following the [Relay specification](https://relay.dev/graphql/connections.htm). There's no proprietary protocol or transport — any GraphQL client in any language works, from `curl` and `fetch` to [`urql`](https://nearform.com/open-source/urql/), [Apollo](https://www.apollographql.com/), [`graphql-request`](https://github.com/jasonkuhrt/graphql-request), and beyond.
This guide walks you through the minimum: a single `fetch` call against `/api/omnigraph` — the same flow as our [omnigraph-graphql-example](https://github.com/namehash/ensnode/tree/main/examples/omnigraph-graphql-example).
@@ -31,7 +31,7 @@ Content-Type: application/json
{ "query": "...", "variables": { ... } }
```
-It returns `{ "data": ..., "errors": [...] }` per the standard GraphQL response shape. Nothing more.
+It returns `{ "data": ..., "errors": [...] }` per the standard GraphQL response shape.
A minimum-viable hello world over `curl`:
diff --git a/examples/enssdk-example/src/index.ts b/examples/enssdk-example/src/index.ts
index 6c882c485f..613e566bce 100644
--- a/examples/enssdk-example/src/index.ts
+++ b/examples/enssdk-example/src/index.ts
@@ -44,12 +44,10 @@ function formatDomain(data: FragmentOf): string {
async function main() {
const name = asInterpretedName("eth");
- const start = performance.now();
const result = await client.omnigraph.query({
query: HelloWorldQuery,
variables: { name },
});
- const elapsed = performance.now() - start;
if (result.errors) throw new Error(JSON.stringify(result.errors));
if (!result.data?.domain) throw new Error(`Domain '${name}' not found`);
@@ -57,7 +55,6 @@ async function main() {
const { domain } = result.data;
const totalCount = domain.subdomains?.totalCount ?? 0;
- console.log(`Query took ${(elapsed / 1000).toFixed(2)}s`);
console.log(formatDomain(domain));
console.log(`\nSubdomains (showing 20 of ${totalCount}):`);
for (const { node } of domain.subdomains?.edges ?? []) {
From c1916e3ca5ae88ffafe6ec993877d9eb6c1472f8 Mon Sep 17 00:00:00 2001
From: shrugs
Date: Wed, 13 May 2026 14:59:42 -0500
Subject: [PATCH 09/17] fix: lint
---
examples/omnigraph-graphql-example/src/index.ts | 15 +++++++++------
.../src/enssdk-example.integration.test.ts | 3 ++-
.../omnigraph-graphql-example.integration.test.ts | 3 ++-
3 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/examples/omnigraph-graphql-example/src/index.ts b/examples/omnigraph-graphql-example/src/index.ts
index 2844643e7e..acee928a6e 100644
--- a/examples/omnigraph-graphql-example/src/index.ts
+++ b/examples/omnigraph-graphql-example/src/index.ts
@@ -1,5 +1,6 @@
// you may use a NameHash Hosted ENSNode instance
// learn more at https://ensnode.io/docs/integrate/hosted-instances
+// biome-ignore lint/style/noNonNullAssertion: invariant
const ENSNODE_URL = process.env.ENSNODE_URL!;
// The Omnigraph is a standard GraphQL API following the Relay spec.
@@ -26,12 +27,14 @@ interface Domain {
interface QueryResult {
data?: {
- domain: (Domain & {
- subdomains: {
- totalCount: number;
- edges: { node: Domain }[];
- } | null;
- }) | null;
+ domain:
+ | (Domain & {
+ subdomains: {
+ totalCount: number;
+ edges: { node: Domain }[];
+ } | null;
+ })
+ | null;
} | null;
errors?: { message: string }[];
}
diff --git a/packages/integration-test-env/src/enssdk-example.integration.test.ts b/packages/integration-test-env/src/enssdk-example.integration.test.ts
index 8c9d00afb1..445a1a1379 100644
--- a/packages/integration-test-env/src/enssdk-example.integration.test.ts
+++ b/packages/integration-test-env/src/enssdk-example.integration.test.ts
@@ -1,6 +1,7 @@
import { spawnSync } from "node:child_process";
-import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+
import { describe, expect, it } from "vitest";
const EXAMPLE_DIR = join(
diff --git a/packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts b/packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts
index ea96148160..7bd941d44d 100644
--- a/packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts
+++ b/packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts
@@ -1,6 +1,7 @@
import { spawnSync } from "node:child_process";
-import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+
import { describe, expect, it } from "vitest";
const EXAMPLE_DIR = join(
From 7c5eaeb940fe28b6320d5a1d921f5f251dc62435 Mon Sep 17 00:00:00 2001
From: shrugs
Date: Wed, 13 May 2026 15:00:43 -0500
Subject: [PATCH 10/17] docs(changeset): Updates the Omnigraph Integration
Documentation with comprehensive getting started examples and walkthroughs.
---
.changeset/tender-items-crash.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/tender-items-crash.md
diff --git a/.changeset/tender-items-crash.md b/.changeset/tender-items-crash.md
new file mode 100644
index 0000000000..40273f453e
--- /dev/null
+++ b/.changeset/tender-items-crash.md
@@ -0,0 +1,5 @@
+---
+"@docs/ensnode": patch
+---
+
+Updates the Omnigraph Integration Documentation with comprehensive getting started examples and walkthroughs.
From c46765a138dbcb77559230fb60d7d177d3bbf863 Mon Sep 17 00:00:00 2001
From: shrugs
Date: Wed, 13 May 2026 15:45:41 -0500
Subject: [PATCH 11/17] docs(integrate): address bot review feedback on
Omnigraph guides
- fix typos: comming/dirctly/idomatic
- hyphenate "up-to-date" compound adjectives
- remove leftover TODO placeholder and stale 60-second quickstart list
- add canonical { name } to DomainFragment to match snippet usage
- destructure data/fetching/error from useOmnigraphQuery result
- add missing article in Omnigraph GraphQL API intro
- update example README link to renamed Omnigraph GraphQL API guide
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../src/content/docs/docs/integrate/index.mdx | 18 +++++-------------
.../integrate/integration-options/enssdk.mdx | 2 +-
.../integrate/integration-options/index.mdx | 4 ++--
.../omnigraph-graphql-api.mdx | 2 +-
examples/omnigraph-graphql-example/README.md | 2 +-
5 files changed, 10 insertions(+), 18 deletions(-)
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
index 2d7f83678f..d5d1b96c98 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
@@ -10,16 +10,14 @@ import { LinkCard } from '@astrojs/starlight/components';
:::tip[Prepare for ENSv2]
-The ENSv2 upgrade to the ENS protocol is comming **Summer 2026**! Your app, regardless of how it interacts with names, needs to be updated to avoid being left behind.
+The ENSv2 upgrade to the ENS protocol is coming **Summer 2026**! Your app, regardless of how it interacts with names, needs to be updated to avoid being left behind.
Learn more about ENSv2 Readiness
:::
-ENSNode fully supports the ENSv2 upgrade via the [Omnigraph API](/docs/integrate/omnigraph), a _unified_ API over **both** ENSv1 and ENSv2. ENSNode takes the guesswork out of ENS integrations, whether you need to resolve up to date records, search all Domains, or see which Domains a user owns (and much, much more).
-
-TODO: insert the Omnigraph ENSv1 + ENSv2 asset here
+ENSNode fully supports the ENSv2 upgrade via the [Omnigraph API](/docs/integrate/omnigraph), a _unified_ API over **both** ENSv1 and ENSv2. ENSNode takes the guesswork out of ENS integrations, whether you need to resolve up-to-date records, search all Domains, or see which Domains a user owns (and much, much more).
You can integrate with ENSNode in many different ways, whether you're using React, any JavaScript runtime, or raw GraphQL.
@@ -40,6 +38,7 @@ const DomainFragment = graphql(`
__typename
id
name
+ canonical { name }
owner { id address }
}
`);
@@ -74,6 +73,7 @@ function RenderDomainFragment({ data }: { data: FragmentOfLoading...
;
@@ -152,7 +152,7 @@ const result = await client.omnigraph.query({ query: HelloWorldQuery });
## 3. Omnigraph GraphQL API
-The Omnigraph API is a GraphQL API following the Relay specification, so you get built-in support for efficient infinite pagination and idomatic access to all of the ENS protocol within a _unified_ ENSv1 + ENSv2 datamodel.
+The Omnigraph API is a GraphQL API following the Relay specification, so you get built-in support for efficient infinite pagination and idiomatic access to all of the ENS protocol within a _unified_ ENSv1 + ENSv2 datamodel.
```gql
query MyDomains($address: Address!) {
@@ -173,11 +173,3 @@ query MyDomains($address: Address!) {
title='Full Omnigraph GraphQL API Documentation'
href="/docs/integrate/integration-options/omnigraph-graphql-api"
/>
-
-This page will be a 60-second quickstart for querying ENSv2 data through ENSNode — including:
-
-- A **React example** with [enskit](/docs/integrate/integration-options/enskit) using the `useOmnigraphQuery` hook.
-- A **interactive code snippet** with [enssdk](/docs/integrate/integration-options/enssdk) showing a typed ENS Omnigraph query (e.g. look up a domain). Optionally can launch the request against the ENS Omnigraph API and see the response in your browser.
-- Links to [hosted instances](/docs/integrate/hosted-instances) so you can start querying immediately with zero setup.
-- [ENSv2 Readiness](/docs/integrate/ensv2-readiness) — how building with ENSNode today prepares your app for ENSv2.
-- [Integration Options](/docs/integrate/integration-options) — choose the right path: enskit, enssdk, raw GraphQL, or ENSDb.
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
index 5a64d1dfd8..178dd6ee23 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
@@ -7,7 +7,7 @@ import { LinkCard } from '@astrojs/starlight/components';
`enssdk` is the foundational TypeScript/JavaScript SDK for ENS development. It's a fully modular and tree-shakeable modern TypeScript/JavaScript package that provides ENS-specific types and helpers — usable from any JS runtime, browser or server.
-`enssdk` also dirctly integrates with ENSNode, providing an `EnsNodeClient` (via `createEnsNodeClient`) that can be extended with a fully-typed Omnigraph API client (powered by [`gql.tada`](https://gql-tada.0no.co/)).
+`enssdk` also directly integrates with ENSNode, providing an `EnsNodeClient` (via `createEnsNodeClient`) that can be extended with a fully-typed Omnigraph API client (powered by [`gql.tada`](https://gql-tada.0no.co/)).
This guide walks you from an empty directory to a working TypeScript script that queries the `eth` Domain and queries its subdomains — the same flow as our [enssdk-example](https://github.com/namehash/ensnode/tree/main/examples/enssdk-example).
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx
index 6db6d75794..0b06e8a3a0 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/index.mdx
@@ -5,7 +5,7 @@ description: Integration options for building with ENSNode.
import { LinkCard } from '@astrojs/starlight/components';
-ENSNode takes the guesswork out of ENS integrations, whether you need to resolve up to date records, search all Domains, or see which Domains a user owns (and much, much more).
+ENSNode takes the guesswork out of ENS integrations, whether you need to resolve up-to-date records, search all Domains, or see which Domains a user owns (and much, much more).
There are a few different ways to integrate with ENSNode, depending on your app, runtime, and needs.
@@ -29,7 +29,7 @@ With `enssdk`, leverage ENSNode and the Omnigraph from any JavaScript runtime to
## 3. Omnigraph GraphQL API
-The Omnigraph API is a GraphQL API following the Relay specification, so you get built-in support for efficient infinite pagination and idomatic access to all of the ENS protocol within a _unified_ ENSv1 + ENSv2 datamodel.
+The Omnigraph API is a GraphQL API following the Relay specification, so you get built-in support for efficient infinite pagination and idiomatic access to all of the ENS protocol within a _unified_ ENSv1 + ENSv2 datamodel.
Date: Wed, 13 May 2026 15:53:41 -0500
Subject: [PATCH 12/17] test(integration-test-env): set 10s timeout on example
smoke tests
vitest's default testTimeout (5s) was tighter than the spawnSync
ceiling (60s), so a slow subprocess would surface as a vitest worker
timeout instead of a meaningful failure. Align both bounds at 10s
per test, keeping the timeout local to the test instead of widening
the workspace config.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../src/enssdk-example.integration.test.ts | 4 ++--
.../src/omnigraph-graphql-example.integration.test.ts | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/integration-test-env/src/enssdk-example.integration.test.ts b/packages/integration-test-env/src/enssdk-example.integration.test.ts
index 445a1a1379..0ba6c15b9e 100644
--- a/packages/integration-test-env/src/enssdk-example.integration.test.ts
+++ b/packages/integration-test-env/src/enssdk-example.integration.test.ts
@@ -19,7 +19,7 @@ describe("enssdk-example", () => {
cwd: EXAMPLE_DIR,
env: process.env,
encoding: "utf8",
- timeout: 60_000,
+ timeout: 10_000,
});
// log into vitest's stdout capture so --silent passed-only hides it on success
@@ -27,5 +27,5 @@ describe("enssdk-example", () => {
if (result.stderr) console.error(result.stderr);
expect(result.status).toBe(0);
- });
+ }, 10_000);
});
diff --git a/packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts b/packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts
index 7bd941d44d..212ba1dd64 100644
--- a/packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts
+++ b/packages/integration-test-env/src/omnigraph-graphql-example.integration.test.ts
@@ -19,7 +19,7 @@ describe("omnigraph-graphql-example", () => {
cwd: EXAMPLE_DIR,
env: process.env,
encoding: "utf8",
- timeout: 60_000,
+ timeout: 10_000,
});
// log into vitest's stdout capture so --silent passed-only hides it on success
@@ -27,5 +27,5 @@ describe("omnigraph-graphql-example", () => {
if (result.stderr) console.error(result.stderr);
expect(result.status).toBe(0);
- });
+ }, 10_000);
});
From 1843483cc464998ab2b27a11c93c395ef948de99 Mon Sep 17 00:00:00 2001
From: sevenzing
Date: Thu, 14 May 2026 11:01:53 +0300
Subject: [PATCH 13/17] small fixes
---
.../docs/docs/integrate/integration-options/enskit.mdx | 4 ++--
.../docs/docs/integrate/integration-options/enssdk.mdx | 5 +++--
.../integration-options/omnigraph-graphql-api.mdx | 4 ++--
examples/enskit-react-example/tsconfig.json | 3 +++
examples/enssdk-example/README.md | 4 ++--
examples/enssdk-example/tsconfig.json | 8 +++-----
examples/omnigraph-graphql-example/README.md | 2 +-
7 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
index 055fed8781..30f37bf1f4 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit.mdx
@@ -33,7 +33,7 @@ npm install
## 2. Install `enskit` and `enssdk`
```sh
-npm install enskit@1.13.1 enssdk@1.13.1
+npm install enskit enssdk
```
:::tip[Pin exact versions]
@@ -384,7 +384,7 @@ export function DomainView() {
## 9. Run it
```sh
-npm run dev
+VITE_ENSNODE_URL=https://api.alpha.ensnode.io npm run dev
```
Open the printed URL and you should see the `eth` Domain, its owner, and the first page of its subdomains. Clicking **Next page** advances the cursor.
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
index 178dd6ee23..ab707b40cd 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk.mdx
@@ -37,7 +37,7 @@ mkdir src
We'll use [`tsx`](https://tsx.is/) to run TypeScript directly without a bundler.
```sh
-npm install enssdk@1.13.1
+npm install enssdk
npm install -D tsx typescript @types/node
```
@@ -61,6 +61,7 @@ Create `tsconfig.json`:
"esModuleInterop": true,
"skipLibCheck": true,
"lib": ["ESNext"],
+ "types": ["node"],
"plugins": [
{
"name": "gql.tada/ts-plugin",
@@ -291,7 +292,7 @@ async function main() {
Point at a hosted ENSNode and go:
```sh
-ENSNODE_URL=https://api.alpha.green.ensnode.io npm start
+ENSNODE_URL=https://api.alpha.ensnode.io npm start
```
You should see the `eth` Domain, followed by its first 20 subdomains and the total subdomain count.
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx
index c86b7239f8..ebf00a4d25 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx
@@ -39,7 +39,7 @@ A minimum-viable hello world over `curl`:
curl -sS -X POST \
-H 'Content-Type: application/json' \
-d '{"query":"{ domain(by: { name: \"eth\" }) { name owner { address } } }"}' \
- https://api.v2-sepolia.ensnode.io/api/omnigraph
+ https://api.alpha.ensnode.io/api/omnigraph
```
The rest of this guide builds the same thing in TypeScript using `fetch`, so you have something to extend.
@@ -162,7 +162,7 @@ A few things to notice:
## 4. Run it
```sh
-ENSNODE_URL=https://api.v2-sepolia.ensnode.io npm start
+ENSNODE_URL=https://api.alpha.ensnode.io npm start
```
You should see the `eth` Domain, its owner, and the first 20 of its subdomains.
diff --git a/examples/enskit-react-example/tsconfig.json b/examples/enskit-react-example/tsconfig.json
index 78e1c025c7..c2bc4a9f7b 100644
--- a/examples/enskit-react-example/tsconfig.json
+++ b/examples/enskit-react-example/tsconfig.json
@@ -12,6 +12,9 @@
// here we enable the gql.tada typescript plugin for
{
"name": "gql.tada/ts-plugin",
+ // NOTE: this example is in the ENSNode monorepo, so we imports it from parent directory.
+ // In a ordinary app you'd import it from the package like so:
+ // "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql",
"schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql",
"tadaOutputLocation": "./src/generated/graphql-env.d.ts"
}
diff --git a/examples/enssdk-example/README.md b/examples/enssdk-example/README.md
index a1f82872da..ff75d02fd6 100644
--- a/examples/enssdk-example/README.md
+++ b/examples/enssdk-example/README.md
@@ -10,8 +10,8 @@ Companion to the [enssdk integration guide](https://ensnode.io/docs/integrate/in
# from the ENSNode monorepo root
pnpm install
-ENSNODE_URL=https://api.v2-sepolia.ensnode.io pnpm -F enssdk-example start
-```
+ENSNODE_URL=https://api.alpha.ensnode.io pnpm -F enssdk-example start
+````
## Usage (with Local ENSNode)
diff --git a/examples/enssdk-example/tsconfig.json b/examples/enssdk-example/tsconfig.json
index cdccbda879..1a1b0a5a12 100644
--- a/examples/enssdk-example/tsconfig.json
+++ b/examples/enssdk-example/tsconfig.json
@@ -11,12 +11,10 @@
// here we enable the gql.tada typescript plugin
{
"name": "gql.tada/ts-plugin",
-
- // because this example is in the ENSNode monorepo, it imports from src
- "schema": "../../packages/enssdk/src/omnigraph/generated/schema.graphql",
- // but in a normal app you'd import it from the package like so:
+ // NOTE: this example is in the ENSNode monorepo, so we imports it from parent directory.
+ // In a ordinary app you'd import it from the package like so:
// "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql",
-
+ "schema": "../../packages/enssdk/src/omnigraph/generated/schema.graphql",
"tadaOutputLocation": "./src/generated/graphql-env.d.ts"
}
]
diff --git a/examples/omnigraph-graphql-example/README.md b/examples/omnigraph-graphql-example/README.md
index a8b4e9845b..56e45e8722 100644
--- a/examples/omnigraph-graphql-example/README.md
+++ b/examples/omnigraph-graphql-example/README.md
@@ -12,7 +12,7 @@ Companion to the [Omnigraph GraphQL API integration guide](https://ensnode.io/do
# from the ENSNode monorepo root
pnpm install
-ENSNODE_URL=https://api.v2-sepolia.ensnode.io pnpm -F omnigraph-graphql-example start
+ENSNODE_URL=https://api.alpha.ensnode.io pnpm -F omnigraph-graphql-example start
```
## Usage (with Local ENSNode)
From 2acff87e5afd1790ec6f695c0b692289632be04c Mon Sep 17 00:00:00 2001
From: sevenzing
Date: Thu, 14 May 2026 12:01:05 +0300
Subject: [PATCH 14/17] small fixes 2
---
.../starlight/sidebar-topics/integrate.ts | 2 +-
.../public/ens-omnigraph-diagram.png | Bin 0 -> 1144978 bytes
.../molecules/EnsDbSelfHostingNote.astro | 9 +++++
.../molecules/IntegrateHostedEnsNodeTip.astro | 32 ++++++++++++++++++
.../integrate/integration-options/ensdb.mdx | 7 ++--
.../integrate/integration-options/enskit.mdx | 9 ++---
.../integrate/integration-options/enssdk.mdx | 10 ++----
.../omnigraph-graphql-api.mdx | 14 +++-----
examples/omnigraph-graphql-example/README.md | 2 +-
9 files changed, 55 insertions(+), 30 deletions(-)
create mode 100644 docs/ensnode.io/public/ens-omnigraph-diagram.png
create mode 100644 docs/ensnode.io/src/components/molecules/EnsDbSelfHostingNote.astro
create mode 100644 docs/ensnode.io/src/components/molecules/IntegrateHostedEnsNodeTip.astro
diff --git a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts
index ca7b2364e8..d5ce80018a 100644
--- a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts
+++ b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts
@@ -54,7 +54,7 @@ export const integrateSidebarTopic = {
link: "/docs/integrate/integration-options/enssdk",
},
{
- label: "Omnigraph GraphQL API",
+ label: "ENS Omnigraph (GraphQL)",
link: "/docs/integrate/integration-options/omnigraph-graphql-api",
},
{
diff --git a/docs/ensnode.io/public/ens-omnigraph-diagram.png b/docs/ensnode.io/public/ens-omnigraph-diagram.png
new file mode 100644
index 0000000000000000000000000000000000000000..60081a86fbedc850a6a4bd3bebd3ab083d1f01c5
GIT binary patch
literal 1144978
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