Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,22 +119,46 @@ export const integrateSidebarTopic = {
label: "Overview",
link: "/docs/integrate/unigraph",
},
{
label: "Core Concepts",
link: "/docs/integrate/unigraph/concepts",
},
{
label: "Examples",
collapsed: true,
items: [
{
label: "Overview",
label: "Connect",
link: "/docs/integrate/unigraph/examples",
},
{
label: "Domain by Name",
link: "/docs/integrate/unigraph/examples/domain-by-name",
},
{
label: "Domain Fuzzy Search",
link: "/docs/integrate/unigraph/examples/domains-fuzzy-search-by-name",
},
{
label: "Domain Events",
link: "/docs/integrate/unigraph/examples/domain-events",
},
{
label: "Subdomains",
link: "/docs/integrate/unigraph/examples/subdomains-by-parent-name",
},
{
label: "Account Domains",
link: "/docs/integrate/unigraph/examples/account-domains",
},
{
label: "Latest Registrations",
link: "/docs/integrate/unigraph/examples/latest-registrations",
},
{
label: "Expiring Registrations",
link: "/docs/integrate/unigraph/examples/expiring-registrations",
},
{
label: "Indexing Status",
link: "/docs/integrate/unigraph/examples/indexing-status",
Expand Down Expand Up @@ -180,7 +204,7 @@ export const integrateSidebarTopic = {
link: "/docs/integrate/integration-options/ensdb-readers",
},
{
label: "ENSNode Plugins (data models)",
label: "ENSNode Plugins (Data Models)",
link: "/docs/integrate/integration-options/ensnode-plugins",
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import { Aside } from "@astrojs/starlight/components";
href="/docs/services/ensdb/concepts/glossary#ensdb-writer-schema"
><code>ensIndexerSchema</code>
schema definition</a
> in the <a href="/docs/integrate/unigraph/examples">Connect</a> section if you haven't already.
> in the <a href="/docs/integrate/unigraph/examples">Connect</a> example if you haven't already.
Comment thread
tk-o marked this conversation as resolved.
</Aside>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
import { Aside } from "@astrojs/starlight/components";
---

<Aside type="tip" title="What is the ENS Unigraph?">
Explore an <a href="/docs/integrate/unigraph">overview</a> and <a
href="/docs/integrate/unigraph/concepts">core concepts</a
> to learn about the ENS Unigraph.
</Aside>
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const defaultResultNote =

<SqlResultTable rows={sql.result} />

<StaticExampleNote class="px-5 pb-5">{sql.resultNote ?? defaultResultNote}</StaticExampleNote>
<StaticExampleNote class="px-5 pb-5" set:html={sql.resultNote ?? defaultResultNote} />
Comment thread
tk-o marked this conversation as resolved.
Comment thread
vercel[bot] marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
Comment thread
vercel[bot] marked this conversation as resolved.
Comment thread
vercel[bot] marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
</UnigraphExampleSQLTab>

<UnigraphExampleEnsDbSdkTab>
Expand All @@ -54,8 +54,6 @@ const defaultResultNote =

<SqlResultTable rows={ensDbSdk.result} />

<StaticExampleNote class="px-5 pb-5">
{ensDbSdk.resultNote ?? defaultResultNote}
</StaticExampleNote>
<StaticExampleNote class="px-5 pb-5" set:html={ensDbSdk.resultNote ?? defaultResultNote} />
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
</UnigraphExampleEnsDbSdkTab>
</UnigraphExampleWrapper>
6 changes: 3 additions & 3 deletions docs/ensnode.io/src/content/docs/docs/integrate/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,9 @@ Same query: `address -> primary name -> forward profile` — via raw GraphQL wit
Beyond [`enssdk`](/docs/integrate/integration-options/enssdk), [`enskit`](/docs/integrate/integration-options/enskit), and the [Omnigraph GraphQL API](/docs/integrate/integration-options/omnigraph-graphql-api), ENSNode exposes a deeper set of integration surfaces for advanced use cases:

- **[ENSDb (SQL)](/docs/integrate/integration-options/ensdb)** — query the indexed ENSv1 and ENSv2 datasets directly via SQL for custom analytics or your own service layer, from any language with a Postgres driver.
- **[ENSDb Writers (Indexers)](/docs/integrate/integration-options/ensdb-writers)** — build your own ENSDb Writer to index ENS data into your own ENSDb instance.
- **[ENSDb Readers](/docs/integrate/integration-options/ensdb-readers)** — build your own ENSDb Reader to query your own ENSDb instance through any interface of your choice.
- **[ENSNode Plugins](/docs/integrate/integration-options/ensnode-plugins)** — define how onchain data should be indexed into ENSDb.
- **[ENSDb Writers (Indexers)](/docs/integrate/integration-options/ensdb-writers)** — enable all other layers of the ENSNode stack to build on your custom indexing engine.
- **[ENSDb Readers (Custom APIs)](/docs/integrate/integration-options/ensdb-readers)** — build your own custom APIs and services on top of ENSDb using any programming language or framework.
- **[ENSNode Plugins (Indexed Data Models)](/docs/integrate/integration-options/ensnode-plugins)** — define how onchain data should be indexed into ENSDb.
- **[enscli (CLI)](/docs/integrate/integration-options/enscli)** — resolve names, look up records, and run ad-hoc Omnigraph queries from the terminal — built for humans and AI agents alike.
- **[ensskills (AI agents)](/docs/integrate/integration-options/ensskills)** — a curated set of skills that gives AI coding agents a well-defined contract for working with ENS.
- **[ensdb-cli (ENSDb Snapshots)](/docs/integrate/integration-options/ensdb-cli)** — bootstrap a fresh ENSDb in minutes from portable, versioned snapshots instead of waiting days on a full historical backfill.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: ENS Unigraph Core Concepts
description: The core concepts behind the ENS Unigraph indexed data model.
---

## Indexed Data Model

The ENS Unigraph indexed data model is available only when the [`unigraph` plugin](/docs/integrate/integration-options/ensnode-plugins#existing-plugins) is activated in your ENSNode instance.

:::caution[`unigraph` plugin required]
Performing SQL queries on the ENS Unigraph requires that you have the `unigraph` plugin activated in your ENSNode instance. [Learn more](/docs/services/ensindexer/usage/configuration) about activating [ENSNode Plugins](/docs/integrate/integration-options/ensnode-plugins).
:::

## Canonical Nametree

The **Canonical Nametree** is the set of all Domains that have an inferrable **Canonical Name** — materialized from the namegraph. For every Domain in it, the canonical fields are populated — `canonical_name`, `canonical_path`, `canonical_node`, and `canonical_depth` — across both ENSv1 and ENSv2. That means you can look a name up by `canonical_name = 'vitalik.eth'`, order by `canonical_depth`, or walk a name's path **without branching by `type` on protocol version** and without traversing the namegraph yourself.

Multichain coverage is part of the same model: Basenames (`.base.eth`), Lineanames (`.linea.eth`), and 3DNS names (`.box`) are materialized into the same Unigraph as mainnet `.eth`, so a single query spans every indexable name.

:::danger[Not a substitute for ENS Resolution]
The Unigraph mirrors onchain state; ENS's resolution-time behavior is applied _on top_ of it by
[ENSApi](/docs/services/ensapi). A direct SQL read of resolver records is therefore **not**
ENSIP-10 / CCIP-Read compliant and **cannot** be used as a source of truth for name resolution.
For correct resolution, use the [ENS Omnigraph API](/docs/integrate/omnigraph) (which adds ENS
Protocol Acceleration on top of the Unigraph). Use Unigraph SQL for analytics, discovery, and
custom indexing.
:::

## Query optimizations

### Domains

A `canonical_name` can be very long, as it's the full, correct name. Due to limitations in Postgres btree indexes, the `domains` table has the `canonical_name` column and also the `__canonical_name_prefix` column (the first 64 code points of `canonical_name`, backed by a GIN trigram index).

:::tip[Searching vs. displaying]
Always **select and display `canonical_name`**. When you need to **search** by prefix (`ILIKE 'vit%'`, case-insensitive to match the Omnigraph `starts_with` filter), match against the materialized `__canonical_name_prefix` column (the first 64 code points of `canonical_name`, backed by a GIN trigram index) so the `ILIKE` filter is index-backed:

```sql
SELECT id, type, canonical_name, canonical_node, owner_id
FROM ensindexer_0.domains
WHERE __canonical_name_prefix ILIKE 'vit%'
ORDER BY __canonical_name_prefix
LIMIT 10;
```

The `SELECT` still returns `canonical_name`; only the `ILIKE` / `ORDER BY` use the prefix. The GIN trigram index backs the `ILIKE` filter; the `ORDER BY` then sorts the matched set (cheap under a small `LIMIT`) — scope the query by `registry_id` to use the `(registry_id, __canonical_name_prefix, id)` btree for fully index-backed ordering. For exact matches, use `canonical_name` directly (`canonical_name = 'vitalik.eth'`).
:::
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,13 @@ sidebar:
import UnigraphStaticExample from "@components/molecules/unigraph-static-example/UnigraphStaticExample.astro";
import EnsDbWriterSchemaIntro from "@components/molecules/EnsDbWriterSchemaIntro.astro";
import EnsDbReaderIntro from "@components/molecules/EnsDbReaderIntro.astro";
import { exampleAccountDomains } from "@data/unigraph-examples/account-domains";

export const resultJson = [
{
type: "ENSv1Domain",
count: 2
},
{
type: "ENSv2Domain",
count: 14
}
];

export const sqlExampleSnippet = `SELECT type, count(*) FROM ensindexer_0.domains
WHERE owner_id = '0x70997970c51812dc3a010c7d01b50e0d17dc79c8'
GROUP BY type;
`;

export const tsExampleSnippet = `import { count, eq } from "drizzle-orm";

const counts = await ensDb
\t.select({ type: ensIndexerSchema.domain.type, count: count() })
\t.from(ensIndexerSchema.domain)
\t.where(eq(ensIndexerSchema.domain.ownerId, "0x70997970c51812dc3a010c7d01b50e0d17dc79c8"))
\t.groupBy(ensIndexerSchema.domain.type);
`

:::caution[`unigraph` plugin required]
Performing SQL queries on the ENS Unigraph requires that you have the `unigraph` plugin activated in your ENSNode instance. [Learn more](/docs/services/ensindexer/usage/configuration)
:::

Count the Domains owned by an address, grouped by Domain `type` (`ENSv1Domain` vs `ENSv2Domain`) — a single query spanning both protocol versions. See [Connect](/docs/integrate/unigraph/examples) for setup.
Fetch the Domains owned by an address (across both ENSv1 and ENSv2), including each Domain's `type` (`ENSv1Domain` vs `ENSv2Domain`). See [Connect](/docs/integrate/unigraph/examples) for setup.
Comment thread
tk-o marked this conversation as resolved.

<UnigraphStaticExample
sql={{
codeSnippet: sqlExampleSnippet,
result: resultJson,
}}
ensDbSdk={{
codeSnippet: tsExampleSnippet,
result: resultJson,
}}
sql={exampleAccountDomains.sql}
ensDbSdk={exampleAccountDomains.sdk}
>
<EnsDbWriterSchemaIntro ensDbWriterSchemaName="ensindexer_0" slot="sql-tab-note"/>
<EnsDbReaderIntro slot="ensdb-sdk-tab-note"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,79 +8,14 @@ sidebar:
import UnigraphStaticExample from "@components/molecules/unigraph-static-example/UnigraphStaticExample.astro";
import EnsDbWriterSchemaIntro from "@components/molecules/EnsDbWriterSchemaIntro.astro";
import EnsDbReaderIntro from "@components/molecules/EnsDbReaderIntro.astro";

export const unigraphSqlResultJson = [
{
id: "1-0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e-0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835",
type: "ENSv1Domain",
canonical_name: "vitalik.eth",
canonical_node: "0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835",
owner_id: "0x220866b1a2219f40e72f5c628b65d54268ca3a9d",
},
];

export const ensDbSdkResultJson = [
{
id: "1-0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e-0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835",
type: "ENSv1Domain",
canonicalName: "vitalik.eth",
canonicalNode: "0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835",
ownerId: "0x220866b1a2219f40e72f5c628b65d54268ca3a9d",
},
];

export const sqlExampleSnippet = `SELECT
id,
type,
canonical_name,
canonical_node,
owner_id
FROM ensindexer_0.domains
WHERE canonical_name = 'vitalik.eth';`

export const tsExampleSnippet = `import { eq } from "drizzle-orm";

const [vitalik] = await ensDb
\t.select({
\t\tid: ensIndexerSchema.domain.id,
\t\ttype: ensIndexerSchema.domain.type,
\t\tcanonicalName: ensIndexerSchema.domain.canonicalName,
\t\tcanonicalNode: ensIndexerSchema.domain.canonicalNode,
\t\townerId: ensIndexerSchema.domain.ownerId,
\t})
\t.from(ensIndexerSchema.domain)
\t.where(eq(ensIndexerSchema.domain.canonicalName, "vitalik.eth"));

console.log(vitalik);`

:::caution[`unigraph` plugin required]
Performing SQL queries on the ENS Unigraph requires that you have the `unigraph` plugin activated in your ENSNode instance. [Learn more](/docs/services/ensindexer/usage/configuration)
:::
import { exampleDomainByName } from "@data/unigraph-examples/domain-by-name";

Fetch a Domain by its canonical name. Because `canonical_name` is materialized across both ENSv1 and ENSv2, the same lookup works regardless of protocol version. See [Connect](/docs/integrate/unigraph/examples) for setup.

:::tip[Searching vs. displaying]
A `canonical_name` can be very long, but it's the full, correct name — always **select and display `canonical_name`**. When you need to **search** by prefix (`ILIKE 'vit%'`, case-insensitive to match the Omnigraph `starts_with` filter), match against the materialized `__canonical_name_prefix` column (the first 64 code points of `canonical_name`, backed by a GIN trigram index) so the `ILIKE` filter is index-backed:

```sql
SELECT id, type, canonical_name, canonical_node, owner_id
FROM ensindexer_0.domains
WHERE __canonical_name_prefix ILIKE 'vit%'
ORDER BY __canonical_name_prefix
LIMIT 10;
```

The `SELECT` still returns `canonical_name`; only the `ILIKE` / `ORDER BY` use the prefix. The GIN trigram index backs the `ILIKE` filter; the `ORDER BY` then sorts the matched set (cheap under a small `LIMIT`) — scope the query by `registry_id` to use the `(registry_id, __canonical_name_prefix, id)` btree for fully index-backed ordering. For exact matches, use `canonical_name` directly (`canonical_name = 'vitalik.eth'`).
:::

:::note[Canonical fields]
Canonical fields are populated on every Domain reachable from the canonical root, across both ENSv1 and ENSv2 — query them uniformly without branching by `type`. In SQL, these columns are `canonical_name`, `canonical_path`, `canonical_node`, and `canonical_depth`; in `ensdb-sdk`, the corresponding fields are `canonicalName`, `canonicalPath`, `canonicalNode`, and `canonicalDepth`.
:::

<UnigraphStaticExample
sql={{ codeSnippet: sqlExampleSnippet, result: unigraphSqlResultJson }}
ensDbSdk={{ codeSnippet: tsExampleSnippet, result: ensDbSdkResultJson }}
sql={exampleDomainByName.sql}
ensDbSdk={exampleDomainByName.sdk}
>
<EnsDbWriterSchemaIntro ensDbWriterSchemaName="ensindexer_0" slot="sql-tab-note"/>
<EnsDbReaderIntro slot="ensdb-sdk-tab-note"/>
</UnigraphStaticExample>
</UnigraphStaticExample>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
title: Domain Events
description: Fetch recent events for a Domain from the ENS Unigraph by its canonical name.
sidebar:
label: Domain Events
---

import UnigraphStaticExample from "@components/molecules/unigraph-static-example/UnigraphStaticExample.astro";
import EnsDbWriterSchemaIntro from "@components/molecules/EnsDbWriterSchemaIntro.astro";
import EnsDbReaderIntro from "@components/molecules/EnsDbReaderIntro.astro";
import { exampleDomainEvents } from "@data/unigraph-examples/domain-events";

Fetch recent events for a Domain by its canonical name. This example joins the `events`, `domain_events`, and `domains` tables to retrieve domain events associated with the `vitalik.eth` name. See [Connect](/docs/integrate/unigraph/examples) for setup.

:::caution[Raw event data]
The `events` table contains low-level onchain event data, which may require additional processing to interpret. For example, the `data` field is a hex string of ABI-encoded event data that may need to be decoded based on the event type (see `selector` field) to extract meaningful details about the event.

In the future, the ENS Unigraph may include data referencing higher-level event types (e.g., `DomainRegistered`, `DomainTransferred`, etc.) to simplify querying for common events without needing to parse raw event data. For now, use `selector`, `topics`, and `data` fields in the `events` table to filter and interpret events based on your app's needs.
:::

<UnigraphStaticExample
sql={exampleDomainEvents.sql}
ensDbSdk={exampleDomainEvents.sdk}
>
<EnsDbWriterSchemaIntro ensDbWriterSchemaName="ensindexer_0" slot="sql-tab-note"/>
<EnsDbReaderIntro slot="ensdb-sdk-tab-note"/>
</UnigraphStaticExample>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: Fuzzy Search for ENS Names
description: Perform a fuzzy search for ENS names.
sidebar:
label: Fuzzy Search
---

import UnigraphStaticExample from "@components/molecules/unigraph-static-example/UnigraphStaticExample.astro";
import EnsDbWriterSchemaIntro from "@components/molecules/EnsDbWriterSchemaIntro.astro";
import EnsDbReaderIntro from "@components/molecules/EnsDbReaderIntro.astro";
import { exampleDomainsFuzzySearchByName } from "@data/unigraph-examples/domains-fuzzy-search-by-name";

Fetch Domains with names similar to a query string, ranked by similarity. This example uses PostgreSQL's `pg_trgm` extension, which provides the `%` operator for fuzzy matching and the `similarity()` function for ranking results. See [Connect](/docs/integrate/unigraph/examples) for setup.

<UnigraphStaticExample
sql={exampleDomainsFuzzySearchByName.sql}
ensDbSdk={exampleDomainsFuzzySearchByName.sdk}
>
<EnsDbWriterSchemaIntro ensDbWriterSchemaName="ensindexer_0" slot="sql-tab-note"/>
<EnsDbReaderIntro slot="ensdb-sdk-tab-note"/>
</UnigraphStaticExample>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: Expiring ENS Registrations
description: Fetch ENS registrations that are about to expire.
sidebar:
label: Expiring Registrations
---

import UnigraphStaticExample from "@components/molecules/unigraph-static-example/UnigraphStaticExample.astro";
import EnsDbWriterSchemaIntro from "@components/molecules/EnsDbWriterSchemaIntro.astro";
import EnsDbReaderIntro from "@components/molecules/EnsDbReaderIntro.astro";
import { exampleExpiringRegistrations } from "@data/unigraph-examples/expiring-registrations";

Fetch Domains with registrations expiring within a certain timeframe. This example uses a simple `WHERE` clause to filter for Domains with `expiry` between the current time and a specified future time (e.g., 3 days from now). See [Connect](/docs/integrate/unigraph/examples) for setup.

<UnigraphStaticExample
sql={exampleExpiringRegistrations.sql}
ensDbSdk={exampleExpiringRegistrations.sdk}
>
<EnsDbWriterSchemaIntro ensDbWriterSchemaName="ensindexer_0" slot="sql-tab-note"/>
<EnsDbReaderIntro slot="ensdb-sdk-tab-note"/>
</UnigraphStaticExample>
Loading
Loading