diff --git a/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro b/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro
index f05ea3fb3e..9aacdf3c6c 100644
--- a/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro
+++ b/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro
@@ -1,40 +1,18 @@
---
import { Aside } from "@astrojs/starlight/components";
-interface Props {
- /**
- * Which SDK(s) the warning is for.
- * - "enssdk": only enssdk
- * - "enskit": both enskit and enssdk (enskit depends on enssdk)
- * - "both": mention both enssdk and enskit, show both install commands
- */
- for?: "enssdk" | "enskit" | "both";
-}
+import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active";
-const { for: target = "enssdk" } = Astro.props;
-
-const isEnskit = target === "enskit";
-const isBoth = target === "both";
-
-const sdkList = isEnskit
- ? "enskit@1.13.1 and enssdk@1.13.1"
- : isBoth
- ? "enssdk@1.13.1 (and enskit@1.13.1 when using React)"
- : "enssdk@1.13.1";
+// The SDK version is locked to the production-deployed Omnigraph version (see
+// `@data/omnigraph-examples/active`). The SDK bundles the Omnigraph schema, so pinning the matching
+// version keeps gql.tada's generated types aligned with the deployed API. Updates on promotion.
+const VERSION = ACTIVE_OMNIGRAPH_VERSION.replace(/^v/, "");
---
diff --git a/docs/ensnode.io/src/components/molecules/IntegrateHostedEnsNodeTip.astro b/docs/ensnode.io/src/components/molecules/IntegrateHostedEnsNodeTip.astro
index eaa37598be..94f34e23c9 100644
--- a/docs/ensnode.io/src/components/molecules/IntegrateHostedEnsNodeTip.astro
+++ b/docs/ensnode.io/src/components/molecules/IntegrateHostedEnsNodeTip.astro
@@ -1,24 +1,12 @@
---
import { Aside, LinkCard } from "@astrojs/starlight/components";
import HostedInstanceSdkVersionWarning from "./HostedInstanceSdkVersionWarning.astro";
-
-interface Props {
- /**
- * Which SDK the warning should target. Passed through to HostedInstanceSdkVersionWarning.
- * - "enssdk": enssdk-only warning
- * - "enskit": both enskit and enssdk warning
- * - "both": mention both enssdk and enskit, show both install commands (default)
- */
- for?: "enssdk" | "enskit" | "both";
-}
-
-const { for: target = "both" } = Astro.props;
---
-
+(
+ "../../data/omnigraph-examples/versions/*/schema.graphql",
+ { query: "?raw", import: "default", eager: true },
+);
+
+const omnigraphSchemaSdl =
+ schemasByVersion[
+ `../../data/omnigraph-examples/versions/${ACTIVE_OMNIGRAPH_VERSION}/schema.graphql`
+ ];
+
+if (!omnigraphSchemaSdl) {
+ throw new Error(`No Omnigraph schema snapshot for version "${ACTIVE_OMNIGRAPH_VERSION}".`);
+}
+
const omnigraphSchema = buildSchema(omnigraphSchemaSdl);
export default function OmnigraphSchemaDocExplorer() {
diff --git a/docs/ensnode.io/src/components/walkthroughs/enskit/v1.13.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.13.1.mdx
new file mode 100644
index 0000000000..675d18ee11
--- /dev/null
+++ b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.13.1.mdx
@@ -0,0 +1,412 @@
+import { LinkCard, Steps } from '@astrojs/starlight/components';
+import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro';
+
+`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.
+
+
+
+
+## 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
+```
+
+:::caution[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, and matched to the ENSNode version your hosted instance runs.
+:::
+
+## 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";
+
+// you may use a NameHash Hosted ENSNode instance
+// learn more at https://ensnode.io/docs/hosted-instances
+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
+ 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.
+- `name` 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
+ name
+ owner { address }
+ subdomains {
+ edges {
+ node {
+ 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
+VITE_ENSNODE_URL=https://api.v2-sepolia.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.
+
+## Where to go next
+
+- Try the [Interactive Example](/docs/integrate/integration-options/enskit/example): edit and run the full `enskit-react-example` app in your browser with a live preview.
+- 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.
+- See [Omnigraph examples](/docs/integrate/omnigraph/examples) 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/components/walkthroughs/enskit/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx
new file mode 100644
index 0000000000..ee1a77508e
--- /dev/null
+++ b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx
@@ -0,0 +1,412 @@
+import { LinkCard, Steps } from '@astrojs/starlight/components';
+import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro';
+
+`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.
+
+
+
+
+## 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.14.1 enssdk@1.14.1
+```
+
+:::caution[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, and matched to the ENSNode version your hosted instance runs.
+:::
+
+## 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";
+
+// you may use a NameHash Hosted ENSNode instance
+// learn more at https://ensnode.io/docs/hosted-instances
+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 } from "enssdk";
+
+const DomainByNameQuery = graphql(`
+ query DomainByName($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ __typename
+ canonical { name { beautified } }
+ 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 { beautified } }
+ owner { address }
+ subdomains {
+ edges {
+ node {
+ canonical { name { beautified } }
+ 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
+VITE_ENSNODE_URL=https://api.v2-sepolia.blue.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.
+
+## Where to go next
+
+- Try the [Interactive Example](/docs/integrate/integration-options/enskit/example): edit and run the full `enskit-react-example` app in your browser with a live preview.
+- 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.
+- See [Omnigraph examples](/docs/integrate/omnigraph/examples) 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/components/walkthroughs/enssdk/v1.13.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.13.1.mdx
new file mode 100644
index 0000000000..a7e169569d
--- /dev/null
+++ b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.13.1.mdx
@@ -0,0 +1,312 @@
+import { LinkCard } from '@astrojs/starlight/components';
+import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro';
+
+`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 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).
+
+
+
+## 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
+```
+
+:::caution[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, and matched to the ENSNode version your hosted instance runs.
+:::
+
+
+## 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" ins={10-15}
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "lib": ["ESNext"],
+ "types": ["node"],
+ "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 instance
+// learn more at https://ensnode.io/docs/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). When non-null, it is the Domain's name as an InterpretedName. **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-38}
+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}):`);
+ for (const { node } of domain.subdomains?.edges ?? []) {
+ const subName = node.name ? beautifyInterpretedName(node.name) : "";
+ console.log(` - ${subName} — Owner ${node.owner?.address ?? "0x0"}`);
+ }
+}
+```
+
+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).
+
+## 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,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 instance
+// learn more at https://ensnode.io/docs/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}):`);
+ 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, 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
+```
+
+You should see the `eth` Domain, followed by its first 20 subdomains and the total subdomain count.
+
+## Where to go next
+
+- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy GraphQL 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/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx
new file mode 100644
index 0000000000..eb410b9adc
--- /dev/null
+++ b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx
@@ -0,0 +1,312 @@
+import { LinkCard } from '@astrojs/starlight/components';
+import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro';
+
+`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 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).
+
+
+
+## 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.14.1
+npm install -D tsx typescript @types/node
+```
+
+:::caution[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, and matched to the ENSNode version your hosted instance runs.
+:::
+
+
+## 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" ins={10-15}
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "lib": ["ESNext"],
+ "types": ["node"],
+ "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 instance
+// learn more at https://ensnode.io/docs/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 } 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
+ canonical { name { beautified } }
+ 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.canonical ? domain.canonical.name.beautified : ""}`);
+ 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.
+- `canonical` is `null` for non-canonical Domains (e.g. Domains whose name cannot be inferred). When non-null, `canonical.name.beautified` is the Domain's Canonical Name, beautified for display. **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-38}
+const HelloWorldQuery = graphql(`
+ query HelloWorld($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ __typename
+ canonical { name { beautified } }
+ owner { address }
+ subdomains(first: 20) {
+ totalCount
+ edges {
+ node { canonical { name { beautified } } 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.canonical ? domain.canonical.name.beautified : ""}`);
+ console.log(`Version: ${domain.__typename}`);
+ console.log(`Owner: ${domain.owner?.address ?? "0x0"}`);
+
+ console.log(`\nSubdomains (showing 20 of ${domain.subdomains?.totalCount ?? 0}):`);
+ for (const { node } of domain.subdomains?.edges ?? []) {
+ const subName = node.canonical ? node.canonical.name.beautified : "";
+ console.log(` - ${subName} — Owner ${node.owner?.address ?? "0x0"}`);
+ }
+}
+```
+
+Notice we're now writing the same canonical/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).
+
+## 7. Extract a typed fragment
+
+Notice we're selecting the same fields (`canonical { name { beautified } }`, `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,11-17,23,26,31,34-40,56,59}
+import { asInterpretedName } from "enssdk";
+import { createEnsNodeClient } from "enssdk/core";
+import { type FragmentOf, graphql, omnigraph, readFragment } from "enssdk/omnigraph";
+
+// you may use a NameHash Hosted ENSNode instance
+// learn more at https://ensnode.io/docs/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
+ canonical { name { beautified } }
+ 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.canonical ? domain.canonical.name.beautified : "";
+ 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}):`);
+ 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, 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.blue.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
+
+- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy GraphQL 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/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.13.1.mdx b/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.13.1.mdx
new file mode 100644
index 0000000000..1527e7509a
--- /dev/null
+++ b/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.13.1.mdx
@@ -0,0 +1,182 @@
+import { LinkCard } from '@astrojs/starlight/components';
+import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro';
+
+The Omnigraph is **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.
+
+
+
+## 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.
+
+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/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 [Omnigraph examples](/docs/integrate/omnigraph/examples) 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/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx
new file mode 100644
index 0000000000..ee7a27c425
--- /dev/null
+++ b/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx
@@ -0,0 +1,182 @@
+import { LinkCard } from '@astrojs/starlight/components';
+import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro';
+
+The Omnigraph is **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.
+
+
+
+## 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.
+
+A minimum-viable hello world over `curl`:
+
+```sh
+curl -sS -X POST \
+ -H 'Content-Type: application/json' \
+ -d '{"query":"{ domain(by: { name: \"eth\" }) { canonical { name { beautified } } owner { address } } }"}' \
+ https://api.v2-sepolia.blue.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/hosted-instances
+const ENSNODE_URL = process.env.ENSNODE_URL!;
+
+const HELLO_WORLD_QUERY = /* GraphQL */ `
+ query HelloWorld($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ __typename
+ canonical { name { beautified } }
+ owner { address }
+ subdomains(first: 20) {
+ totalCount
+ edges { node { __typename canonical { name { beautified } } owner { address } } }
+ }
+ }
+ }
+`;
+
+interface Domain {
+ __typename: "ENSv1Domain" | "ENSv2Domain";
+ canonical: { name: { beautified: 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.canonical?.name.beautified ?? "";
+ 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.blue.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 [Omnigraph examples](/docs/integrate/omnigraph/examples) 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/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.13.1.mdx b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.13.1.mdx
new file mode 100644
index 0000000000..461f5d6227
--- /dev/null
+++ b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.13.1.mdx
@@ -0,0 +1,207 @@
+import { LinkCard, CardGrid, Aside } from "@astrojs/starlight/components";
+import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanceSdkVersionWarning.astro";
+import OmnigraphAPIExample from "@components/organisms/OmnigraphAPIExample.astro";
+
+## What is ENSv2?
+
+[ENSv2](https://ens.domains/ensv2) is the next generation of the [Ethereum Name Service](https://ens.domains) — a protocol upgrade that fundamentally changes how the ENS protocol works.
+
+:::tip[Prepare for ENSv2]
+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
+
+:::
+
+## What is the ENS Omnigraph?
+
+ENSNode fully supports ENSv2 via the [ENS Omnigraph API](/docs/integrate/omnigraph), the world's first and only _unified_ API over the full state of **both ENSv1 and ENSv2**. When ENSv2 launches in **Summer 2026**, ENSv1 continues to exist, and apps _must_ be updated to use the new protocol version. ENSNode takes the guesswork out of building on ENS, whether you need to resolve up-to-date records, search all Domains, or see which Domains a user owns (and much, much more).
+
+
+
+ENS Omnigraph supports both ENSv1 and ENSv2 **concurrently** within the **same unified data model**. This means you can integrate today (before ENSv2 launches) and continue with full ENSv2 support when it goes live, with zero downtime!
+
+## ENSNode's Integration Options
+
+ENSNode supports a full range of different integration options across the stack, whether you're using React, any JavaScript runtime, raw GraphQL, or looking to go deep and build a fully custom service using indexed ENS data.
+
+
+
+Here's a summary of some popular integration strategies:
+
+### 1. `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 });
+```
+
+
+
+
+
+
+
+
+### 2. `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.name ? beautifyInterpretedName(domain.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 } });
+ const { data, fetching, error } = result;
+
+ // 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 (
+
+ );
+}
+```
+
+
+
+
+
+
+
+### 3. ENS Omnigraph GraphQL API
+
+The ENS 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.
+
+
+
+
+
+
+
+### 4. Further Integration Options
+
+Beyond `enssdk`, `enskit`, and the Omnigraph GraphQL API, ENSNode exposes a deeper set of integration surfaces for advanced use cases:
+
+- **ENSDb** — query the indexed ENS dataset directly over Postgres for custom analytics or your own service layer.
+- **enscli** — (coming soon) script ENSNode operations from the command line.
+- **ensskills** — (coming soon) AI agent tooling for working with ENS data.
+- **ensdb-cli** — (coming soon) share and load point-in-time ENSDb snapshots.
+- **ENSEngine** (coming soon) — subscribe to ENS-aware webhooks driven by changes in ENSDb.
+
+
diff --git a/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx
new file mode 100644
index 0000000000..1fc1fffcd8
--- /dev/null
+++ b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx
@@ -0,0 +1,207 @@
+import { LinkCard, CardGrid, Aside } from "@astrojs/starlight/components";
+import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanceSdkVersionWarning.astro";
+import OmnigraphAPIExample from "@components/organisms/OmnigraphAPIExample.astro";
+
+## What is ENSv2?
+
+[ENSv2](https://ens.domains/ensv2) is the next generation of the [Ethereum Name Service](https://ens.domains) — a protocol upgrade that fundamentally changes how the ENS protocol works.
+
+:::tip[Prepare for ENSv2]
+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
+
+:::
+
+## What is the ENS Omnigraph?
+
+ENSNode fully supports ENSv2 via the [ENS Omnigraph API](/docs/integrate/omnigraph), the world's first and only _unified_ API over the full state of **both ENSv1 and ENSv2**. When ENSv2 launches in **Summer 2026**, ENSv1 continues to exist, and apps _must_ be updated to use the new protocol version. ENSNode takes the guesswork out of building on ENS, whether you need to resolve up-to-date records, search all Domains, or see which Domains a user owns (and much, much more).
+
+
+
+ENS Omnigraph supports both ENSv1 and ENSv2 **concurrently** within the **same unified data model**. This means you can integrate today (before ENSv2 launches) and continue with full ENSv2 support when it goes live, with zero downtime!
+
+## ENSNode's Integration Options
+
+ENSNode supports a full range of different integration options across the stack, whether you're using React, any JavaScript runtime, raw GraphQL, or looking to go deep and build a fully custom service using indexed ENS data.
+
+
+
+Here's a summary of some popular integration strategies:
+
+### 1. `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
+ canonical { name { beautified } }
+ owner { address }
+ }
+ }
+`);
+
+// `result` is fully typed!
+const result = await client.omnigraph.query({ query: HelloWorldQuery });
+```
+
+
+
+
+
+
+
+
+### 2. `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
+ canonical { name { beautified } }
+ 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 ? domain.canonical.name.beautified : '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 } });
+ const { data, fetching, error } = result;
+
+ // 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 (
+
+ );
+}
+```
+
+
+
+
+
+
+
+### 3. ENS Omnigraph GraphQL API
+
+The ENS 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.
+
+
+
+
+
+
+
+### 4. Further Integration Options
+
+Beyond `enssdk`, `enskit`, and the Omnigraph GraphQL API, ENSNode exposes a deeper set of integration surfaces for advanced use cases:
+
+- **ENSDb** — query the indexed ENS dataset directly over Postgres for custom analytics or your own service layer.
+- **enscli** — (coming soon) script ENSNode operations from the command line.
+- **ensskills** — (coming soon) AI agent tooling for working with ENS data.
+- **ensdb-cli** — (coming soon) share and load point-in-time ENSDb snapshots.
+- **ENSEngine** (coming soon) — subscribe to ENS-aware webhooks driven by changes in ENSDb.
+
+
diff --git a/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx b/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx
index 99e6fbbf5a..946eee7352 100644
--- a/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx
@@ -15,7 +15,7 @@ NameHash Labs provides freely available hosted instances for ENS developers look
These instances are provided free of charge with no API key required, have no rate limiting, and are maintained and monitored by the NameHash Labs team.
-
+
### Available instance configurations
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 31c824b196..e74f1efcfe 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
@@ -6,210 +6,24 @@ sidebar:
order: 1
---
-import { LinkCard, CardGrid, Aside } from "@astrojs/starlight/components";
-import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanceSdkVersionWarning.astro";
-import OmnigraphAPIExample from "@components/organisms/OmnigraphAPIExample.astro";
-
-## What is ENSv2?
-
-[ENSv2](https://ens.domains/ensv2) is the next generation of the [Ethereum Name Service](https://ens.domains) — a protocol upgrade that fundamentally changes how the ENS protocol works.
-
-:::tip[Prepare for ENSv2]
-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
-
-:::
-
-## What is the ENS Omnigraph?
-
-ENSNode fully supports ENSv2 via the [ENS Omnigraph API](/docs/integrate/omnigraph), the world's first and only _unified_ API over the full state of **both ENSv1 and ENSv2**. When ENSv2 launches in **Summer 2026**, ENSv1 continues to exist, and apps _must_ be updated to use the new protocol version. ENSNode takes the guesswork out of building on ENS, whether you need to resolve up-to-date records, search all Domains, or see which Domains a user owns (and much, much more).
-
-
-
-ENS Omnigraph supports both ENSv1 and ENSv2 **concurrently** within the **same unified data model**. This means you can integrate today (before ENSv2 launches) and continue with full ENSv2 support when it goes live, with zero downtime!
-
-## ENSNode's Integration Options
-
-ENSNode supports a full range of different integration options across the stack, whether you're using React, any JavaScript runtime, raw GraphQL, or looking to go deep and build a fully custom service using indexed ENS data.
-
-
-
-Here's a summary of some popular integration strategies:
-
-### 1. `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
- canonical { name { interpreted } }
- owner { address }
- }
- }
-`);
-
-// `result` is fully typed!
-const result = await client.omnigraph.query({ query: HelloWorldQuery });
-```
-
-
-
-
-
-
-
-
-### 2. `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
- canonical { name { interpreted } }
- owner { id address }
+import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active";
+import QuickstartV1131 from "@components/walkthroughs/quickstart/v1.13.1.mdx";
+import QuickstartV1141 from "@components/walkthroughs/quickstart/v1.14.1.mdx";
+
+{/*
+ Version-locked to the production-deployed Omnigraph schema (see `@data/omnigraph-examples/active`).
+ Author one partial per supported version under `@components/walkthroughs/quickstart/` and render the
+ active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION (and, for a new version, add its partial and
+ a `case` below — an unmapped version throws at build rather than silently rendering stale docs).
+*/}
+
+{(() => {
+ switch (ACTIVE_OMNIGRAPH_VERSION) {
+ case "v1.13.1":
+ return ;
+ case "v1.14.1":
+ return ;
+ default:
+ throw new Error(`No quickstart partial for Omnigraph version "${ACTIVE_OMNIGRAPH_VERSION}".`);
}
-`);
-
-// 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.interpreted) : '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 } });
- const { data, fetching, error } = result;
-
- // 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 (
-
- );
-}
-```
-
-
-
-
-
-
-
-### 3. ENS Omnigraph GraphQL API
-
-The ENS 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.
-
-
-
-
-
-
-
-### 4. Further Integration Options
-
-Beyond `enssdk`, `enskit`, and the Omnigraph GraphQL API, ENSNode exposes a deeper set of integration surfaces for advanced use cases:
-
-- **ENSDb** — query the indexed ENS dataset directly over Postgres for custom analytics or your own service layer.
-- **enscli** — (coming soon) script ENSNode operations from the command line.
-- **ensskills** — (coming soon) AI agent tooling for working with ENS data.
-- **ensdb-cli** — (coming soon) share and load point-in-time ENSDb snapshots.
-- **ENSEngine** (coming soon) — subscribe to ENS-aware webhooks driven by changes in ENSDb.
-
-
+})()}
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/example.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/example.mdx
index 3ff146694a..9aef54179a 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/example.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/example.mdx
@@ -12,7 +12,7 @@ import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanc
This playground loads the same source as [`enskit-react-example`](https://github.com/namehash/ensnode/tree/main/examples/enskit-react-example): a Vite + React app with routing, domain and account browsers, registry cache, and search — powered by [`enskit`](/docs/integrate/integration-options/enskit) and the [ENS Omnigraph API](/docs/integrate/omnigraph).
-
+
:::note[First load may take a moment]
The editor runs entirely in your browser. Downloading of **enskit**, **enssdk** and their heavy dependencies may take 30-60 seconds.
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx
index 60140297c9..0bc69f4c87 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx
@@ -3,417 +3,24 @@ title: enskit
description: React toolkit for ENSv2 development, includes fully typed providers for the ENS Omnigraph API.
---
-import { LinkCard, Steps } from '@astrojs/starlight/components';
-import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro';
-
-`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.
-
-
-
-
-## 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
-```
-
-:::caution[Pin exact versions — hosted instance compatibility]
-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.
-
-**Important:** Our hosted ENSNode instances currently run ENSNode v1.13. The latest published versions of `enskit` and `enssdk` are `1.14.0+`, which contain breaking changes in the Omnigraph API data model not yet deployed to our hosted infrastructure. If you are querying a hosted instance from your own app, you **must** use `enskit@1.13.1` and `enssdk@1.13.1` to avoid type errors and runtime mismatches. This notice will be removed once the hosted instances are upgraded.
-:::
-
-## 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";
-
-// you may use a NameHash Hosted ENSNode instance
-// learn more at https://ensnode.io/docs/hosted-instances
-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 { interpreted } }
- 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 { interpreted } }
- owner { address }
- subdomains {
- edges {
- node {
- canonical { name { interpreted } }
- 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
- );
-}
-```
-
-`FragmentOf` is the opaque type for any selection that includes `...DomainFragment` — `RenderDomain` accepts any of them. `readFragment(DomainFragment, data)` unwraps that opaque type to the typed fields you declared.
-
-## 8. Paginate with "Load more"
-
-`subdomains` is a Relay Connection — page through it with the `first` and `after` arguments. Add `pageInfo { hasNextPage endCursor }` to the query, track the cursor in component state, and wire up a "Next page" button.
-
-```tsx title="src/DomainView.tsx" ins={1,6,9,11,19,23,27,51-59}
-import { useState } from "react";
-// ...other imports
-
-const DomainByNameQuery = graphql(
- `
- query DomainByName($name: InterpretedName!, $first: Int!, $after: String) {
- domain(by: { name: $name }) {
- ...DomainFragment
- subdomains(first: $first, after: $after) {
- edges { node { ...DomainFragment } }
- pageInfo { hasNextPage endCursor }
- }
- }
+import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active";
+import WalkthroughV1131 from "@components/walkthroughs/enskit/v1.13.1.mdx";
+import WalkthroughV1141 from "@components/walkthroughs/enskit/v1.14.1.mdx";
+
+{/*
+ Version-locked to the production-deployed Omnigraph schema (see `@data/omnigraph-examples/active`).
+ Author one partial per supported version under `@components/walkthroughs/enskit/` and render the
+ active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION (and, for a new version, add its partial and
+ a `case` below — an unmapped version throws at build rather than silently rendering stale docs).
+*/}
+
+{(() => {
+ switch (ACTIVE_OMNIGRAPH_VERSION) {
+ case "v1.13.1":
+ return ;
+ case "v1.14.1":
+ return ;
+ default:
+ throw new Error(`No walkthrough partial for Omnigraph version "${ACTIVE_OMNIGRAPH_VERSION}".`);
}
-`,
- [DomainFragment],
-);
-
-const PAGE_SIZE = 20;
-
-export function DomainView() {
- const name = asInterpretedName("eth");
- const [after, setAfter] = useState(null);
-
- const [result] = useOmnigraphQuery({
- query: DomainByNameQuery,
- variables: { name, first: PAGE_SIZE, after },
- });
-
- const { data, fetching, error } = result;
-
- if (!data && fetching) return
- );
-}
-```
-
-## 9. Run it
-
-```sh
-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.
-
-## Where to go next
-
-- Try the [Interactive Example](/docs/integrate/integration-options/enskit/example): edit and run the full `enskit-react-example` app in your browser with a live preview.
-- 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.
-- See [Omnigraph examples](/docs/integrate/omnigraph/examples) 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/example.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/example.mdx
index 3f9940c0f3..4e1b87f13c 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/example.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/example.mdx
@@ -12,7 +12,7 @@ import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanc
This playground loads the same source as [`enssdk-example`](https://github.com/namehash/ensnode/tree/main/examples/enssdk-example): a TypeScript script that queries the `eth` domain and lists its first 20 subdomains via [`enssdk`](/docs/integrate/integration-options/enssdk) and the [ENS Omnigraph API](/docs/integrate/omnigraph).
-
+
:::note[First load may take a few minutes]
The embedded StackBlitz editor runs entirely in your browser. Downloading and installing all npm packages may take a few minutes. Watch the install progress in the terminal of the StackBlitz editor.
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx
index 090c2a749d..2d09563c1e 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx
@@ -3,317 +3,24 @@ title: enssdk
description: SDK for ENSv2 development in TypeScript/JavaScript.
---
-import { LinkCard } from '@astrojs/starlight/components';
-import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro';
-
-`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 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).
-
-
-
-## 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
-```
-
-:::caution[Pin exact versions — hosted instance compatibility]
-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.
-
-**Important:** Our hosted ENSNode instances currently run ENSNode v1.13. The latest published version of `enssdk` is `1.14.0+`, which contains breaking changes in the Omnigraph API data model not yet deployed to our hosted infrastructure. If you are querying a hosted instance from your own app, you **must** use `enssdk@1.13.1` to avoid type errors and runtime mismatches. This notice will be removed once the hosted instances are upgraded.
-:::
-
-
-## 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" ins={10-15}
-{
- "compilerOptions": {
- "target": "ES2022",
- "module": "ESNext",
- "moduleResolution": "bundler",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "lib": ["ESNext"],
- "types": ["node"],
- "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 instance
-// learn more at https://ensnode.io/docs/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
- canonical { name { interpreted } }
- 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.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : ""}`);
- 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.
-- `canonical` is `null` for non-canonical Domains (e.g. Domains whose name cannot be inferred). When non-null, `canonical.name.interpreted` is the Domain's Canonical Name as an InterpretedName. **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-38}
-const HelloWorldQuery = graphql(`
- query HelloWorld($name: InterpretedName!) {
- domain(by: { name: $name }) {
- __typename
- canonical { name { interpreted } }
- owner { address }
- subdomains(first: 20) {
- totalCount
- edges {
- node { canonical { name { interpreted } } owner { address } }
- }
- }
- }
+import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active";
+import WalkthroughV1131 from "@components/walkthroughs/enssdk/v1.13.1.mdx";
+import WalkthroughV1141 from "@components/walkthroughs/enssdk/v1.14.1.mdx";
+
+{/*
+ Version-locked to the production-deployed Omnigraph schema (see `@data/omnigraph-examples/active`).
+ Author one partial per supported version under `@components/walkthroughs/enssdk/` and render the
+ active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION (and, for a new version, add its partial and
+ a `case` below — an unmapped version throws at build rather than silently rendering stale docs).
+*/}
+
+{(() => {
+ switch (ACTIVE_OMNIGRAPH_VERSION) {
+ case "v1.13.1":
+ return ;
+ case "v1.14.1":
+ return ;
+ default:
+ throw new Error(`No walkthrough partial for Omnigraph version "${ACTIVE_OMNIGRAPH_VERSION}".`);
}
-`);
-
-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.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : ""}`);
- console.log(`Version: ${domain.__typename}`);
- console.log(`Owner: ${domain.owner?.address ?? "0x0"}`);
-
- console.log(`\nSubdomains (showing 20 of ${domain.subdomains?.totalCount ?? 0}):`);
- for (const { node } of domain.subdomains?.edges ?? []) {
- const subName = node.canonical ? beautifyInterpretedName(node.canonical.name.interpreted) : "";
- console.log(` - ${subName} — Owner ${node.owner?.address ?? "0x0"}`);
- }
-}
-```
-
-Notice we're now writing the same canonical/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).
-
-## 7. Extract a typed fragment
-
-Notice we're selecting the same fields (`canonical { name { interpreted } }`, `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,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 instance
-// learn more at https://ensnode.io/docs/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
- canonical { name { interpreted } }
- 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.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : "";
- 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}):`);
- 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, 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.alpha.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
-
-- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy GraphQL 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/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 a328732829..169d38779a 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
@@ -3,185 +3,25 @@ title: ENS Omnigraph GraphQL API
description: Query the ENS Omnigraph API directly via HTTP from any language.
---
-import { LinkCard } from '@astrojs/starlight/components';
-import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro';
-
-The Omnigraph is **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.
-
-
-
-## 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.
-
-A minimum-viable hello world over `curl`:
-
-```sh
-curl -sS -X POST \
- -H 'Content-Type: application/json' \
- -d '{"query":"{ domain(by: { name: \"eth\" }) { canonical { name { interpreted } } owner { address } } }"}' \
- 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.
-
-## 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/hosted-instances
-const ENSNODE_URL = process.env.ENSNODE_URL!;
-
-const HELLO_WORLD_QUERY = /* GraphQL */ `
- query HelloWorld($name: InterpretedName!) {
- domain(by: { name: $name }) {
- __typename
- canonical { name { interpreted } }
- owner { address }
- subdomains(first: 20) {
- totalCount
- edges { node { __typename canonical { name { interpreted } } owner { address } } }
- }
- }
- }
-`;
-
-interface Domain {
- __typename: "ENSv1Domain" | "ENSv2Domain";
- canonical: { name: { interpreted: 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.canonical?.name.interpreted ?? "";
- 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)}`);
+import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active";
+import WalkthroughV1131 from "@components/walkthroughs/omnigraph-graphql-api/v1.13.1.mdx";
+import WalkthroughV1141 from "@components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx";
+
+{/*
+ This walkthrough is version-locked to the production-deployed Omnigraph schema (see
+ `@data/omnigraph-examples/active`). Production lags `main`, and its schema differs, so we author one
+ partial per supported version under `@components/walkthroughs/omnigraph-graphql-api/` and render the
+ active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION (and, for a new version, add its partial and
+ a `case` below — an unmapped version throws at build rather than silently rendering stale docs).
+*/}
+
+{(() => {
+ switch (ACTIVE_OMNIGRAPH_VERSION) {
+ case "v1.13.1":
+ return ;
+ case "v1.14.1":
+ return ;
+ default:
+ throw new Error(`No walkthrough partial for Omnigraph version "${ACTIVE_OMNIGRAPH_VERSION}".`);
}
-}
-
-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.alpha.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 [Omnigraph examples](/docs/integrate/omnigraph/examples) 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/docs/ensnode.io/src/content/docs/docs/integrate/unigraph/schema-reference.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/unigraph/schema-reference.mdx
index 405f4f74dc..ab75738048 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/unigraph/schema-reference.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/unigraph/schema-reference.mdx
@@ -17,20 +17,15 @@ The Unigraph plugin has a hard dependency on the **Protocol Acceleration** plugi
Defined in [`unigraph.schema.ts`](https://github.com/namehash/ensnode/blob/main/packages/ensdb-sdk/src/ensindexer-abstract/unigraph.schema.ts).
-:::note[Design principles]
-While the initial approach was a highly materialized view of the ENS protocol, abstracting away
-as many on-chain details as possible, in practice—due to the sheer complexity of the protocol at
-resolution-time—full materialization of resolution behavior is impractical. The Canonical Nametree,
-however, is materialized inline via synchronous handler-side cascades; see the `canonical*` fields
-on `domains` and `canonicality-db-helpers.ts`.
-
-As a result, this schema takes a balanced approach. It mimics on-chain state as closely as possible,
-with the obvious exception of materializing specific state that must be trivially filterable. Then,
+## Design Principles
+
+This schema takes a balanced approach to materialization: it mimics on-chain state as closely as
+possible, with the exception of materializing specific state that must be trivially filterable. Then,
resolution-time logic is applied on _top_ of this index, at query-time, mimicking ENS's own resolution-time
behavior. This forces our implementation to match the protocol as closely as possible, with the
obvious note that the performance tradeoffs of evm code and our app are different. For example,
it's more expensive for us to recursively traverse the `namegraph` (like evm code does) because our
-individual roundtrips from the db are relatively more expensive.
+individual roundtrips from the db are relatively more expensive, but can be batched.
In general: the indexed schema should match on-chain state as closely as possible, and
resolution-time behavior within the ENS protocol should _also_ be implemented at resolution time
@@ -40,6 +35,10 @@ including the ENSv1 Registry, various Registrars, and the NameWrapper. The ENSv1
within this Unigraph plugin materializes the effective owner to simplify this aspect of ENS and
enable efficient queries against `domains.owner_id`.
+Additionally, a Domain's canonicality-derived fields (`canonical_name`, `canonical_label_hash_path`,
+`canonical_path`, `canonical_depth`, `canonical_node`) are materialized to facilitate query-time
+performance.
+
When necessary, all data models are shared or polymorphic between ENSv1 and ENSv2, including
Domains, Registries, Registrations, Renewals, and Resolvers.
@@ -48,11 +47,6 @@ guarantees (for example, ENSv1 BaseRegistrar Registrations may have a gracePerio
Registry Registrations do not).
The `Label` entity (`labelHash` → `InterpretedLabel`) remains the source of truth for label values.
-Canonical-tree fields on `domains` (`canonical_name`, `canonical_label_hash_path`, `canonical_path`,
-`canonical_depth`, `canonical_node`) are materialized inline by the handlers in
-`canonicality-db-helpers.ts`. Label heals propagate to `canonical_name` via a GIN-indexed bulk
-UPDATE outside Ponder's cache; cascade round-trips are bounded to events that already pay a
-flush (canonicality flip, heal of an unknown label).
ENSv1 and ENSv2 both fit the `Registry` → `Domain` → (`Sub`)`Registry` → `Domain` → ... `namegraph` model.
For ENSv1, each domain that has children implicitly owns a "virtual" `Registry` (a row of type
@@ -62,13 +56,7 @@ the Basenames Registry, the Lineanames Registry) sit at the top. ENSv2 namegraph
a single `ENSv2Registry` RootRegistry on the ENS Root Chain and are possibly circular directed
graphs. The full namegraph is never materialized, only _navigated_ at resolution-time, with the
exception of the Canonical Nametree (the set of Domains with an inferrable Canonical Name), which
-_is_ materialized inline: the `registries.canonical` / `domains.canonical` membership flags plus the
-`domains.canonical*` name/path/depth fields on the rows themselves. The bidirectional canonical edge is NOT
-materialized in a parallel table; it is derived on demand by checking that the two unidirectional
-pointers agree (`registries.canonical_domain_id = domains.id` ↔ `domains.subregistry_id = registries.id`).
-Cascading canonicality flips through the subgraph run as either an in-memory PK update (when
-`registries.has_children = false`, the dominant case for fresh ENSv1 virtual registries on first
-wire-up) or a single recursive-CTE batch UPDATE otherwise (see `canonicality-db-helpers.ts`).
+_is_ materialized inline.
Note also that the Protocol Acceleration plugin is a hard requirement for the Unigraph plugin. This
allows us to rely on the shared logic for indexing:
@@ -91,7 +79,6 @@ entity that has many events (`domains`, `resolvers`) to the relevant set of Even
A Registration references the event that initiated the Registration. A Renewal, too, references
the Event responsible for its existence.
-:::
## Enums
diff --git a/docs/ensnode.io/src/data/omnigraph-examples/examples.ts b/docs/ensnode.io/src/data/omnigraph-examples/examples.ts
index f997236821..9e26bbb2b2 100644
--- a/docs/ensnode.io/src/data/omnigraph-examples/examples.ts
+++ b/docs/ensnode.io/src/data/omnigraph-examples/examples.ts
@@ -1,4 +1,3 @@
-import { ENSNODE_URL } from "src/lib/playground/constants";
import { OmnigraphExampleQuerySchema, type OmnigraphExampleQuery } from "src/lib/playground/types";
import { ACTIVE_OMNIGRAPH_VERSION } from "./active";
@@ -43,7 +42,8 @@ export const graphqlApiOmnigraphExamples: OmnigraphExampleQuery[] = Object.entri
query: example.query.trim(),
variables: example.variables,
...(response ? { response } : {}),
- connection: ENSNODE_URL,
+ // NOTE: always pointing at production url
+ connection: "https://api.v2-sepolia.ensnode.io",
}),
];
});
diff --git a/docs/ensnode.io/src/lib/playground/constants.ts b/docs/ensnode.io/src/lib/playground/constants.ts
index b0d41e5701..d6657a2e97 100644
--- a/docs/ensnode.io/src/lib/playground/constants.ts
+++ b/docs/ensnode.io/src/lib/playground/constants.ts
@@ -3,7 +3,6 @@ import { ENSNamespaceIds } from "@ensnode/ensnode-sdk";
/** TODO: Update all to the latest ENSNode URL */
/** Sepolia v2 namespace — matches the public v2 Sepolia ENSNode URL in docs playgrounds. */
export const DOCS_OMNIGRAPH_NAMESPACE = ENSNamespaceIds.SepoliaV2;
+
/** Heading anchor for the docs playground instance (`#### ENSNode 'v2 Sepolia'` on /docs/hosted-instances). */
export const DOCS_HOSTED_INSTANCE_ANCHOR = "ensnode-v2-sepolia";
-/** Sepolia v2 ENSNode URL — matches the public v2 Sepolia ENSNode URL in docs playgrounds. */
-export const ENSNODE_URL = "https://api.v2-sepolia.ensnode.io";
diff --git a/docs/ensnode.io/src/lib/playground/example-project/loadExampleProject.ts b/docs/ensnode.io/src/lib/playground/example-project/loadExampleProject.ts
index 29150a83fa..6f46268403 100644
--- a/docs/ensnode.io/src/lib/playground/example-project/loadExampleProject.ts
+++ b/docs/ensnode.io/src/lib/playground/example-project/loadExampleProject.ts
@@ -1,11 +1,9 @@
import { assemblePlaygroundProject } from "./assemblePlaygroundProject";
import { buildNodePlaygroundTsconfig } from "./buildPlaygroundTsconfig";
-import { replaceEnvWithValues } from "./replaceEnvWithValues";
import type { ExampleProjectConfig, PlaygroundProject } from "./types";
export function loadExampleProject(config: ExampleProjectConfig): PlaygroundProject {
const raw = config.fetchRaw();
- const transformed = replaceEnvWithValues(raw, config.envReplacements);
const { dependencies, devDependencies } = config.resolvePackageManifest();
const tsconfig = config.buildTsconfig?.() ?? buildNodePlaygroundTsconfig();
@@ -16,15 +14,11 @@ export function loadExampleProject(config: ExampleProjectConfig): PlaygroundProj
view: config.view,
entryFileName: config.entryFileName,
openFile: config.openFile,
- transformed,
+ transformed: raw,
dependencies,
devDependencies,
tsconfig,
});
- if (config.extraFiles) {
- project.files = { ...project.files, ...config.extraFiles };
- }
-
return project;
}
diff --git a/docs/ensnode.io/src/lib/playground/example-project/replaceEnvWithValues.test.ts b/docs/ensnode.io/src/lib/playground/example-project/replaceEnvWithValues.test.ts
deleted file mode 100644
index b707607132..0000000000
--- a/docs/ensnode.io/src/lib/playground/example-project/replaceEnvWithValues.test.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { replaceEnvWithValues } from "./replaceEnvWithValues";
-
-const ENSNODE_URL_ASSIGNMENT = /const ENSNODE_URL = process\.env\.ENSNODE_URL!;/;
-
-describe("replaceEnvWithValues", () => {
- it("replaces ENSNODE_URL env access with a literal", () => {
- const source = `const ENSNODE_URL = process.env.ENSNODE_URL!;\nconst x = 1;`;
- const result = replaceEnvWithValues({ files: { "src/index.ts": source } }, [
- {
- pattern: ENSNODE_URL_ASSIGNMENT,
- replacement: 'const ENSNODE_URL = "https://example.test";',
- },
- ]);
-
- expect(result.files["src/index.ts"]).not.toContain("process.env.ENSNODE_URL");
- expect(result.files["src/index.ts"]).toContain('const ENSNODE_URL = "https://example.test";');
- expect(result.files["src/index.ts"]).toContain("const x = 1;");
- });
-});
diff --git a/docs/ensnode.io/src/lib/playground/example-project/replaceEnvWithValues.ts b/docs/ensnode.io/src/lib/playground/example-project/replaceEnvWithValues.ts
deleted file mode 100644
index dac4ad9629..0000000000
--- a/docs/ensnode.io/src/lib/playground/example-project/replaceEnvWithValues.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { EnvReplacement, RawExampleProject, TransformedExampleProject } from "./types";
-
-export function replaceEnvWithValues(
- project: RawExampleProject,
- replacements: EnvReplacement[],
-): TransformedExampleProject {
- if (replacements.length === 0) {
- return project;
- }
-
- const files: Record = {};
-
- for (const [path, content] of Object.entries(project.files)) {
- let next = content;
- for (const { pattern, replacement } of replacements) {
- next = next.replace(pattern, replacement);
- }
- files[path] = next;
- }
-
- return { files };
-}
diff --git a/docs/ensnode.io/src/lib/playground/example-project/types.ts b/docs/ensnode.io/src/lib/playground/example-project/types.ts
index 9744a3fe6e..317d41af6d 100644
--- a/docs/ensnode.io/src/lib/playground/example-project/types.ts
+++ b/docs/ensnode.io/src/lib/playground/example-project/types.ts
@@ -31,11 +31,6 @@ export type PlaygroundProject = {
tsconfig?: string;
};
-export type EnvReplacement = {
- pattern: RegExp;
- replacement: string;
-};
-
export type ExampleProjectConfig = {
title: string;
description?: string;
@@ -44,9 +39,6 @@ export type ExampleProjectConfig = {
entryFileName: string;
openFile?: string;
fetchRaw: () => RawExampleProject;
- envReplacements: EnvReplacement[];
resolvePackageManifest: () => PlaygroundPackageManifest;
buildTsconfig?: () => string;
- /** Merged into project files after assembly (e.g. `.env` for Vite). */
- extraFiles?: Record;
};
diff --git a/docs/ensnode.io/src/lib/playground/loadEnskitExampleProject.test.ts b/docs/ensnode.io/src/lib/playground/loadEnskitExampleProject.test.ts
index 8183f3fa85..0a72016678 100644
--- a/docs/ensnode.io/src/lib/playground/loadEnskitExampleProject.test.ts
+++ b/docs/ensnode.io/src/lib/playground/loadEnskitExampleProject.test.ts
@@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest";
import enskitExamplePackageJson from "@workspace/examples/enskit-react-example/package.json";
-import { ENSNODE_URL } from "./constants";
import { loadEnskitExampleProject } from "./loadEnskitExampleProject";
describe("loadEnskitExampleProject", () => {
@@ -12,7 +11,6 @@ describe("loadEnskitExampleProject", () => {
expect(project.files["index.html"]).toContain('id="root"');
expect(project.files["vite.config.ts"]).toContain("@vitejs/plugin-react");
expect(project.files["src/App.tsx"]).toContain("OmnigraphProvider");
- expect(project.files[".env"]).toBe(`VITE_ENSNODE_URL=${ENSNODE_URL}\n`);
expect(Object.keys(project.dependencies).sort()).toEqual(
Object.keys(enskitExamplePackageJson.dependencies).sort(),
diff --git a/docs/ensnode.io/src/lib/playground/loadEnskitExampleProject.ts b/docs/ensnode.io/src/lib/playground/loadEnskitExampleProject.ts
index 6fef3fce7c..9fe11576f8 100644
--- a/docs/ensnode.io/src/lib/playground/loadEnskitExampleProject.ts
+++ b/docs/ensnode.io/src/lib/playground/loadEnskitExampleProject.ts
@@ -1,4 +1,3 @@
-import { ENSNODE_URL } from "./constants";
import {
fetchRawExampleProjectFromGlob,
mergeRawExampleProjects,
@@ -33,11 +32,7 @@ export function loadEnskitExampleProject(): PlaygroundProject {
fetchRawExampleProjectFromGlob(enskitExampleSourceModules, EXAMPLE_PATH_PREFIX),
fetchRawExampleProjectFromGlob(enskitExampleRootModules, EXAMPLE_PATH_PREFIX),
),
- envReplacements: [],
resolvePackageManifest: resolveEnskitExamplePackageManifest,
buildTsconfig: buildViteReactPlaygroundTsconfig,
- extraFiles: {
- ".env": `VITE_ENSNODE_URL=${ENSNODE_URL}\n`,
- },
});
}
diff --git a/docs/ensnode.io/src/lib/playground/loadEnssdkExampleProject.test.ts b/docs/ensnode.io/src/lib/playground/loadEnssdkExampleProject.test.ts
index 99c34a28e6..c7a7bae072 100644
--- a/docs/ensnode.io/src/lib/playground/loadEnssdkExampleProject.test.ts
+++ b/docs/ensnode.io/src/lib/playground/loadEnssdkExampleProject.test.ts
@@ -11,7 +11,6 @@ describe("loadEnssdkExampleProject", () => {
expect(indexSource).toBeDefined();
expect(indexSource).toContain("HelloWorldQuery");
- expect(indexSource).not.toContain("process.env.ENSNODE_URL");
expect(Object.keys(project.dependencies).sort()).toEqual(
Object.keys(enssdkExamplePackageJson.dependencies).sort(),
diff --git a/docs/ensnode.io/src/lib/playground/loadEnssdkExampleProject.ts b/docs/ensnode.io/src/lib/playground/loadEnssdkExampleProject.ts
index d7e335421a..84a2588962 100644
--- a/docs/ensnode.io/src/lib/playground/loadEnssdkExampleProject.ts
+++ b/docs/ensnode.io/src/lib/playground/loadEnssdkExampleProject.ts
@@ -1,4 +1,3 @@
-import { ENSNODE_URL } from "./constants";
import { fetchRawExampleProjectFromGlob } from "./example-project/fetchRawExampleProject";
import { loadExampleProject } from "./example-project/loadExampleProject";
import { resolveEnssdkExamplePackageManifest } from "./example-project/resolvePinnedDependencies";
@@ -22,12 +21,6 @@ export function loadEnssdkExampleProject(): PlaygroundProject {
entryFileName: "src/index.ts",
fetchRaw: () =>
fetchRawExampleProjectFromGlob(enssdkExampleSourceModules, "examples/enssdk-example"),
- envReplacements: [
- {
- pattern: /const ENSNODE_URL = process\.env\.ENSNODE_URL!;/,
- replacement: `const ENSNODE_URL = ${JSON.stringify(ENSNODE_URL)};`,
- },
- ],
resolvePackageManifest: resolveEnssdkExamplePackageManifest,
});
}
diff --git a/examples/enskit-react-example/README.md b/examples/enskit-react-example/README.md
index 894c6b5efc..62486cc920 100644
--- a/examples/enskit-react-example/README.md
+++ b/examples/enskit-react-example/README.md
@@ -8,14 +8,14 @@ This app is hosted at [https://enskit-react-example.ensnode.io/](https://enskit-
## Usage (with NameHash Hosted Instance)
-> **Version compatibility:** Our hosted ENSNode instances currently run ENSNode v1.13. If you are querying them from your own app, you **must** use `enskit@1.13.1` and `enssdk@1.13.1`. The latest published versions (`1.14.0+`) contain breaking changes in the Omnigraph API data model not yet deployed to our hosted infrastructure. This notice will be removed once the hosted instances are upgraded.
+> **Schema version:** This example tracks the latest Omnigraph schema (ENSNode 1.14.x). It connects to the `blue` hosted deployment by default, which runs `1.14.x`; the production hosted instances still serve an older schema (`1.13.x`) that wouldn't satisfy these queries. If you query a hosted instance from your own app, match its ENSNode version with the same `enskit`/`enssdk` version.
```sh
# from the ENSNode monorepo root
pnpm install
# set the VITE_ENSNODE_URL to a NameHash Hosted Instance and run this example in dev mode
-VITE_ENSNODE_URL=https://api.alpha.ensnode.io pnpm -F enskit-react-example dev
+VITE_ENSNODE_URL=https://api.v2-sepolia.blue.ensnode.io pnpm -F enskit-react-example dev
```
## Usage (with Local ENSNode)
diff --git a/examples/enskit-react-example/package.json b/examples/enskit-react-example/package.json
index 1adc33966a..08c1bc1c97 100644
--- a/examples/enskit-react-example/package.json
+++ b/examples/enskit-react-example/package.json
@@ -12,8 +12,8 @@
"generate:gqlschema": "gql.tada generate-output"
},
"dependencies": {
- "enskit": "0.0.0-preview-fix-sha-89c022b-20260519094840",
- "enssdk": "0.0.0-preview-fix-sha-89c022b-20260519094840",
+ "enskit": "workspace:*",
+ "enssdk": "workspace:*",
"react": "catalog:",
"react-dom": "catalog:",
"react-router": "^7.6.1"
diff --git a/examples/enskit-react-example/src/AccountView.tsx b/examples/enskit-react-example/src/AccountView.tsx
index 895902d796..d0c67269a4 100644
--- a/examples/enskit-react-example/src/AccountView.tsx
+++ b/examples/enskit-react-example/src/AccountView.tsx
@@ -1,10 +1,5 @@
import { graphql, useOmnigraphQuery } from "enskit/react/omnigraph";
-import {
- beautifyInterpretedName,
- isNormalizedAddress,
- type NormalizedAddress,
- toNormalizedAddress,
-} from "enssdk";
+import { isNormalizedAddress, type NormalizedAddress, toNormalizedAddress } from "enssdk";
import { useState } from "react";
import { Link, Navigate, useParams } from "react-router";
@@ -15,9 +10,7 @@ const AccountDomainsQuery = graphql(`
domains(first: $first, after: $after) {
totalCount
edges {
- # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to:
- # node { __typename id canonical { name { interpreted } } }
- node { __typename id name }
+ node { __typename id canonical { name { beautified } } }
}
pageInfo { hasNextPage endCursor }
}
@@ -62,15 +55,10 @@ function RenderAccount({ address }: { address: NormalizedAddress }) {
{domains.edges.map((edge) => (
- {/*
- TODO: after upgrading v2-sepolia to have materialized canonical name, update this to:
{edge.node.canonical ? (
-
- {beautifyInterpretedName(edge.node.canonical.name.interpreted)}
- */}
- {edge.node.name ? (
-
- {beautifyInterpretedName(edge.node.name)}
+ // link by DomainId so the exact ENSv1/ENSv2 variant the user clicked is preserved
+
+ {edge.node.canonical.name.beautified}
) : (
non-canonical domain
diff --git a/examples/enskit-react-example/src/App.tsx b/examples/enskit-react-example/src/App.tsx
index da4e26c1d4..bd10bceaa0 100644
--- a/examples/enskit-react-example/src/App.tsx
+++ b/examples/enskit-react-example/src/App.tsx
@@ -5,7 +5,7 @@ import { StrictMode } from "react";
import { HashRouter, Link, Outlet, Route, Routes } from "react-router";
import { AccountView } from "./AccountView";
-import { DomainView } from "./DomainView";
+import { DomainByIdView, DomainByNameView } from "./DomainView";
import { RegistryView } from "./RegistryView";
import { SearchView } from "./SearchView";
@@ -13,7 +13,10 @@ const EXAMPLE_ACCOUNT_ADDRESS = "0x2f8e8b1126e75fde0b7f731e7cb5847eba2d2574";
// you may use a NameHash Hosted ENSNode instance
// learn more at https://ensnode.io/docs/hosted-instances
-const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL ?? "https://api.v2-sepolia.ensnode.io";
+//
+// NOTE: we point at the `blue` deployment, which runs ENSNode 1.14.x — the version this example's
+// queries target. The production v2-sepolia instance still serves an older Omnigraph schema (1.13.x).
+const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL ?? "https://api.v2-sepolia.blue.ensnode.io";
console.log(`Connecting to ENSNode at ${ENSNODE_URL}`);
@@ -26,7 +29,7 @@ function Layout() {
return (
<>
@@ -56,7 +59,8 @@ export function App() {
}>
} />
- } />
+ } />
+ } />
} />
} />
} />
diff --git a/examples/enskit-react-example/src/DomainView.tsx b/examples/enskit-react-example/src/DomainView.tsx
index 3ea47cef1b..8aa323efa2 100644
--- a/examples/enskit-react-example/src/DomainView.tsx
+++ b/examples/enskit-react-example/src/DomainView.tsx
@@ -1,6 +1,12 @@
import { EnsureInterpretedName } from "enskit/react";
-import { type FragmentOf, graphql, readFragment, useOmnigraphQuery } from "enskit/react/omnigraph";
-import { asLiteralName, beautifyInterpretedName, type InterpretedName } from "enssdk";
+import {
+ type FragmentOf,
+ graphql,
+ readFragment,
+ useOmnigraphQuery,
+ type VariablesOf,
+} from "enskit/react/omnigraph";
+import { asLiteralName, beautifyInterpretedName, type DomainId } from "enssdk";
import { useState } from "react";
import { Link, Navigate, useParams } from "react-router";
@@ -8,21 +14,18 @@ const DomainFragment = graphql(`
fragment DomainFragment on Domain {
__typename
id
- # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to:
- # canonical { name { interpreted } }
- name
+ canonical { name { beautified } }
owner { id address }
}
`);
-const DomainByNameQuery = graphql(
+// a single query that identifies a Domain by either DomainId or its Name
+const DomainQuery = graphql(
`
- query DomainByName($name: InterpretedName!, $first: Int!, $after: String) {
- domain(by: { name: $name }) {
+ query DomainBy($by: DomainIdInput!, $first: Int!, $after: String) {
+ domain(by: $by) {
...DomainFragment
- # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to:
- # parent { canonical { name { interpreted } } }
- parent { name }
+ parent { id canonical { name { beautified } } }
subdomains(first: $first, after: $after) {
edges {
node {
@@ -40,6 +43,8 @@ const DomainByNameQuery = graphql(
[DomainFragment],
);
+type DomainBy = VariablesOf["by"];
+
const SUBDOMAINS_PAGE_SIZE = 20;
function SubdomainLink({ data }: { data: FragmentOf }) {
@@ -47,16 +52,10 @@ function SubdomainLink({ data }: { data: FragmentOf }) {
return (
- {domain.name ? (
- {beautifyInterpretedName(domain.name)}
- // TODO: after upgrading v2-sepolia to have materialized canonical name, update this to:
- // {domain.canonical ? (
- //
- // {beautifyInterpretedName(domain.canonical.name.interpreted)}
- //
- ) : (
- non-canonical domain
- )}{" "}
+ {/* link by DomainId so the exact Domain (and its ENSv1/ENSv2 variant) is preserved */}
+
+ {domain.canonical?.name.beautified ?? non-canonical domain}
+ {" "}
({domain.__typename})
{" "}
@@ -69,30 +68,29 @@ function SubdomainLink({ data }: { data: FragmentOf }) {
);
}
-function RenderDomain({ name }: { name: InterpretedName }) {
+function RenderDomain({ by }: { by: DomainBy }) {
const [after, setAfter] = useState(null);
const [result] = useOmnigraphQuery({
- query: DomainByNameQuery,
- variables: { name, first: SUBDOMAINS_PAGE_SIZE, after },
+ query: DomainQuery,
+ variables: { by, first: SUBDOMAINS_PAGE_SIZE, after },
});
const { data, fetching, error } = result;
if (!data && fetching) return
Loading...
;
if (error) return
Error: {error.message}
;
- if (!data?.domain) return
No domain was found with name '{beautifyInterpretedName(name)}'.
;
+ if (!data?.domain) {
+ const reference = "id" in by ? `id '${by.id}'` : `name '${beautifyInterpretedName(by.name)}'`;
+ return
Owner:{" "}
{domain.owner ? (
@@ -105,17 +103,13 @@ function RenderDomain({ name }: { name: InterpretedName }) {
Version: {domain.__typename}
- {/*
- TODO: after upgrading v2-sepolia to have materialized canonical name, update this to:
- {data.domain.parent?.canonical && (
-
- ← {beautifyInterpretedName(data.domain.parent.canonical.name.interpreted)}
-
- )}
- */}
- {data.domain.parent?.name && (
-
- ← {beautifyInterpretedName(data.domain.parent.name)}
+ {data.domain.parent && (
+ // always link the parent by its (stable) DomainId; fall back to the id as the label when the
+ // parent has no Canonical Name
+
+ ←{" "}
+ {data.domain.parent.canonical?.name.beautified ??
+ `non-canonical parent domain with id '${data.domain.parent.id}'`}
)}
@@ -151,56 +145,53 @@ function RenderDomain({ name }: { name: InterpretedName }) {
);
}
-export function DomainView() {
- const params = useParams();
-
- // if a user accesses '/domain' directly, redirect to '/domain/eth'
- // TODO: render the set of tlds
- if (params.name === undefined || params.name === "") return ;
+// Identify a Domain by its Name (`/domain/name/:name`).
+// Resolves to the name's Canonical Domain.
+export function DomainByNameView() {
+ // the `/domain/name/:name` route guarantees `:name` is present
+ const { name } = useParams() as { name: string };
- // here we ensure that the provided /domain/:name parameter is an InterpretedName
+ // here we ensure that the provided /domain/name/:name parameter is an InterpretedName
return (
- <>
-
- Heads up! sepolia-v2's ENSv1Resolver is misconfigured, and ENSv1-only names aren't
- resolvable, so they're not currently visible here! This will be fixed by the ENS Team in the
- near future. If you followed a link to a Domain and it isn't showing up here, it's likely an
- ENSv1-only name (unmigrated) and isn't currently resolvable.
-
- }
- //
- // this name can't conform to InterpretedName nor can it be coerced: it is malformed: show an error
- malformed={(name) => (
-
-
Invalid name: '{name}'
- Back to 'eth' Domain.
-
- )}
- >
- {(name) => }
-
- >
+ }
+ //
+ // this name can't conform to InterpretedName nor can it be coerced: it is malformed: show an error
+ malformed={(name) => (
+
+
Invalid name: '{name}'
+ Back to 'eth' Domain.
+
+ )}
+ >
+ {(name) => }
+
);
}
+
+// Renders a Domain by its DomainId (`/domain/id/:id`).
+// This is the preferred link target when a stable DomainId is already in hand.
+export function DomainByIdView() {
+ // the `/domain/id/:id` route guarantees `:id` is present; a DomainId is an opaque, stable
+ // identifier, so it requires no normalization
+ const { id } = useParams() as { id: DomainId };
+
+ return ;
+}
diff --git a/examples/enskit-react-example/src/SearchView.tsx b/examples/enskit-react-example/src/SearchView.tsx
index a3b64219c7..ec963dc8f9 100644
--- a/examples/enskit-react-example/src/SearchView.tsx
+++ b/examples/enskit-react-example/src/SearchView.tsx
@@ -1,15 +1,12 @@
import { graphql, useOmnigraphQuery } from "enskit/react/omnigraph";
-import { beautifyInterpretedName } from "enssdk";
import { useEffect, useState } from "react";
import { Link, useSearchParams } from "react-router";
const DomainsByNameQuery = graphql(`
query DomainsByName($name: String!, $first: Int!, $after: String) {
- domains(where: { name: $name }, first: $first, after: $after) {
+ domains(where: { name: { starts_with: $name } }, first: $first, after: $after) {
edges {
- # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to:
- # node { __typename id canonical { name { interpreted } } }
- node { __typename id name }
+ node { __typename id canonical { name { beautified } } }
}
pageInfo {
hasNextPage
@@ -64,14 +61,18 @@ export function SearchView() {
const { data, fetching, error } = result;
+ // only Canonical Domains are rendered, so filter before computing the empty state — otherwise a
+ // page of entirely non-canonical edges would render as a blank list with no "No matches."
+ const visibleEdges = data?.domains?.edges.filter((edge) => edge.node.canonical !== null) ?? [];
+
return (
Domain Search
- Showcases live querying via Query.domains(where: {"{ name }"}). Only{" "}
- Canonical Domains are rendered. Input is debounced by {DEBOUNCE_MS}ms and synced to
- the URL as ?query=.
+ Showcases live querying via Query.domains(where: {"{ name: { starts_with } }"})
+ . Only Canonical Domains are rendered. Input is debounced by {DEBOUNCE_MS}ms and
+ synced to the URL as ?query=.
({edge.node.__typename === "ENSv1Domain" ? "v1" : "v2"}){" "}
-
- {beautifyInterpretedName(edge.node.name)}
- {/*
- TODO: after upgrading v2-sepolia to have materialized canonical name, update this to:
-
- {beautifyInterpretedName(edge.node.canonical.name.interpreted)}
- */}
+ {/* link by DomainId so the exact ENSv1/ENSv2 variant the user clicked is preserved */}
+
+ {edge.node.canonical?.name.beautified}