From 00befeb8995f7ecbef04de15ddf71cf28ce4d3eb Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 16 Apr 2026 17:49:13 +0200 Subject: [PATCH 1/3] Initial docs Please consider it still being work in progress. Many more refinements to be applied. --- .../config/integrations/starlight.ts | 27 +- docs/ensnode.io/mindmap.md | 80 ++ .../docs/ensdb/concepts/architecture.mdx | 365 +++++ .../docs/ensdb/concepts/database-schemas.mdx | 1216 +++++++++++++++++ .../content/docs/ensdb/concepts/glossary.mdx | 219 +++ .../src/content/docs/ensdb/concepts/index.mdx | 63 + .../ensdb/concepts/indexing-lifecycle.mdx | 170 +++ .../src/content/docs/ensdb/index.mdx | 261 +++- .../content/docs/ensdb/integrations/index.mdx | 289 ++++ .../docs/ensdb/integrations/reader.mdx | 596 ++++++++ .../docs/ensdb/integrations/writer.mdx | 452 ++++++ .../content/docs/ensdb/operations/index.mdx | 324 +++++ .../docs/ensdb/usage/ensdb-sdk/index.mdx | 101 ++ .../docs/ensdb/usage/ensdb-sdk/reader.mdx | 160 +++ .../docs/ensdb/usage/ensdb-sdk/writer.mdx | 146 ++ .../src/content/docs/ensdb/usage/index.mdx | 137 ++ .../src/content/docs/ensdb/usage/querying.mdx | 290 ++++ .../content/docs/ensdb/use-cases/index.mdx | 502 +++++++ docs/ensnode.io/src/styles/starlight.css | 54 +- 19 files changed, 5428 insertions(+), 24 deletions(-) create mode 100644 docs/ensnode.io/mindmap.md create mode 100644 docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/concepts/glossary.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/concepts/index.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/concepts/indexing-lifecycle.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/integrations/index.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/integrations/reader.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/integrations/writer.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/operations/index.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/index.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/reader.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/writer.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/querying.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/use-cases/index.mdx diff --git a/docs/ensnode.io/config/integrations/starlight.ts b/docs/ensnode.io/config/integrations/starlight.ts index ba0bdc30f3..b55c260b12 100644 --- a/docs/ensnode.io/config/integrations/starlight.ts +++ b/docs/ensnode.io/config/integrations/starlight.ts @@ -118,11 +118,36 @@ export function starlight(): AstroIntegration { label: "Overview", items: [ { - label: "Coming soon", + label: "Getting started", link: "/ensdb", }, ], }, + { + label: "Concepts", + collapsed: false, + autogenerate: { directory: "ensdb/concepts" }, + }, + { + label: "Usage", + collapsed: false, + autogenerate: { directory: "ensdb/usage" }, + }, + { + label: "Use Cases", + collapsed: false, + autogenerate: { directory: "ensdb/use-cases" }, + }, + { + label: "Integrations", + collapsed: true, + autogenerate: { directory: "ensdb/integrations" }, + }, + { + label: "Operations", + collapsed: true, + autogenerate: { directory: "ensdb/operations" }, + }, ], }, { diff --git a/docs/ensnode.io/mindmap.md b/docs/ensnode.io/mindmap.md new file mode 100644 index 0000000000..b9f5962e28 --- /dev/null +++ b/docs/ensnode.io/mindmap.md @@ -0,0 +1,80 @@ +```mermaid +mindmap + root((ENSDb)) + Vision + Bi-directional ENS integration + Writers provide data about ENS + ENSIndexer indexes onchain data for ENS + Readers query data about ENS + ENSAwards queries ENS referrals to build dashboards + ENSApi queries data about resolver records to enable Protocol Acceleration + + Foundations + ENSDb instance + Database Schema + Ponder Schema + Exactly one per ENSDb instance + Schema Lifecycle managed by Ponder runtime + Shared + Among apps writing to the ENSDb instances + ENSIndexer instances + Among apps reading from the ENSDb instance + ENSApi instances + ENSNode Schema + Exactly one per ENSDb instance + Schema Lifecycle managed by ENSIndexer runtime + Shared + Among apps writing to the ENSDb instances + ENSIndexer instances + Among apps reading from the ENSDb instance + ENSApi instances + ENSIndexer Schema + At least one per ENSDb instance + Schema Lifecycle managed by Ponder runtime + Isolated writer + A single ENSIndexer can write data + Shared readers + Any number of readers can read data + + Operations + Infrastructure + Database Server + Serves at least one ENSDb instance + Writers + ENSIndexer instance + Writes into the ENSDb instance + Cached RPC requests + Ponder Schema + Indexed ENS onchain data + ENSIndexer Schema + ENSNode Metadata + ENSNode Schema + Readers + ENSApi instance + Reads from the ENSDb instance + Indexed ENS onchain data + ENSNode Metadata + Provides powerful APIs for querying data about ENS + Possible future ENS primitives + Event notifications + Cache invalidations + Advanced Dashboards + + Tools + Snapshot tool + Capabilities + Takes a snapshot of a selected ENSDb instance + Restores the ENSDb instance from the snapshot + Goals + Cut the cost of RPC requests + Cut the time needed to complete indexing from scratch + + Integrations + TypeScript + ENSDb SDK + ENSDb Reader + Drizzle client + ENSDb Writer + Any tech-stack + PostgreSQL client +``` diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx new file mode 100644 index 0000000000..51883e47e2 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx @@ -0,0 +1,365 @@ +--- +title: ENSDb Architecture +description: How ENSDb works as a bi-directional open standard, including the writer/reader pattern, PostgreSQL server/instance architecture, and data flow. +sidebar: + label: Architecture + order: 3 +--- + +import { Aside } from '@astrojs/starlight/components'; + +ENSDb is a **bi-directional open standard** for ENS integration. This page explains the architecture: how data flows from onchain to your applications, and how ENSDb instances are served from PostgreSQL servers. + +For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). + +## The ENSDb Open Standard + +ENSDb defines a standard way to store and query ENS data in a PostgreSQL database. The standard is **implementation-agnostic** — anyone can build writers, readers, or both. + +### Key Concepts + +| Term | Definition | +|------|------------| +| **ENSDb** | An open standard defining schemas, rules, and constraints for ENS data in a PostgreSQL database | +| **ENSDb Instance** | A PostgreSQL database that follows the ENSDb standard | +| **PostgreSQL Server** | A running PostgreSQL process that can serve multiple ENSDb instances (databases) | + +### Bi-Directional Integration Pattern + +```mermaid +flowchart TB + subgraph WriteSide["Write Side (Any Implementation)"] + Onchain["Onchain ENS State
Ethereum / L2s"] + Writers["Writers"] + EI[ENSIndexer] + CW[Custom Writer] + FW[Future Writers] + end + + subgraph ENSDb["ENSDb Standard
(PostgreSQL Database)"] + PS[(Ponder Schema)] + NS[(ENSNode Schema)] + IS[(ENSIndexer Schema)] + end + + subgraph ReadSide["Read Side (Any Implementation)"] + Readers["Readers"] + EA[ENSApi] + CR[Custom API] + DA[Dashboard] + CLI[CLI Tool] + end + + Onchain -->|Index| Writers + EI -->|Write| ENSDb + CW -->|Write| ENSDb + FW -->|Write| ENSDb + + ENSDb -->|Query| Readers + ENSDb -->|Query| EA + ENSDb -->|Query| CR + ENSDb -->|Query| DA + ENSDb -->|Query| CLI +``` + +**Writers** index onchain ENS data and write to ENSDb. **Readers** query ENSDb and serve data to applications. Both can be built in any programming language with PostgreSQL support. + +## PostgreSQL Server vs ENSDb Instance + +Understanding the relationship between servers and instances is key to deploying ENSDb: + +### [PostgreSQL Server](/ensdb/concepts/glossary#postgresql-server) + +A **[PostgreSQL server](/ensdb/concepts/glossary#postgresql-server)** is a running PostgreSQL process that can host multiple databases: + +```mermaid +flowchart TB + subgraph PGServer["PostgreSQL Server (localhost:5432)"] + direction TB + Mainnet["ensdb_mainnet
← ENSDb instance: Production environment"] + Testnet["ensdb_testnet
← ENSDb instance: Pre-production environment"] + Devnet["ensdb_devnet
← ENSDb instance: Staging / local development"] + Other["other_db
← Non-ENSDb database"] + end +``` + +### [ENSDb Instance](/ensdb/concepts/glossary#ensdb-instance) + +An **[ENSDb instance](/ensdb/concepts/glossary#ensdb-instance)** is a single PostgreSQL database that follows the [ENSDb](/ensdb/concepts/glossary#ensdb) standard: + +```mermaid +flowchart TB + subgraph PGServer["PostgreSQL Server"] + direction TB + + subgraph Instance1["ENSDb Instance: ensdb_mainnet"] + Ponder1["ponder_sync
(Ponder Schema)"] + ENSNode1["ensnode
(ENSNode Schema)"] + Indexer1["ensindexer_mainnet
(ENSIndexer Schema)"] + end + + subgraph Instance2["ENSDb Instance: ensdb_testnet"] + Ponder2["ponder_sync
(Ponder Schema)"] + ENSNode2["ensnode
(ENSNode Schema)"] + Indexer2["ensindexer_testnet
(ENSIndexer Schema)"] + end + end +``` + + + +## Instance Structure + +An [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance) — a single PostgreSQL database — contains exactly: + +- **1 [Ponder Schema](/ensdb/concepts/glossary#ponder-schema)** — named `ponder_sync` (fixed) +- **1 [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema)** — named `ensnode` (fixed) +- **1+ [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema)** — dynamic names, one per writer + +```mermaid +flowchart TB + subgraph PGServer["PostgreSQL Server"] + subgraph ENSDb["ENSDb Instance (Database)"] + Ponder["ponder_sync
(Ponder Schema)"] + ENSNode["ensnode
(ENSNode Schema)
metadata table"] + + subgraph Indexers["ENSIndexer Schemas"] + Foo["ensindexer_foo
v1_domains, v2_domains
labels, ..."] + Bar["ensindexer_bar
v1_domains, v2_domains
labels, ..."] + More["ensindexer_...
..."] + end + end + end + + ENSNode -->|discovers| Foo + ENSNode -->|discovers| Bar + ENSNode -->|discovers| More +``` + +## Schema Relationships + +### ENSNode Metadata → ENSIndexer Schemas + +The [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) links to ENSIndexer Schemas via the `ens_indexer_schema_name` column: + +```mermaid +erDiagram + ENSNODE_METADATA ||--o{ ENSINDEXER_SCHEMA : "tracks" + + ENSNODE_METADATA { + text ens_indexer_schema_name PK + text key PK + text value_version + jsonb value + } + + ENSINDEXER_SCHEMA["ensindexer_* Schema"] { + text schema_name + timestamp created_at + } +``` + +```sql +-- [Schema Discovery](/ensdb/concepts/glossary#schema-discovery): find all ENSIndexer Schemas +SELECT DISTINCT ens_indexer_schema_name +FROM ensnode.metadata; + +-- Result example: +-- ensindexer_schema_name +-- ──────────────────────── +-- ensindexer_mainnet +-- ensindexer_base +-- ensindexer_custom +``` + +Each writer has at least one row in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table), with its `ens_indexer_schema_name` pointing to the [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) it owns. + +### Ponder Schema → ENSIndexer Schemas + +All writers share the [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) for RPC caching: + +```mermaid +flowchart TB + WriterA["Writer A
(ENSIndexer Mainnet)"] + WriterB["Writer B
(ENSIndexer Base)"] + WriterC["Writer C
(Custom Writer)"] + Ponder["Ponder Schema
(ponder_sync)"] + RPC["RPC Cache"] + + WriterA -->|reads/writes| Ponder + WriterB -->|reads/writes| Ponder + WriterC -->|reads/writes| Ponder + Ponder -->|caches| RPC +``` + +When any writer performs an RPC call, the result is cached in the [Ponder Schema](/ensdb/concepts/glossary#ponder-schema). Subsequent requests for the same data (from any writer) use the cached result, reducing RPC costs. + +## Data Flow + +### Indexing Flow (Writers) + +1. **Writer** starts indexing from onchain +2. Reads onchain data via RPC (cached in **[Ponder Schema](/ensdb/concepts/glossary#ponder-schema)**) +3. Transforms data according to ENSDb schema +4. Writes transformed data to its **[ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema)** +5. Updates [Indexing Status](/ensdb/concepts/glossary#indexing-status) in **[ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table)** + +```mermaid +flowchart TB + Onchain["Onchain Data
Ethereum / L2s"] + RPC["RPC Node"] + Ponder["Ponder Schema
(ponder_sync)
cached RPC"] + Writer["Writer
(ENSIndexer / Custom)"] + ISchema1["ENSIndexer Schema
(write)"] + ISchema2["ENSIndexer Schema
(write)"] + Metadata["ENSNode Metadata
(status)"] + + Onchain -->|RPC Request| RPC + RPC -->|cached RPC| Ponder + Ponder -->|cached response| Writer + Writer --> ISchema1 + Writer --> ISchema2 + Writer --> Metadata +``` + +### Query Flow (Readers) + +1. **Reader** connects to an ENSDb instance (PostgreSQL database) +2. Queries [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) to discover [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema) +3. Queries specific [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) for data +4. Transforms and serves data to applications + +```mermaid +flowchart LR + Reader["Reader
(ENSApi / Custom)"] + ENSDb["ENSDb Instance
(PostgreSQL DB)"] + ENSNode["ENSNode Schema
(discovery)"] + ENSIndexer["ENSIndexer Schema
(data)"] + App["Application
User Interface"] + + Reader -->|Connect| ENSDb + Reader -->|1. Discover| ENSNode + Reader -->|2. Query| ENSIndexer + Reader -->|3. Serve| App +``` + +```sql +-- 1. Connect to specific ENSDb instance +-- psql postgresql://host:5432/ensdb_mainnet + +-- 2. Discover available schemas in this instance +SELECT ens_indexer_schema_name, value +FROM ensnode.metadata +WHERE key = 'ensindexer_indexing_status'; + +-- 3. Query data from a specific ENSIndexer Schema +SELECT * FROM ensindexer_mainnet.v1_domains +WHERE owner_id = '\x1234...'; +``` + +## [Multi-Tenant ENSDb](/ensdb/concepts/glossary#multi-tenant-ensdb): Multiple ENSIndexer Instances Per ENSDb Instance + +An ENSDb instance is **multi-tenant** because a single database can store data from multiple [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) (tenants), each operating independently: + +| Writer | Owns Schema | Purpose | +|--------|-------------|---------| +| ENSIndexer Mainnet | `ensindexer_mainnet` | Ethereum mainnet ENS data | +| ENSIndexer Base | `ensindexer_base` | Base L2 ENS data | +| Custom Writer | `ensindexer_custom` | Custom indexing logic | + +Each tenant (ENSIndexer instance): +- Has its own [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) — isolated data namespace +- Shares the [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) — shared RPC cache across all tenants +- Has its own row in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) — tracked status per tenant + +Multitenancy enables: +- **Separate indexing per chain** — mainnet, L2s, testnets as independent tenants +- **Independent operation** — one tenant can restart while others continue unaffected +- **Custom indexing** — specialized tenants for specific use cases +- **[Schema Version](/ensdb/concepts/glossary#schema-version) evolution** — different tenants can use different schema versions + +## Multi-Instance: Multiple ENSDb Instances Per Server + +A single PostgreSQL server can serve multiple ENSDb instances for different environments: + +```mermaid +flowchart TB + subgraph PGServer["PostgreSQL Server (localhost:5432)"] + direction TB + + subgraph Instance1["ENSDb Instance: ensdb_mainnet
(Production)"] + schemas1["Ponder + ENSNode + ENSIndexer Schemas"] + end + + subgraph Instance2["ENSDb Instance: ensdb_testnet
(Pre-production)"] + schemas2["Ponder + ENSNode + ENSIndexer Schemas"] + end + + subgraph Instance3["ENSDb Instance: ensdb_devnet
(Staging / Local Dev)"] + schemas3["Ponder + ENSNode + ENSIndexer Schemas"] + end + end + + Reader["Reader
(Multi-Instance)"] --> Instance1 + Reader --> Instance2 + Reader --> Instance3 +``` + +This enables: +- **Cost efficiency** — One PostgreSQL server, multiple ENS datasets +- **Organization** — Separate production, staging, and test data +- **Multi-chain aggregation** — Query across instances for cross-chain views + +## Scalability Patterns + +ENSDb can scale to handle massive workloads: + +### Multiple Readers Per Instance + +Any number of readers can query the same ENSDb instance: + +```mermaid +flowchart TB + ENSDb["ENSDb Instance
(ensdb_mainnet)"] + + Reader1["ENSApi #1"] + Reader2["Custom API"] + Reader3["Dashboard"] + Reader4["CLI"] + + ENSDb --> Reader1 + ENSDb --> Reader2 + ENSDb --> Reader3 + ENSDb --> Reader4 +``` + +### Read Replicas + +Distribute read load across PostgreSQL replicas: + +```mermaid +flowchart TB + Primary["Primary ENSDb Instance"] + + Replica1["Read Replica"] + Replica2["Read Replica"] + + Primary -->|Streaming Replication| Replica1 + Primary -->|Streaming Replication| Replica2 +``` + +### Future: ENS Sync Engine + +The upcoming ENS Sync Engine will enable: +- Real-time event streaming from PostgreSQL WAL +- Cache invalidation automation +- Continuous sync between ENSDb instances without running writers + +## Related Concepts + +- **[Glossary](/ensdb/concepts/glossary)** — Definitions for all terms used here +- **[Database Schemas](/ensdb/concepts/database-schemas)** — Deep dive on each schema type +- **[Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle)** — How indexing phases affect database behavior +- **[Building Integrations](/ensdb/integrations/)** — Build custom writers and readers diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx new file mode 100644 index 0000000000..44feaa4d54 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx @@ -0,0 +1,1216 @@ +--- +title: Database Schemas +description: Detailed explanation of ENSDb schemas, including Ponder Schema, ENSNode Schema, and the modular ENSIndexer Schema with all its sub-schemas. +sidebar: + label: Database Schemas + order: 4 +--- + +import { Aside } from '@astrojs/starlight/components'; + +ENSDb organizes data using PostgreSQL [database schemas](/ensdb/concepts/glossary#database-schema). Each schema serves a specific purpose and has specific ownership and naming rules. + +For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). + +## Schema Types Overview + +| Schema | Name | Fixed Name? | Owner | Purpose | +|--------|------|-------------|-------|---------| +| [Ponder Schema](#ponder-schema) | `ponder_sync` | Yes | Shared | RPC cache | +| [ENSNode Schema](#ensnode-schema) | `ensnode` | Yes | ENSNode instance | Metadata | +| [ENSIndexer Schema](#ensindexer-schema) | Dynamic | No | [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) | Indexed data | + +## Ponder Schema + +The [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) caches RPC requests and responses to optimize indexing performance. + +### Properties + +- **Name:** `ponder_sync` (fixed) +- **Created by:** Ponder (external tool) +- **Shared by:** All [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) connected to the [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance) +- **[Schema Definition](/ensdb/concepts/glossary#schema-definition):** Defined by Ponder + +### Purpose + +When an [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) indexes onchain data, it makes RPC calls to blockchain nodes. The [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) caches these calls — including block headers, contract storage slots, call results, and event logs — so that: + +1. Multiple [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) don't make duplicate RPC calls +2. Restarting an [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) doesn't require re-fetching cached data +3. RPC costs are reduced + +### Behavior + +- **Do not drop manually** — If dropped, Ponder will recreate it, but with increased RPC costs during the backfill period +- Schema is managed by Ponder, not by ENSNode + +### RPC Cache and ENS Namespaces + +The Ponder Schema caches RPC data **only for the chains being indexed** by ENSIndexer instances connected to the ENSDb instance. This has important implications for ENS Namespaces: + +| Setup | RPC Cache Contents | +|-------|-------------------| +| All ENSIndexer instances index "mainnet" namespace | Mainnet chains only | +| All ENSIndexer instances index "sepolia" namespace | Testnet chains only | +| Mixed namespaces (mainnet + sepolia instances) | Both mainnet and testnet chains | + +**Critical consideration:** In a mixed-namespace setup, approximately 95%+ of cached RPC data will be for mainnet chains due to the significantly larger onchain history and state. This makes the Ponder Schema inefficient for testnet-only operations — the cache will be dominated by mainnet data while testnet data constitutes a tiny fraction. + +**Recommendation:** For a lean ENSNode setup dedicated to testnets, use a **separate ENSDb instance** for the "sepolia" namespace. This ensures: +- The Ponder Schema contains **only testnet RPC cache** +- ENSDb snapshots are **significantly smaller** (no mainnet bloat) +- You can **precisely snapshot and restore** testnet state +- **Substantial RPC cost savings** (hundreds to thousands of dollars) when spinning up new testnet instances, as the cache is already primed with relevant testnet data + +Conversely, mixing mainnet and sepolia in a single ENSDb instance pollutes the cache with mostly mainnet data, making it unsuitable for efficient testnet-only deployments. + +## ENSNode Schema + +The [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) stores metadata about [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) and their [Indexing Status](/ensdb/concepts/glossary#indexing-status). + +### Properties + +- **Name:** `ensnode` (fixed) +- **Created by:** First ENSIndexer instance to connect +- **Schema Definition:** `@ensnode/ensdb-sdk/ensnode` + +### Multi-Tenancy: Tracking Multiple ENSIndexer Schemas + +A single ENSNode Schema tracks metadata for **multiple** ENSIndexer Schemas within the same ENSDb instance. This enables multi-tenant indexing where different chains, configurations, or use cases can be indexed independently. + +```mermaid +erDiagram + ENSNODE_METADATA ||--o{ ENSINDEXER_MAINNET : "tracks" + ENSNODE_METADATA ||--o{ ENSINDEXER_L2 : "tracks" + ENSNODE_METADATA ||--o{ ENSINDEXER_CUSTOM : "tracks" + + ENSNODE_METADATA { + text ens_indexer_schema_name PK + text key PK + text value_version + jsonb value + } + + ENSINDEXER_MAINNET["ensindexer_mainnet Schema"] { + text schema_name "ensindexer_mainnet" + } + + ENSINDEXER_L2["ensindexer_l2 Schema"] { + text schema_name "ensindexer_l2" + } + + ENSINDEXER_CUSTOM["ensindexer_custom Schema"] { + text schema_name "ensindexer_custom" + } +``` + +### Contents + +The ENSNode Schema contains a single table: [ENSNode Metadata Table](#ensnode-metadata-table). + +### ENSNode Metadata Table + +| Column | Type | Description | +|--------|------|-------------| +| `ens_indexer_schema_name` | `text` | Name of the [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) this record belongs to | +| `key` | `text` | Type of metadata record | +| `value_version` | `text` | [ENSNode Metadata Value Version](/ensdb/concepts/glossary#ensnode-metadata-value-version) | +| `value` | `jsonb` | The metadata content | + +**Primary Key:** (`ens_indexer_schema_name`, `key`) + +### Common Metadata Keys + +| Key | Purpose | +|-----|---------| +| `ensindexer_indexing_status` | Current [Indexing Status](/ensdb/concepts/glossary#indexing-status) and progress | +| `ensindexer_public_config` | Public configuration of the [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) | +| `ensdb_version` | [ENSDb version](/ensdb/concepts/glossary#ensdb-sdk) information for this instance | + +### Behavior + +- **Do not drop manually** — If dropped, ENSNode cannot track [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) +- The first [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) to connect creates the schema and migrations table +- Each [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) writes its own row(s) with its [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name) + +### Schema Discovery + +Query [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) to discover all [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema): + +```sql +SELECT DISTINCT ens_indexer_schema_name +FROM ensnode.metadata; +``` + +## ENSIndexer Schema + +Each [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) owns an [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) where it writes indexed ENS data. + +### Properties + +- **Name:** Dynamic, determined by ENSIndexer instance configuration +- **Created by:** Ponder app running inside the ENSIndexer instance +- **Schema Definition:** `@ensnode/ensdb-sdk/ensindexer-abstract` + +### Modular Sub-Schema Architecture + +The ENSIndexer Schema is **modular** — it's composed of five distinct sub-schemas that each serve a specific purpose: + +| Sub-Schema | SDK Path | Purpose | Key Entities | +|------------|----------|---------|--------------| +| **ensv2** | `/ensindexer-abstract/ensv2.schema` | Core ENS domain and event indexing | Domains, Events, Registrations, Labels | +| **protocol-acceleration** | `/ensindexer-abstract/protocol-acceleration.schema` | Resolution acceleration and resolver records | Resolvers, Reverse Names, Domain-Resolver Relations | +| **registrars** | `/ensindexer-abstract/registrars.schema` | Registration lifecycle tracking | Subregistries, Registration Lifecycles, Registrar Actions | +| **subgraph** | `/ensindexer-abstract/subgraph.schema` | Backward compatibility with legacy ENS Subgraph | Subgraph-compatible entities and events | +| **tokenscope** | `/ensindexer-abstract/tokenscope.schema` | NFT market data and token tracking | Name Sales, Name Tokens | + +These sub-schemas work together to provide a complete picture of ENS onchain state while maintaining separation of concerns. All five sub-schemas exist within a single ENSIndexer Schema namespace. + +```mermaid +flowchart TB + subgraph ENSIndexerSchema["ENSIndexer Schema (Dynamic Name)"] + subgraph ENSv2["ensv2 Sub-Schema"] + v1d[v1_domains] + v2d[v2_domains] + reg[registries] + lbl[labels] + acc[accounts] + evt[events] + dom_evt[domain_events] + reg_tbl[registrations] + rnl[renewals] + perm[permissions] + end + + subgraph Protocol["protocol-acceleration Sub-Schema"] + res[resolvers] + rr[resolver_records] + rar[resolver_address_records] + rtr[resolver_text_records] + drr[domain_resolver_relations] + rnr[reverse_name_records] + mn[migrated_nodes] + end + + subgraph Registrars["registrars Sub-Schema"] + subreg[subregistries] + rl[registration_lifecycles] + ra[registrar_actions] + ram[internal_registrar_action_metadata] + end + + subgraph Subgraph["subgraph Sub-Schema"] + sgd[subgraph_domains] + sga[subgraph_accounts] + sgr[subgraph_registrations] + sgres[subgraph_resolvers] + sgwd[subgraph_wrapped_domains] + sge[subgraph_*_events] + end + + subgraph TokenScope["tokenscope Sub-Schema"] + ns[name_sales] + nt[name_tokens] + end + end +``` + +### Naming + +[ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) names are dynamic. The name is determined by the [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) based on its configuration. + +Examples: `ensindexer_0`, `ensindexer_mainnet`, `ensindexer_abc123` + +### Index Behavior + +Indexes on [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) tables are created/dropped based on [Indexing Status](/ensdb/concepts/glossary#indexing-status): + +| Status | [Indexes](/ensdb/concepts/glossary#database-objects) | Reason | +|--------|---------|--------| +| Backfill | Dropped | Optimize write throughput for historical data | +| Following | Created | Optimize read queries for live data | + +See [Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle) for details. + +--- + +## Sub-Schema Reference + +### ensv2 Sub-Schema + +The **ensv2** sub-schema provides the core ENS protocol indexing functionality. It tracks domains (both ENSv1 and ENSv2), events, registrations, renewals, and labels. + +```mermaid +erDiagram + V1_DOMAINS ||--o{ V1_DOMAINS : "parent/children" + V1_DOMAINS ||--o{ REGISTRATIONS : "has" + V1_DOMAINS ||--|| LABELS : "labelHash" + V1_DOMAINS ||--|| ACCOUNTS : "owner" + V1_DOMAINS ||--o{ DOMAIN_EVENTS : "events" + + V2_DOMAINS ||--|| REGISTRIES : "registryId" + V2_DOMAINS ||--o{ REGISTRATIONS : "has" + V2_DOMAINS ||--|| LABELS : "labelHash" + V2_DOMAINS ||--|| ACCOUNTS : "owner" + V2_DOMAINS ||--o{ DOMAIN_EVENTS : "events" + V2_DOMAINS ||--|| REGISTRIES : "subregistryId" + + REGISTRIES ||--o{ V2_DOMAINS : "has" + + EVENTS ||--o{ DOMAIN_EVENTS : "tracked_by" + EVENTS ||--o{ RESOLVER_EVENTS : "tracked_by" + EVENTS ||--o{ PERMISSIONS_EVENTS : "tracked_by" + + DOMAIN_EVENTS }o--|| V1_DOMAINS : "domainId" + DOMAIN_EVENTS }o--|| V2_DOMAINS : "domainId" + + REGISTRATIONS ||--o{ RENEWALS : "has" + REGISTRATIONS ||--|| ACCOUNTS : "registrant" + REGISTRATIONS ||--|| EVENTS : "eventId" + + RENEWALS ||--|| REGISTRATIONS : "registration" + RENEWALS ||--|| EVENTS : "eventId" + + PERMISSIONS ||--o{ PERMISSIONS_RESOURCE : "resources" + PERMISSIONS ||--o{ PERMISSIONS_USER : "users" + + PERMISSIONS_RESOURCE ||--|| PERMISSIONS : "permissions" + PERMISSIONS_USER ||--|| PERMISSIONS : "permissions" + PERMISSIONS_USER ||--|| ACCOUNTS : "user" + + LABELS ||--o{ V1_DOMAINS : "domains" + LABELS ||--o{ V2_DOMAINS : "domains" +``` + +#### Table: `events` + +Core event log table storing all onchain event metadata. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Ponder's event.id | +| `chain_id` | `integer` | Not Null, Indexed | Chain ID (EIP-155) | +| `block_number` | `bigint` | Not Null | Block number | +| `block_hash` | `bytea` | Not Null | Block hash | +| `timestamp` | `bigint` | Not Null, Indexed | Block timestamp (Unix seconds) | +| `transaction_hash` | `bytea` | Not Null | Transaction hash | +| `transaction_index` | `integer` | Not Null | Transaction index in block | +| `from` | `bytea` | Not Null, Indexed | Transaction sender | +| `to` | `bytea` | Nullable | Transaction recipient (null for contract deployment) | +| `address` | `bytea` | Not Null | Contract address that emitted the event | +| `log_index` | `integer` | Not Null | Log index in transaction | +| `selector` | `bytea` | Not Null, Indexed | Event selector (topic0) | +| `topics` | `bytea[]` | Not Null | All event topics | +| `data` | `bytea` | Not Null | Event data payload | + +**Indexes:** +- Primary Key: `id` +- `bySelector`: `selector` +- `byFrom`: `from` +- `byTimestamp`: `timestamp` + +#### Table: `domain_events` + +Join table linking domains to their events. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `domain_id` | `text` | Not Null, PK | Domain ID (namehash) | +| `event_id` | `text` | Not Null, PK | Event ID | + +**Primary Key:** (`domain_id`, `event_id`) + +#### Table: `resolver_events` + +Join table linking resolvers to their events. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `resolver_id` | `text` | Not Null, PK | Resolver ID | +| `event_id` | `text` | Not Null, PK | Event ID | + +**Primary Key:** (`resolver_id`, `event_id`) + +#### Table: `permissions_events` + +Join table linking permissions to their events. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `permissions_id` | `text` | Not Null, PK | Permissions ID | +| `event_id` | `text` | Not Null, PK | Event ID | + +**Primary Key:** (`permissions_id`, `event_id`) + +#### Table: `accounts` + +Ethereum accounts that interact with ENS. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `bytea` | Primary Key | Ethereum address | + +#### Table: `registries` + +ENSv2 registries (both root and subregistries). + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Registry ID (CAIP-10 format: chainId:address) | +| `chain_id` | `integer` | Not Null | Chain ID | +| `address` | `bytea` | Not Null | Registry contract address | + +**Indexes:** +- Primary Key: `id` +- `byId`: Unique index on (`chain_id`, `address`) + +#### Table: `v1_domains` + +ENSv1 domain records (flat namespace, keyed by node). + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Domain ID (node/namehash) | +| `parent_id` | `text` | Not Null, Indexed | Parent domain ID | +| `owner_id` | `bytea` | Nullable, Indexed | Effective owner address | +| `label_hash` | `bytea` | Not Null, Indexed | Label hash | +| `root_registry_owner_id` | `bytea` | Nullable | ENSv1 Registry owner (zeroAddress = null) | + +**Indexes:** +- Primary Key: `id` +- `byParent`: `parent_id` +- `byOwner`: `owner_id` +- `byLabelHash`: `label_hash` + +#### Table: `v2_domains` + +ENSv2 domain records (registry-based namespace). + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Domain ID | +| `token_id` | `bigint` | Not Null | ERC721 token ID | +| `registry_id` | `text` | Not Null, Indexed | Parent registry ID | +| `subregistry_id` | `text` | Nullable, Indexed | Subregistry ID (if domain is a registry) | +| `owner_id` | `bytea` | Nullable, Indexed | Domain owner address | +| `label_hash` | `bytea` | Not Null, Indexed | Label hash | + +**Indexes:** +- Primary Key: `id` +- `byRegistry`: `registry_id` +- `bySubregistry`: `subregistry_id` (where not null) +- `byOwner`: `owner_id` +- `byLabelHash`: `label_hash` + +#### Enum: `registration_type` + +Registration types supported by the schema. + +| Value | Description | +|-------|-------------| +| `NameWrapper` | NameWrapper registration | +| `BaseRegistrar` | BaseRegistrar registration | +| `ThreeDNS` | 3DNS registration | +| `ENSv2RegistryRegistration` | ENSv2 Registry registration | +| `ENSv2RegistryReservation` | ENSv2 Registry reservation | + +#### Table: `registrations` + +Domain registration records (polymorphic across types). + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Registration ID (domainId:registrationIndex) | +| `domain_id` | `text` | Not Null, Indexed | Domain ID | +| `registration_index` | `integer` | Not Null | Index of this registration for the domain | +| `type` | `registration_type` | Not Null | Registration type | +| `start` | `bigint` | Not Null | Registration start timestamp | +| `expiry` | `bigint` | Nullable | Registration expiry timestamp | +| `grace_period` | `bigint` | Nullable | Grace period duration (for BaseRegistrar) | +| `registrar_chain_id` | `integer` | Not Null | Registrar chain ID | +| `registrar_address` | `bytea` | Not Null | Registrar contract address | +| `registrant_id` | `bytea` | Nullable | Registrant address | +| `unregistrant_id` | `bytea` | Nullable | Previous registrant (on transfer) | +| `referrer` | `bytea` | Nullable | Encoded referrer data | +| `fuses` | `integer` | Nullable | NameWrapper fuses | +| `base` | `bigint` | Nullable | Base cost in wei | +| `premium` | `bigint` | Nullable | Premium cost in wei | +| `wrapped` | `boolean` | Default: false | Whether registration is wrapped | +| `event_id` | `text` | Not Null | Event that created this registration | + +**Indexes:** +- Primary Key: `id` +- `byId`: Unique index on (`domain_id`, `registration_index`) + +#### Table: `latest_registration_indexes` + +Tracks the latest registration index for each domain. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `domain_id` | `text` | Primary Key | Domain ID | +| `registration_index` | `integer` | Not Null | Latest registration index | + +#### Table: `renewals` + +Domain renewal records. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Renewal ID (domainId:registrationIndex:renewalIndex) | +| `domain_id` | `text` | Not Null | Domain ID | +| `registration_index` | `integer` | Not Null | Registration index | +| `renewal_index` | `integer` | Not Null | Index of this renewal for the registration | +| `duration` | `bigint` | Not Null | Renewal duration in seconds | +| `referrer` | `bytea` | Nullable | Encoded referrer data | +| `base` | `bigint` | Nullable | Base cost in wei | +| `premium` | `bigint` | Nullable | Premium cost in wei | +| `event_id` | `text` | Not Null | Event that created this renewal | + +**Indexes:** +- Primary Key: `id` +- `byId`: Unique index on (`domain_id`, `registration_index`, `renewal_index`) + +#### Table: `latest_renewal_indexes` + +Tracks the latest renewal index for each registration. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `domain_id` | `text` | Not Null, PK | Domain ID | +| `registration_index` | `integer` | Not Null, PK | Registration index | +| `renewal_index` | `integer` | Not Null | Latest renewal index | + +**Primary Key:** (`domain_id`, `registration_index`) + +#### Table: `permissions` + +Permission manager contracts (ERC-7715 style). + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Permissions ID (chainId:address) | +| `chain_id` | `integer` | Not Null | Chain ID | +| `address` | `bytea` | Not Null | Contract address | + +**Indexes:** +- Primary Key: `id` +- `byId`: Unique index on (`chain_id`, `address`) + +#### Table: `permissions_resources` + +Permission resources within a permission manager. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Resource ID (chainId:address:resource) | +| `chain_id` | `integer` | Not Null | Chain ID | +| `address` | `bytea` | Not Null | Permission manager address | +| `resource` | `bigint` | Not Null | Resource identifier | + +**Indexes:** +- Primary Key: `id` +- `byId`: Unique index on (`chain_id`, `address`, `resource`) + +#### Table: `permissions_users` + +Permission user assignments. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | User assignment ID (chainId:address:resource:user) | +| `chain_id` | `integer` | Not Null | Chain ID | +| `address` | `bytea` | Not Null | Permission manager address | +| `resource` | `bigint` | Not Null | Resource identifier | +| `user` | `bytea` | Not Null | User address | +| `roles` | `bigint` | Not Null | Roles bitmap | + +**Indexes:** +- Primary Key: `id` +- `byId`: Unique index on (`chain_id`, `address`, `resource`, `user`) + +#### Table: `labels` + +Label hash to interpreted label mapping (rainbow table for name healing). + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `label_hash` | `bytea` | Primary Key | Label hash | +| `interpreted` | `text` | Not Null, Indexed | Interpreted label text | + +**Indexes:** +- Primary Key: `label_hash` +- `byInterpreted`: `interpreted` + +#### Table: `registry_canonical_domains` + +Tracks canonical domain references for registries (temporary table). + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `registry_id` | `text` | Primary Key | Registry ID | +| `domain_id` | `text` | Not Null | Canonical domain ID | + +--- + +### protocol-acceleration Sub-Schema + +The **protocol-acceleration** sub-schema provides data structures that accelerate ENS resolution and track resolver relationships. + +```mermaid +erDiagram + RESOLVERS ||--o{ RESOLVER_RECORDS : "has" + RESOLVERS { + text id PK + integer chain_id + bytea address + } + + RESOLVER_RECORDS ||--o{ RESOLVER_ADDRESS_RECORDS : "address_records" + RESOLVER_RECORDS ||--o{ RESOLVER_TEXT_RECORDS : "text_records" + RESOLVER_RECORDS { + text id PK + integer chain_id + bytea address + bytea node + text name + } + + RESOLVER_ADDRESS_RECORDS { + integer chain_id PK + bytea address PK + bytea node PK + bigint coin_type PK + text value + } + + RESOLVER_TEXT_RECORDS { + integer chain_id PK + bytea address PK + bytea node PK + text key PK + text value + } + + DOMAIN_RESOLVER_RELATIONS }o--|| RESOLVERS : "resolver" + DOMAIN_RESOLVER_RELATIONS { + integer chain_id PK + bytea address PK + bytea domain_id PK + bytea resolver + } + + REVERSE_NAME_RECORDS ||--|| ACCOUNTS : "address" + REVERSE_NAME_RECORDS { + bytea address PK + bigint coin_type PK + text value + } + + MIGRATED_NODES { + bytea node PK + } +``` + +#### Table: `reverse_name_records` + +ENSIP-19 reverse name records indexed by account and coin type. + + + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `address` | `bytea` | Not Null, PK | Account address | +| `coin_type` | `bigint` | Not Null, PK | SLIP-44 coin type | +| `value` | `text` | Not Null | Reverse name record value | + +**Primary Key:** (`address`, `coin_type`) + +#### Table: `domain_resolver_relations` + +Domain-to-resolver mappings for accelerated lookups. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `chain_id` | `integer` | Not Null, PK | Chain ID | +| `address` | `bytea` | Not Null, PK | Registry address | +| `domain_id` | `bytea` | Not Null, PK | Domain ID (node) | +| `resolver` | `bytea` | Not Null | Resolver contract address | + +**Primary Key:** (`chain_id`, `address`, `domain_id`) + +#### Table: `resolvers` + +Resolver contracts that have emitted events. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Resolver ID (chainId:address) | +| `chain_id` | `integer` | Not Null | Chain ID | +| `address` | `bytea` | Not Null | Resolver contract address | + +**Indexes:** +- Primary Key: `id` +- `byId`: Unique index on (`chain_id`, `address`) + +#### Table: `resolver_records` + +Resolver records for specific nodes. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Record ID (chainId:resolver:node) | +| `chain_id` | `integer` | Not Null | Chain ID | +| `address` | `bytea` | Not Null | Resolver address | +| `node` | `bytea` | Not Null | Node (namehash) | +| `name` | `text` | Nullable | ENSIP-3 name record value | + +**Indexes:** +- Primary Key: `id` +- `byId`: Unique index on (`chain_id`, `address`, `node`) + + + +#### Table: `resolver_address_records` + +Address records (ENSIP-9) within resolver records. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `chain_id` | `integer` | Not Null, PK | Chain ID | +| `address` | `bytea` | Not Null, PK | Resolver address | +| `node` | `bytea` | Not Null, PK | Node (namehash) | +| `coin_type` | `bigint` | Not Null, PK | SLIP-44 coin type | +| `value` | `text` | Not Null | Address record value (interpreted) | + +**Primary Key:** (`chain_id`, `address`, `node`, `coin_type`) + +#### Table: `resolver_text_records` + +Text records (ENSIP-5) within resolver records. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `chain_id` | `integer` | Not Null, PK | Chain ID | +| `address` | `bytea` | Not Null, PK | Resolver address | +| `node` | `bytea` | Not Null, PK | Node (namehash) | +| `key` | `text` | Not Null, PK | Text record key | +| `value` | `text` | Not Null | Text record value | + +**Primary Key:** (`chain_id`, `address`, `node`, `key`) + +#### Table: `migrated_nodes` + +Tracks nodes that have migrated from RegistryOld to the new Registry. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `node` | `bytea` | Primary Key | Migrated node (namehash) | + + + +--- + +### registrars Sub-Schema + +The **registrars** sub-schema tracks registration lifecycles and registrar actions across different registrar implementations. + +```mermaid +erDiagram + SUBREGISTRIES ||--o{ REGISTRATION_LIFECYCLES : "manages" + SUBREGISTRIES { + text subregistry_id PK + bytea node + } + + REGISTRATION_LIFECYCLES ||--o{ REGISTRAR_ACTIONS : "actions" + REGISTRATION_LIFECYCLES { + bytea node PK + text subregistry_id + bigint expires_at + } + + REGISTRAR_ACTIONS ||--o{ EVENTS : "eventIds" + REGISTRAR_ACTIONS { + text id PK + registrar_action_type type + text subregistry_id + bytea node + bigint incremental_duration + bigint base_cost + bigint premium + bigint total + bytea registrant + bytea encoded_referrer + bytea decoded_referrer + bigint block_number + bigint timestamp + bytea transaction_hash + text[] event_ids + } +``` + +#### Table: `subregistries` + +Subregistry contracts that manage subname registrations. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `subregistry_id` | `text` | Primary Key | Subregistry ID (CAIP-10: chainId:address) | +| `node` | `bytea` | Not Null, Unique | Node this subregistry manages subnames of | + +**Indexes:** +- Primary Key: `subregistry_id` +- `uniqueNode`: Unique index on `node` + +#### Table: `registration_lifecycles` + +Tracks the current registration lifecycle for each name. + + + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `node` | `bytea` | Primary Key | Domain node (namehash) | +| `subregistry_id` | `text` | Not Null | Subregistry managing this registration | +| `expires_at` | `bigint` | Not Null | Expiration timestamp | + +**Indexes:** +- Primary Key: `node` +- `bySubregistry`: `subregistry_id` + +#### Enum: `registrar_action_type` + +Types of registrar actions. + +| Value | Description | +|-------|-------------| +| `registration` | New registration | +| `renewal` | Renewal/extension | + +#### Table: `registrar_actions` + +Logical registrar actions aggregated from multiple events. + + + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Action ID (Ponder checkpoint string) | +| `type` | `registrar_action_type` | Not Null | Action type | +| `subregistry_id` | `text` | Not Null | Subregistry ID | +| `node` | `bytea` | Not Null | Domain node | +| `incremental_duration` | `bigint` | Not Null | Duration added to registration | +| `base_cost` | `bigint` | Nullable | Base cost in wei | +| `premium` | `bigint` | Nullable | Premium cost in wei | +| `total` | `bigint` | Nullable | Total cost in wei (base + premium) | +| `registrant` | `bytea` | Not Null | Action initiator address | +| `encoded_referrer` | `bytea` | Nullable | Raw 32-byte referrer value | +| `decoded_referrer` | `bytea` | Nullable | Decoded referrer address | +| `block_number` | `bigint` | Not Null | Block number | +| `timestamp` | `bigint` | Not Null | Block timestamp | +| `transaction_hash` | `bytea` | Not Null | Transaction hash | +| `event_ids` | `text[]` | Not Null | Contributing event IDs | + +**Indexes:** +- Primary Key: `id` +- `byDecodedReferrer`: `decoded_referrer` +- `byTimestamp`: `timestamp` + +#### Table: `_ensindexer_registrar_action_metadata` + +Internal metadata for aggregating registrar action data across events. + + + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `metadata_type` | `_ensindexer_registrar_action_metadata_type` | Primary Key | Metadata type | +| `logical_event_key` | `text` | Not Null | Key for grouping events (domainId:transactionHash) | +| `logical_event_id` | `text` | Not Null | Current logical action ID being built | + +--- + +### subgraph Sub-Schema + +The **subgraph** sub-schema provides backward compatibility with the legacy ENS Subgraph data model. When paired with `@ensnode/ponder-subgraph`, it enables a fully subgraph-compatible GraphQL API. + +```mermaid +erDiagram + SUBGRAPH_DOMAINS ||--o{ SUBGRAPH_REGISTRATIONS : "has" + SUBGRAPH_DOMAINS ||--|| SUBGRAPH_RESOLVERS : "resolver" + SUBGRAPH_DOMAINS ||--o{ SUBGRAPH_WRAPPED_DOMAINS : "wrapped" + SUBGRAPH_DOMAINS ||--|| SUBGRAPH_ACCOUNTS : "owner" + SUBGRAPH_DOMAINS ||--|| SUBGRAPH_ACCOUNTS : "registrant" + SUBGRAPH_DOMAINS ||--|| SUBGRAPH_ACCOUNTS : "wrapped_owner" + SUBGRAPH_DOMAINS ||--|| SUBGRAPH_ACCOUNTS : "resolved_address" + SUBGRAPH_DOMAINS ||--o{ SUBGRAPH_DOMAINS : "parent/subdomains" + + SUBGRAPH_ACCOUNTS ||--o{ SUBGRAPH_DOMAINS : "domains" + SUBGRAPH_ACCOUNTS ||--o{ SUBGRAPH_WRAPPED_DOMAINS : "wrapped_domains" + SUBGRAPH_ACCOUNTS ||--o{ SUBGRAPH_REGISTRATIONS : "registrations" + + SUBGRAPH_RESOLVERS ||--o{ SUBGRAPH_ADDR_CHANGED : "events" + SUBGRAPH_RESOLVERS ||--o{ SUBGRAPH_TEXT_CHANGED : "events" + SUBGRAPH_RESOLVERS ||--o{ SUBGRAPH_CONTENTHASH_CHANGED : "events" + + SUBGRAPH_REGISTRATIONS ||--o{ SUBGRAPH_NAME_REGISTERED : "events" + SUBGRAPH_REGISTRATIONS ||--o{ SUBGRAPH_NAME_RENEWED : "events" +``` + +#### Table: `subgraph_domains` + +Subgraph-compatible domain entity. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `bytea` | Primary Key | Namehash | +| `name` | `text` | Nullable | Domain name (subgraph-interpreted or normalized) | +| `label_name` | `text` | Nullable | Label name | +| `labelhash` | `bytea` | Nullable, Indexed | Label hash | +| `parent_id` | `bytea` | Nullable, Indexed | Parent domain namehash | +| `subdomain_count` | `integer` | Not Null, Default: 0 | Number of subdomains | +| `resolved_address_id` | `bytea` | Nullable, Indexed | Resolved address | +| `resolver_id` | `text` | Nullable | Resolver ID | +| `ttl` | `bigint` | Nullable | Time-to-live | +| `is_migrated` | `boolean` | Not Null, Default: false | Migration status | +| `created_at` | `bigint` | Not Null | Creation timestamp | +| `owner_id` | `bytea` | Not Null, Indexed | Owner address | +| `registrant_id` | `bytea` | Nullable, Indexed | Registrant address | +| `wrapped_owner_id` | `bytea` | Nullable, Indexed | Wrapped owner address | +| `expiry_date` | `bigint` | Nullable | Expiration date | + +**Indexes:** +- Primary Key: `id` +- `byLabelhash`: `labelhash` +- `byParentId`: `parent_id` +- `byOwnerId`: `owner_id` +- `byRegistrantId`: `registrant_id` +- `byWrappedOwnerId`: `wrapped_owner_id` +- `byResolvedAddressId`: `resolved_address_id` + + + +#### Table: `subgraph_accounts` + +Subgraph-compatible account entity. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `bytea` | Primary Key | Ethereum address | + +#### Table: `subgraph_resolvers` + +Subgraph-compatible resolver entity. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Resolver ID (domainId:address) | +| `domain_id` | `bytea` | Not Null, Indexed | Domain ID | +| `address` | `bytea` | Not Null | Resolver address | +| `addr_id` | `bytea` | Nullable | Current addr record | +| `content_hash` | `text` | Nullable | Contenthash | +| `texts` | `text[]` | Nullable | Observed text record keys | +| `coin_types` | `bigint[]` | Nullable | Observed coin types | + +**Indexes:** +- Primary Key: `id` +- `byDomainId`: `domain_id` + +#### Table: `subgraph_registrations` + +Subgraph-compatible registration entity. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `bytea` | Primary Key | Registration ID | +| `domain_id` | `bytea` | Not Null, Indexed | Domain ID | +| `registration_date` | `bigint` | Not Null | Registration timestamp | +| `expiry_date` | `bigint` | Not Null | Expiry timestamp | +| `cost` | `bigint` | Nullable | Registration cost | +| `registrant_id` | `bytea` | Not Null, Indexed | Registrant address | +| `label_name` | `text` | Nullable | Label name | + +**Indexes:** +- Primary Key: `id` +- `byDomainId`: `domain_id` +- `byRegistrationDate`: `registration_date` +- `byExpiryDate`: `expiry_date` + +#### Table: `subgraph_wrapped_domains` + +Subgraph-compatible wrapped domain entity. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `bytea` | Primary Key | Wrapped domain ID | +| `domain_id` | `bytea` | Not Null, Indexed | Domain ID | +| `expiry_date` | `bigint` | Not Null | Expiry timestamp | +| `fuses` | `integer` | Not Null | Fuses bitmap | +| `owner_id` | `bytea` | Not Null | Owner address | +| `name` | `text` | Nullable | DNS-encoded name | + +**Indexes:** +- Primary Key: `id` +- `byDomainId`: `domain_id` + +#### Domain Event Tables + +All domain events share a common structure: + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Event ID | +| `block_number` | `integer` | Not Null | Block number | +| `transaction_id` | `bytea` | Not Null | Transaction hash | +| `domain_id` | `bytea` | Not Null, Indexed | Domain ID | + +**Tables:** +- `subgraph_transfers` — Domain ownership transfers (`owner_id`) +- `subgraph_new_owners` — New owner events (`owner_id`, `parent_domain_id`) +- `subgraph_new_resolvers` — Resolver set events (`resolver_id`) +- `subgraph_new_ttls` — TTL change events (`ttl`) +- `subgraph_wrapped_transfers` — Wrapped domain transfers (`owner_id`) +- `subgraph_name_wrapped` — Name wrapped events (`name`, `fuses`, `owner_id`, `expiry_date`) +- `subgraph_name_unwrapped` — Name unwrapped events (`owner_id`) +- `subgraph_fuses_set` — Fuses set events (`fuses`) +- `subgraph_expiry_extended` — Expiry extension events (`expiry_date`) + +#### Registration Event Tables + +Registration events include: + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Event ID | +| `block_number` | `integer` | Not Null | Block number | +| `transaction_id` | `bytea` | Not Null | Transaction hash | +| `registration_id` | `bytea` | Not Null, Indexed | Registration ID | + +**Tables:** +- `subgraph_name_registered` — Registration events (`registrant_id`, `expiry_date`) +- `subgraph_name_renewed` — Renewal events (`expiry_date`) +- `subgraph_name_transferred` — Registration transfer events (`new_owner_id`) + +#### Resolver Event Tables + +Resolver events include: + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Event ID | +| `block_number` | `integer` | Not Null | Block number | +| `transaction_id` | `bytea` | Not Null | Transaction hash | +| `resolver_id` | `text` | Not Null, Indexed | Resolver ID | + +**Tables:** +- `subgraph_addr_changed` — Addr record changes (`addr_id`) +- `subgraph_multicoin_addr_changed` — Multicoin addr changes (`coin_type`, `addr`) +- `subgraph_name_changed` — Name record changes (`name`) +- `subgraph_abi_changed` — ABI changes (`content_type`) +- `subgraph_pubkey_changed` — Pubkey changes (`x`, `y`) +- `subgraph_text_changed` — Text record changes (`key`, `value`) +- `subgraph_contenthash_changed` — Contenthash changes (`hash`) +- `subgraph_interface_changed` — Interface changes (`interface_id`, `implementer`) +- `subgraph_authorisation_changed` — Authorisation changes (`owner`, `target`, `is_authorized`) +- `subgraph_version_changed` — Version changes (`version`) + +--- + +### tokenscope Sub-Schema + +The **tokenscope** sub-schema tracks NFT sales and token ownership for ENS names in secondary markets. + +```mermaid +erDiagram + NAME_TOKENS ||--o{ NAME_SALES : "sold_via" + NAME_TOKENS { + text id PK "CAIP-19 Asset ID" + bytea domain_id + integer chain_id + bytea contract_address + bigint token_id + text asset_namespace "erc721/erc1155" + text asset_id "CAIP-19" + bytea owner + text mint_status "minted/burned" + } + + NAME_SALES ||--|| NAME_TOKENS : "token_sold" + NAME_SALES { + text id PK "{chainId}-{blockNumber}-{logIndex}" + integer chain_id + bigint block_number + integer log_index + bytea transaction_hash + bytea order_hash "Seaport order" + bytea contract_address + bigint token_id + text asset_namespace + text asset_id + bytea domain_id + bytea buyer + bytea seller + text currency "ETH/USDC/DAI" + bigint amount + bigint timestamp + } +``` + +#### Table: `name_sales` + +NFT sale records from secondary markets. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | Sale ID (format: `{chainId}-{blockNumber}-{logIndex}`) | +| `chain_id` | `integer` | Not Null | Chain ID | +| `block_number` | `bigint` | Not Null | Block number | +| `log_index` | `integer` | Not Null | Log index | +| `transaction_hash` | `bytea` | Not Null | Transaction hash | +| `order_hash` | `bytea` | Not Null | Seaport order hash | +| `contract_address` | `bytea` | Not Null | NFT contract address | +| `token_id` | `bigint` | Not Null | Token ID | +| `asset_namespace` | `text` | Not Null | `erc721` or `erc1155` | +| `asset_id` | `text` | Not Null | CAIP-19 Asset ID | +| `domain_id` | `bytea` | Not Null, Indexed | Domain namehash | +| `buyer` | `bytea` | Not Null, Indexed | Buyer address | +| `seller` | `bytea` | Not Null, Indexed | Seller address | +| `currency` | `text` | Not Null | Payment currency (`ETH`, `USDC`, `DAI`) | +| `amount` | `bigint` | Not Null | Amount in smallest unit | +| `timestamp` | `bigint` | Not Null | Block timestamp | + +**Indexes:** +- Primary Key: `id` +- `idx_domainId`: `domain_id` +- `idx_assetId`: `asset_id` +- `idx_buyer`: `buyer` +- `idx_seller`: `seller` +- `idx_timestamp`: `timestamp` + + + +#### Table: `name_tokens` + +ENS name NFT tracking. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `id` | `text` | Primary Key | CAIP-19 Asset ID | +| `domain_id` | `bytea` | Not Null, Indexed | Domain namehash | +| `chain_id` | `integer` | Not Null | Chain ID | +| `contract_address` | `bytea` | Not Null | NFT contract address | +| `token_id` | `bigint` | Not Null | Token ID | +| `asset_namespace` | `text` | Not Null | `erc721` or `erc1155` | +| `owner` | `bytea` | Not Null, Indexed | Current owner (zeroAddress if burned) | +| `mint_status` | `text` | Not Null | `minted` or `burned` | + +**Indexes:** +- Primary Key: `id` +- `idx_domainId`: `domain_id` +- `idx_owner`: `owner` + + + +--- + +## Cross-Sub-Schema Relationships + +While each sub-schema serves a distinct purpose, tables across sub-schemas are related to provide a complete ENS data model: + +| From Sub-Schema | Table | Relationship | To Sub-Schema | Table | Via | +|-----------------|-------|--------------|---------------|-------|-----| +| ensv2 | `v1_domains` | has | registrars | `registrations` | `domain_id` | +| ensv2 | `v2_domains` | has | registrars | `registrations` | `domain_id` | +| ensv2 | `v1_domains` | maps to | subgraph | `subgraph_domains` | `id` (node) | +| ensv2 | `v2_domains` | maps to | subgraph | `subgraph_domains` | `id` (node) | +| protocol-acceleration | `resolver_records` | resolves | ensv2 | `v1_domains` | `node` | +| protocol-acceleration | `resolver_records` | resolves | ensv2 | `v2_domains` | `node` | +| tokenscope | `name_tokens` | represents | ensv2 | `v1_domains` | `domain_id` | +| tokenscope | `name_tokens` | represents | ensv2 | `v2_domains` | `domain_id` | +| registrars | `registration_lifecycles` | tracks | ensv2 | `v1_domains` | `node` | + +## Multiple Instances + +Multiple [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema) can exist in one [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance), each containing all five sub-schemas: + +```mermaid +flowchart TB + subgraph ENSDb["ENSDb Instance"] + ENSNODE["ensnode.metadata
(tracks all)"] + + subgraph Mainnet["ensindexer_mainnet"] + M1["ensv2.*"] + M2["protocol-acceleration.*"] + M3["registrars.*"] + M4["subgraph.*"] + M5["tokenscope.*"] + end + + subgraph L2["ensindexer_l2"] + L1["ensv2.*"] + L2a["protocol-acceleration.*"] + L3["registrars.*"] + L4["subgraph.*"] + L5["tokenscope.*"] + end + + subgraph Custom["ensindexer_custom"] + C1["ensv2.*"] + C2["protocol-acceleration.*"] + C3["registrars.*"] + C4["subgraph.*"] + C5["tokenscope.*"] + end + end + + ENSNODE -->|tracks| Mainnet + ENSNODE -->|tracks| L2 + ENSNODE -->|tracks| Custom +``` + +Each [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance): +- Owns exactly one [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) with all five sub-schemas +- Has its own [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name) +- Can use a different [Schema Definition](/ensdb/concepts/glossary#schema-definition) version +- Has its own row in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) + +## Schema Versioning + +Each schema has a [Schema Version](/ensdb/concepts/glossary#schema-version) that changes when the [Schema Definition](/ensdb/concepts/glossary#schema-definition) changes. + +| Schema | Version Stored In | +|--------|------------------| +| [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) | Stored in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) as a metadata key | +| [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) | [ENSIndexer Schema Version](/ensdb/concepts/glossary#ensindexer-schema-version) (stored in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) `value` column) | + +[Schema Versions](/ensdb/concepts/glossary#schema-version) enable programmatic detection of schema compatibility. A consumer can verify that their code expects the correct schema structure before querying. + +See [Glossary: Schema Version](/ensdb/concepts/glossary#schema-version) for the concept definition. + +## Related Concepts + +- **[Glossary](/ensdb/concepts/glossary)** — All terminology definitions +- **[Architecture](/ensdb/concepts/architecture)** — How schemas relate and data flows +- **[Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle)** — How indexing affects database behavior diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/glossary.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/glossary.mdx new file mode 100644 index 0000000000..2a7e1d6f32 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/concepts/glossary.mdx @@ -0,0 +1,219 @@ +--- +title: Glossary +description: Core terminology used throughout ENSDb documentation. +sidebar: + label: Glossary + order: 1 +keywords: [ensdb, glossary, terminology, definitions] +--- + +import { Aside } from '@astrojs/starlight/components'; + +This page defines the **core terminology** used throughout ENSDb documentation. If you encounter an unfamiliar term elsewhere, check here for its definition. + +## PostgreSQL Concepts + +### PostgreSQL Server + +A running PostgreSQL server process that manages one or more [databases](#postgresql-database). + +### PostgreSQL Database + +A PostgreSQL database is an isolated collection of [database objects](#database-objects) managed by a [PostgreSQL server](#postgresql-server). + +### Database Schema + +A container that groups related [database objects](#database-objects). There can be multiple database schemas within a single [PostgreSQL database](#postgresql-database). Each database schema has a unique name within the [PostgreSQL database](#postgresql-database). + + + +### Database Objects + +Objects contained within a [database schema](#database-schema), including: + +- Tables +- Enums +- Indexes +- Constraints +- Relations + +## ENSDb Standard + +An open standard for bi-directional ENS integration. The ENSDb Standard defines: +- Schema designs for storing ENS data in a [PostgreSQL database](#postgresql-database) +- Rules and constraints for writers and readers +- A modular architecture enabling interoperability + +Any [PostgreSQL database](#postgresql-database) following the ENSDb Standard is an [ENSDb instance](#ensdb-instance). + +## ENSDb Concepts + +### Schema Definition + +For ENSDb, a Drizzle ORM object that defines the structure of [database objects](#database-objects) within a [database schema](#database-schema). Schema Definitions specify: + +- Tables and their columns +- Column types +- Enums +- Indexes +- Constraints +- Relations + +### Schema Checksum + +A deterministic checksum computed from a [Schema Definition](#schema-definition). Used to detect when the schema structure and/or semantics change. + +### ENSDb Instance + +A [PostgreSQL database](#postgresql-database) that follows the [ENSDb Standard](#ensdb-standard). An ENSDb instance stores indexed ENS data and is composed of three types of database schemas: + +- Exactly one [Ponder Schema](#ponder-schema) +- Exactly one [ENSNode Schema](#ensnode-schema) +- One or more [ENSIndexer Schemas](#ensindexer-schema) + +Multiple [ENSDb instances](#ensdb-instance) can be hosted on the same [PostgreSQL server](#postgresql-server). For example: +- One ENSDb instance could be used for production workloads and include data for the "mainnet" ENS Namespace. +- At the same time, another ENSDb instance could be used for testing workloads and include data for the "sepolia" ENS Namespace. +- At the same time, another ENSDb instance could be used for local development workloads and include data for the "ens-test-env" ENS Namespace. + +#### Multi-Tenant ENSDb instance + +An [ENSDb instance](#ensdb-instance) is multi-tenant because it can store data from multiple [ENSIndexer instances](#ensindexer-instance) (tenants) in isolated [ENSIndexer Schemas](#ensindexer-schema). + +Within a single [ENSDb instance](#ensdb-instance): +- Each [ENSIndexer instance](#ensindexer-instance) gets its own [ENSIndexer Schema](#ensindexer-schema) — fully isolated data following the [ENSIndexer Schema Definition](#schema-definition) that was current when the ENSIndexer instance started. +- All [ENSIndexer instances](#ensindexer-instance) share the [Ponder Schema](#ponder-schema), including the RPC cache. +- Metadata of all [ENSIndexer instances](#ensindexer-instance) is tracked in the [ENSNode Schema](#ensnode-schema) + +This enables separate indexing by multiple [ENSIndexer instance](#ensindexer-instance) with different configs. The configs may require indexing just certain chains. For example, one [ENSIndexer instance](#ensindexer-instance) is configured to index data just from the Ethereum Mainnet, while another [ENSIndexer instance](#ensindexer-instance) is configured to index data from both, Ethereum Mainnet, and Base Mainnet. Each of these [ENSIndexer instances](#ensindexer-instance) would have its own [ENSIndexer Schema](#ensindexer-schema) in the same [ENSDb instance](#ensdb-instance). + +### Ponder runtime + +Ponder runtime is executed by the ENSIndexer instance. It manages fetching onchain data, and having it cached in RPC cache inside the [Ponder Schema](#ponder-schema). The Ponder runtime also performs writes to the [ENSIndexer Schema](#ensindexer-schema) owned by the ENSIndexer instance. + +### Ponder Schema + +A [database schema](#database-schema) in the [ENSDb instance](#ensdb-instance) following the [Ponder Schema Definition](#ponder-schema-definition). It has a fixed `ponder_sync` name, and it serves as a shared RPC cache for all [ENSIndexer instances](#ensindexer-instance) connected to the [ENSDb instance](#ensdb-instance). It's lifecycle is managed by Ponder runtime. + +The Ponder Schema enables multiple [ENSIndexer instances](#ensindexer-instance) to share RPC cache, reducing costs and improving indexing speed. + +#### Ponder Schema Definition + +A [Schema Definition](#schema-definition) that defines the structure of the [Ponder Schema](#ponder-schema) in the [ENSDb instance](#ensdb-instance). This Schema Definition is an implementation detail of Ponder, so we treat is as an opaque black box in ENSDb documentation. + +### ENSNode Schema + +A [database schema](#database-schema) in the [ENSDb instance](#ensdb-instance) following the [ENSNode Schema Definition](#ensnode-schema-definition). It has a fixed `ensnode` name, and it serves as a registry for all [ENSIndexer instances](#ensindexer-instance) that have ever connected to the [ENSDb instance](#ensdb-instance). It also stores metadata about these [ENSIndexer instances](#ensindexer-instance). + +### ENSNode Schema Definition + +A [Schema Definition](#schema-definition) that defines the structure of the [ENSNode Schema](#ensnode-schema) in the [ENSDb instance](#ensdb-instance). The ENSNode Schema Definition is part of the ENSDb standard and is maintained in the ENSDb SDK. It includes the definition of the [ENSNode Metadata Table](#ensnode-metadata-table). + +### ENSIndexer Schema + +A [database schema](#database-schema) within an [ENSDb instance](#ensdb-instance), used to store indexed ENS data. Each [ENSIndexer instance](#ensindexer-instance) owns exactly one ENSIndexer Schema, whose structure follows the [ENSIndexer Schema Definition](#ensindexer-schema-definition) that was current when the instance first started. The schema's name is determined at startup, when the ENSIndexer instance connects to the ENSDb instance and creates its schema using the configured [ENSIndexer Schema Name](#ensindexer-schema-name). + +#### ENSIndexer Schema Definition + +A [Schema Definition](#schema-definition) that defines the structure of an [ENSIndexer Schema](#ensindexer-schema). It is part of the ENSDb standard, maintained in the [ENSDb SDK](#ensdb-sdk), and specifies the core tables and columns used to store indexed ENS data. + +### ENSIndexer Schema Name + +The name of a specific [ENSIndexer Schema](#ensindexer-schema) in the [ENSDb instance](#ensdb-instance). This name is dynamic and determined by the [ENSIndexer instance](#ensindexer-instance) that owns the schema. + +Multiple [ENSIndexer Schema Names](#ensindexer-schema-name) exist in an [ENSDb instance](#ensdb-instance) when multiple [ENSIndexer instances](#ensindexer-instance) are connected. + +### ENSNode Metadata Table + +A table within the [ENSNode Schema](#ensnode-schema) that tracks all [ENSIndexer instances](#ensindexer-instance) that have ever connected to the [ENSDb instance](#ensdb-instance). + +| Column | Type | Purpose | +|--------|------|---------| +| `ens_indexer_schema_name` | `text` | References the [ENSIndexer Schema Name](#ensindexer-schema-name) of the ENSIndexer instance that manages this metadata record | +| `key` | `text` | Type of metadata record | +| `value` | `jsonb` | The context object (configuration, status, etc.) | + +Primary key: (`ens_indexer_schema_name`, `key`) + +The `value` column stores a JSON object which structure may evolve over time. To track this, the JSON object is guaranteed to always include a `version` field indicating the version of the structure. This allows for future-proofing as the metadata needs evolve. + +### Metadata Keys + +The `key` column identifies the type of metadata: + +| Key | Description | +|-----|-------------| +| `indexing_metadata_context` | [Indexing metadata context](#indexing-metadata-context) of the [ENSIndexer instance](#ensindexer-instance) | + +#### Indexing Metadata Context + +A JSON object that provides context about the ENSNode stack the Indexing Status. It includes: +- `version`: The version of the Indexing Metadata Context structure +- `data`: The actual context data, whose structure may evolve over time as the needs of the ENSNode stack evolve. The `data` object may include fields such as: + - `indexingStatus`: the current Indexing Status of the [ENSIndexer instance](#ensindexer-instance). + - `ensDbPublicConfig`: the public config of the [ENSDb instance](#ensdb-instance) that this [ENSIndexer instance](#ensindexer-instance) is connected to. + - `ensIndexerPublicConfig`: the public config of the [ENSIndexer instance](#ensindexer-instance) that this metadata record belongs to. + - `ensRainbowPublicConfig`: the public config of the ENSRainbow instance that this [ENSIndexer instance](#ensindexer-instance) is connected to. Might be `null` during the ENSNode stack cold start, when the ENSIndexer instance starts before the ENSRainbow instance. + +### ENSApi Instance + +A running ENSApi process that serves GraphQL and REST APIs from an [ENSDb instance](#ensdb-instance). + +- Any [ENSApi instance](#ensapi-instance) can connect to any [ENSDb instance](#ensdb-instance) +- Multiple [ENSApi instances](#ensapi-instance) can connect to the same [ENSDb instance](#ensdb-instance) for improved availability + +### ENSIndexer Instance + +A running ENSIndexer process that indexes onchain ENS data and writes to an [ENSIndexer Schema](#ensindexer-schema) in ENSDb. + +- Each [ENSIndexer instance](#ensindexer-instance) owns exactly one [ENSIndexer Schema](#ensindexer-schema) +- Multiple [ENSIndexer instances](#ensindexer-instance) can connect to one [ENSDb instance](#ensdb-instance) +- Each ENSIndexer instance has a row in the [ENSNode Metadata Table](#ensnode-metadata-table) + +### Indexing Status + +The status of an [ENSIndexer instance](#ensindexer-instance)'s indexing progress. + +The Indexing Status affects database behavior: + +- During **Backfill**, [indexes](#database-objects) on the [ENSIndexer Schema](#ensindexer-schema), if any, are dropped to optimize write performance. +- When transitioning to **Following**, [indexes](#database-objects) on the [ENSIndexer Schema](#ensindexer-schema) are created to optimize read performance. + +## Schema Discovery + +The process of finding all [ENSIndexer Schemas](#ensindexer-schema) in an [ENSDb instance](#ensdb-instance) by querying the [ENSNode Metadata Table](#ensnode-metadata-table): + +```sql +SELECT DISTINCT ens_indexer_schema_name +FROM ensnode.metadata; +``` + +This returns all [ENSIndexer Schema Names](#ensindexer-schema-name) that have been used by [ENSIndexer instances](#ensindexer-instance) connected to this [ENSDb instance](#ensdb-instance). + +## ENSDb SDK + + +A TypeScript package published as [`@ensnode/ensdb-sdk`](https://www.npmjs.com/package/@ensnode/ensdb-sdk) providing utilities for interacting with the [ENSDb instance](#ensdb-instance). + +The ENSDb SDK includes: + +- [EnsDbReader](#ensdb-reader) — Reader implementation for querying ENSDb Metadata, and building custom queries against the [ENSDb instance](#ensdb-instance). +- [EnsDbWriter](#ensdb-writer) — Writer implementation for ENSNode Metadata and ENSNode migrations. +- [Schema Definitions](#schema-definition) — Drizzle schemas for [ENSIndexer Schema](#ensindexer-schema) and [ENSNode Schema](#ensnode-schema) +- [Schema Checksums](#schema-checksum) — Deterministic checksums for ENSIndexer and ENSNode schema definitions. + + +### ENSDb Reader + +A class in [ENSDb SDK](#ensdb-sdk) for querying data from the [ENSDb instance](#ensdb-instance). + +### ENSDb Writer + +A class in [ENSDb SDK](#ensdb-sdk) that extends [ENSDb Reader](#ensdb-reader) with write capabilities. + +## Related Documentation + +- [Architecture](/ensdb/concepts/architecture) — How schemas relate and data flows +- [Database Schemas](/ensdb/concepts/database-schemas) — Deep dive on schema types diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/index.mdx new file mode 100644 index 0000000000..134fecf4b9 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/concepts/index.mdx @@ -0,0 +1,63 @@ +--- +title: ENSDb Concepts +description: Understanding the fundamentals of ENSDb architecture, schemas, and data flow. +sidebar: + label: Concepts + order: 3 +--- + +import { LinkCard } from '@astrojs/starlight/components'; + +This section explains the core concepts of ENSDb. Understanding these fundamentals will help you work effectively with ENSDb, whether you're querying data, integrating with third-party services, or operating an ENSNode instance. + +## Core Concepts + + + + + + + + + +## What is ENSDb? + +ENSDb is an **open standard** for bi-directional ENS integration. It represents a new category of integration point for building on ENS: + +- **Open Standard** — Anyone can build writers or readers following the schema specifications +- **Bi-Directional** — Write operations produce an ENSDb, read operations consume it +- **Language Agnostic** — Any programming language with PostgreSQL support +- **Complete State** — The entire onchain ENS state, in your database + +Read the [What is ENSDb?](/ensdb/) overview for the full vision. + +## Who Should Read This + +- **Developers integrating with ENSDb** — Understand the schema structure to write effective queries +- **DevOps operators running ENSNode** — Understand how ENSDb behaves during different indexing phases +- **Anyone building custom writers or readers** — Learn the architecture for implementing the standard + +## Next Steps + +After understanding these concepts: + +1. **[Query ENSDb with SQL](/ensdb/usage/querying)** — Language-agnostic SQL examples +2. **[Use the TypeScript SDK](/ensdb/usage/ensdb-sdk/)** — Type-safe database access +3. **[Build an Integration](/ensdb/integrations/)** — Create custom writers or readers +4. **[Explore Use Cases](/ensdb/use-cases/)** — See what others are building diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/indexing-lifecycle.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/indexing-lifecycle.mdx new file mode 100644 index 0000000000..1904099ecb --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/concepts/indexing-lifecycle.mdx @@ -0,0 +1,170 @@ +--- +title: Indexing Lifecycle +description: How ENSIndexer processes data, from Backfill to Following, and how it affects database behavior. +sidebar: + label: Indexing Lifecycle + order: 5 +--- + +An [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) processes onchain data in distinct phases. Understanding the [Indexing Status](/ensdb/concepts/glossary#indexing-status) lifecycle helps explain database behavior changes, particularly around index creation and deletion. + +For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). + +## Indexing Status + +An [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) is always in one of two [Indexing Status](/ensdb/concepts/glossary#indexing-status) states: + +| Status | Description | +|--------|-------------| +| **backfill** | Processing historical events from genesis to current block | +| **following** | Caught up to chain tip, processing new events as they occur | + +The current status is tracked in the [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table). + +## Lifecycle Flow + +```mermaid +flowchart TD + Start["ENSIndexer Start"] --> Backfill + Backfill["Backfill
Process Historical Events"] -->|Caught up to chain tip| Following + Following["Following
Process New Events"] -->|Restart| Backfill +``` + +### Backfill Phase + +During backfill, ENSIndexer processes all historical events from the beginning of the chain: + +1. Scans blocks from genesis to current block +2. For each event matching the indexing filter: + - Reads cached RPC from Ponder Schema (or fetches and caches) + - Transforms data according to indexing logic + - Writes to ENSIndexer Schema +3. Updates indexing status in ENSNode Metadata + +**Database behavior during backfill:** + +- **[Indexes](/ensdb/concepts/glossary#database-objects) are dropped** on [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) tables +- Reason: Writing millions of rows with indexes is significantly slower +- Trade-off: Queries are slower during backfill, but backfill completes faster + +### Following Phase + +Once caught up to the chain tip, an [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) transitions to following: + +1. Listens for new blocks +2. For each new event matching the indexing filter: + - Reads cached RPC from [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) (or fetches and caches) + - Transforms data according to indexing logic + - Writes to [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) +3. Updates [Indexing Status](/ensdb/concepts/glossary#indexing-status) in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) + +**Database behavior during following:** + +- **[Indexes](/ensdb/concepts/glossary#database-objects) are created** on [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) tables +- Reason: Read queries should be fast for serving API requests +- Trade-off: Writes are slightly slower, but queries are fast + +## Index Management + +The transition between Backfill and Following triggers index management: + +```mermaid +flowchart LR + subgraph BackfillState["Status: Backfill"] + SchemaNoIndex1["ENSIndexer Schema
(no indexes)"] + end + + subgraph FollowingState["Status: Following"] + SchemaWithIndex1["ENSIndexer Schema
(has indexes)"] + end + + subgraph FollowingState2["Status: Following"] + SchemaWithIndex2["ENSIndexer Schema
(has indexes)"] + end + + subgraph BackfillState2["Status: Backfill"] + SchemaNoIndex2["ENSIndexer Schema
(no indexes)"] + end + + SchemaNoIndex1 -->|create indexes| SchemaWithIndex1 + SchemaWithIndex2 -->|drop indexes| SchemaNoIndex2 +``` + +### Why This Matters + +**For query consumers:** +- Queries during Backfill will be slower (no indexes) +- Consider waiting for Following status before running heavy queries +- Check [Indexing Status](/ensdb/concepts/glossary#indexing-status) via [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) + +```sql +SELECT value->>'status' as status +FROM ensnode.metadata +WHERE ens_indexer_schema_name = 'ensindexer_abc123' + AND key = 'ensindexer_indexing_status'; +``` + +**For database operators:** +- Backfill generates high write load +- Following generates moderate write load + read load +- Plan resource allocation accordingly + +## Restart Behavior + +When an ENSIndexer instance restarts: + +1. **If previously Following:** + - Drops indexes on [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) + - Enters Backfill to verify/repair any missed events + - Re-creates indexes when caught up to Following + +2. **Reasons for restart:** + - Configuration change + - Chain reorganization + - Software update + - Manual intervention + +The [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) RPC cache persists across restarts, reducing the cost of re-backfilling. + +## Querying During Lifecycle + +### Checking Status + +```sql +SELECT + ens_indexer_schema_name, + value->>'status' as status, + value->>'progress' as progress +FROM ensnode.metadata +WHERE key = 'ensindexer_indexing_status'; +``` + +### Safe Querying + +For production queries, verify the [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) is in Following [Indexing Status](/ensdb/concepts/glossary#indexing-status): + +```sql +-- Only query if following +DO $$ +DECLARE + status text; +BEGIN + SELECT value->>'status' INTO status + FROM ensnode.metadata + WHERE ens_indexer_schema_name = 'ensindexer_abc123' + AND key = 'ensindexer_indexing_status'; + + IF status != 'following' THEN + RAISE EXCEPTION 'ENSIndexer not in following status'; + END IF; +END $$; + +-- Now safe to run indexed queries +SELECT * FROM ensindexer_abc123.v1_domains WHERE ...; +``` + +## Related Concepts + +- **[Glossary](/ensdb/concepts/glossary)** — [Indexing Status](/ensdb/concepts/glossary#indexing-status), [ENSIndexer Instance](/ensdb/concepts/glossary#ensindexer-instance) definitions +- **[Database Schemas](/ensdb/concepts/database-schemas)** — [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) structure +- **[Architecture](/ensdb/concepts/architecture)** — Data flow through ENSDb diff --git a/docs/ensnode.io/src/content/docs/ensdb/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/index.mdx index 55dc1f3656..586fb07fa4 100644 --- a/docs/ensnode.io/src/content/docs/ensdb/index.mdx +++ b/docs/ensnode.io/src/content/docs/ensdb/index.mdx @@ -1,7 +1,260 @@ --- -title: ENSDb Overview +title: Getting started with ENSDb +description: ENSDb is an open standard for bi-directional ENS integration. An ENSDb instance is a PostgreSQL database served from a PostgreSQL server — and a single server can serve multiple ENSDb instances. +sidebar: + label: Overview + order: 1 --- -:::caution[Work in Progress] -This documentation is under active development. -::: +import { LinkCard, Aside, Card, CardGrid } from '@astrojs/starlight/components'; + +## Vision + +Getting the whole onchain state of ENS in your database. + +## Core Philosophy + +### Open Standard + +ENSDb is an open standard for bi-directional ENS integration. It defines a carefully crafted set of database schema designs, rules, and constraints for storing the entire ENS onchain state in a PostgreSQL database — making the data accessible from any programming language. + +Any app following the standard can: +- Perform write operations to produce an ENSDb instance. +- Perform read operations against the ENSDb instance. + + + +### Reference Implementation + +ENSNode is the reference implementation of the ENSDb standard, providing a complete ecosystem of tools and services for building with ENSDb. Each ENSNode instance includes: +- [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance) — The PostgreSQL database following the ENSDb standard +- [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) — The reference ENSDb Writer implementation that indexes onchain ENS data +- [ENSApi instance](/ensdb/concepts/glossary#ensapi-instance) — The reference ENSDb Reader implementation that serves GraphQL and REST APIs + +```mermaid +flowchart LR + subgraph ENSNode["ENSNode Environment"] + subgraph ENSNodeMainnet["ENSNode 'Mainnet' instance"] + direction LR + ENSIndexerMainnet[ENSIndexer 'Mainnet' instance] + ENSApiMainnet@{ shape: procs, label: "ENSApi Sepolia instances" } + end + + subgraph ENSNodeSepolia["ENSNode 'Sepolia' instance"] + direction LR + ENSIndexerSepolia[ENSIndexer 'Sepolia' instance] + ENSApiSepolia[ENSApi 'Sepolia' instance] + end + end + + subgraph PostgreSQLServer["PostgreSQL Server instance"] + ENSDbMainnet[(ENSDb 'Mainnet' instance)] + PSMainnet(Ponder Schema) + NSMainnet(ENSNode Schema) + EISMainnet@{ shape: procs, label: "ENSIndexer Schema" } + + ENSDbMainnet -->|1..1| PSMainnet + ENSDbMainnet -->|1..1| NSMainnet + ENSDbMainnet -->|1..*| EISMainnet + + + ENSDbSepolia[(ENSDb 'Sepolia' instance)] + PSSepolia(Ponder Schema) + NSSepolia(ENSNode Schema) + EISSepolia@{ shape: procs, label: "ENSIndexer Schema" } + + ENSDbSepolia -->|1..1| PSSepolia + ENSDbSepolia -->|1..1| NSSepolia + ENSDbSepolia -->|1..*| EISSepolia + end + + ENSIndexerMainnet -->|Write| ENSDbMainnet + ENSIndexerSepolia -->|Write| ENSDbSepolia + ENSApiMainnet -->|Read| ENSDbMainnet + ENSApiSepolia -->|Read| ENSDbSepolia + +``` + + + +## What is an ENSDb Instance? + +An **ENSDb instance** is a PostgreSQL database that follows the ENSDb open standard. Key characteristics: + +| Aspect | Description | +|--------|-------------| +| **What it is** | A PostgreSQL database (logical database within a server) | +| **Where it runs** | Served from a PostgreSQL server | +| **Multi-tenancy** | One ENSDb instance can store data from multiple ENSIndexer instances (tenants) | +| **Contains** | Complete indexed ENS state | + +### Example: Multi-Instance Server + +A single PostgreSQL server can serve multiple ENSDb instances for different environments: + +```mermaid +flowchart TB + subgraph PGServer["PostgreSQL Server (localhost:5432)"] + direction TB + Mainnet["ensdb_mainnet
← Production environment (mainnet data)"] + Testnet["ensdb_testnet
← Pre-production environment (testnet data)"] + Devnet["ensdb_devnet
← Staging / local development environment"] + end +``` + +Each ENSDb instance is an independent database containing complete ENS data for its respective environment. + +## What You Get + +### Complete ENS State + +ENSDb contains the **entire onchain state of ENS**: + +- All domains (ENSv1 and ENSv2) +- All registrations and renewals +- All resolver records and text records +- All events and ownership history +- All NFT/token data for names + + +### PostgreSQL Benefits + +By building on a PostgreSQL database, ENSDb inherits world-class capabilities: + +- **ACID transactions** — Data integrity guarantees +- **Complex queries** — Joins, aggregations, window functions +- **Scalability** — Replication, sharding, connection pooling +- **Ecosystem** — Mature tools, ORMs, dashboards, analytics platforms +- **Reliability** — Decades of production-proven technology + +## What You Can Build + +ENSDb unlocks a new universe of ENS applications: + + + +Build specialized GraphQL or REST APIs tailored to your use case. Query exactly the data you need with full SQL power. + + +Create real-time dashboards and analytics pipelines. Better than Dune — you have the full ENS state locally with sub-second query latency. + + +Build command-line tools for ENS operations. Query domains, check expiration, analyze name patterns — all from your terminal. + + +Build reactive systems that respond to ENS state changes. Monitor registration lifecycles, ownership transfers, resolver updates. + + +Feed ENS data into your existing data infrastructure. Sync to data warehouses, trigger webhooks, populate search indexes. + + +Train machine learning models on complete ENS datasets. Predict name values, detect patterns, analyze market trends. + + + +## Quick Start + +### Connect with Any PostgreSQL Client + +Connect to an ENSDb instance (a PostgreSQL database). The examples below assume you that ENSDb instances are served from a PostgreSQL server at `host:5432` with databases named `ensdb_mainnet`, `ensdb_testnet`, and `ensdb_devnet`: + +```bash +# Production environment (mainnet data) +psql postgresql://user:password@host:5432/ensdb_mainnet + +# Pre-production environment (testnet data) +psql postgresql://user:password@host:5432/ensdb_testnet + +# Staging / local development environment +psql postgresql://user:password@host:5432/ensdb_devnet +``` + +### Discover Available Schemas + +Once connected to an ENSDb instance, discover its ENSIndexer Schemas: + +```sql +SELECT DISTINCT ens_indexer_schema_name +FROM ensnode.metadata; +``` + +### Query Indexed Data + +Query data from a specific ENSIndexer Schema within your ENSDb instance: + +```sql +-- Get domains from the ENSIndexer instance using the `ensindexer_mainnet` ENSIndexer Schema Name +SELECT * FROM ensindexer_mainnet.v1_domains LIMIT 10; + +-- Get indexing status for the ENSIndexer instance using the `ensindexer_mainnet` ENSIndexer Schema Name +SELECT * FROM "ensnode"."metadata" +WHERE ens_indexer_schema_name = 'ensindexer_mainnet' +AND key = 'ensindexer_indexing_status' +AND value -> 'data' -> 'omnichainSnapshot' ->> 'omnichainStatus' = 'omnichain-following'; + +``` + +### Use the TypeScript SDK + +For TypeScript projects, the [ENSDb SDK](/ensdb/concepts/glossary#ensdb-sdk) provides typed access: + +```typescript +import { EnsDbReader } from '@ensnode/ensdb-sdk'; + +// Connect to a specific ENSDb instance by providing its connection string and the ENSIndexer Schema Name you want to query +const ensDbReader = new EnsDbReader(ensDbConnectionString, ensIndexerSchemaName); +const v1Domains = await + ensDbReader.ensDb.select() + .from(ensDbReader.ensIndexerSchema.v1Domain) + .limit(10); + .limit(10); +``` + +## Documentation Structure + + + + + + + + + + + +## Get Started + +- **[Query ENSDb with SQL](/ensdb/usage/querying)** — Language-agnostic SQL examples +- **[Use the TypeScript SDK](/ensdb/usage/ensdb-sdk/)** — Type-safe database access +- **[Learn the Architecture](/ensdb/concepts/architecture)** — How schemas relate and data flows +- **[Build an Integration](/ensdb/integrations/)** — Create custom writers or readers + +## Related Projects + +- **[ENSIndexer](/ensindexer/)** — The reference writer implementation +- **[ENSApi](/ensapi/)** — The reference reader implementation +- **[ENSRainbow](/ensrainbow/)** — Label healing service for ENSIndexer diff --git a/docs/ensnode.io/src/content/docs/ensdb/integrations/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/integrations/index.mdx new file mode 100644 index 0000000000..b2accbabcf --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/integrations/index.mdx @@ -0,0 +1,289 @@ +--- +title: Building ENSDb Integrations +description: Build custom writers and readers that follow the ENSDb open standard. Create indexing solutions, APIs, analytics tools, and more in any programming language. +sidebar: + label: Overview + order: 6 +--- + +import { LinkCard, Aside, Card, CardGrid, Steps } from '@astrojs/starlight/components'; + +ENSDb is an **open standard** — anyone can build applications that write to or read from an ENSDb instance. This guide explains how to build custom integrations that follow the standard. + +## The Writer/Reader Pattern + +ENSDb follows a **bi-directional integration pattern**: + +```mermaid +flowchart TB + subgraph WriteSide["Write Side"] + Onchain["Onchain ENS State"] + Writer["Writer Implementation
(You Build This)"] + end + + subgraph ENSDb["ENSDb Standard
(PostgreSQL Database)"] + PS[(Ponder Schema)] + NS[(ENSNode Schema)] + IS[(ENSIndexer Schema)] + end + + subgraph ReadSide["Read Side"] + Reader["Reader Implementation
(You Build This)"] + Apps["Applications"] + end + + Onchain -->|Index| Writer + Writer -->|Write| ENSDb + ENSDb -->|Read| Reader + Reader -->|Serve| Apps +``` + +| Role | Responsibility | Example Implementations | +|------|----------------|------------------------| +| **Writer** | Indexes onchain ENS data and writes to ENSDb | ENSIndexer, Custom Indexers | +| **Reader** | Queries ENSDb and serves data to applications | ENSApi, Custom APIs, Dashboards | + + + +## Building a Writer + +A writer is responsible for: +1. Reading onchain ENS events +2. Transforming data according to ENSDb schema definitions +3. Writing to an ENSIndexer Schema +4. Updating ENSNode Schema metadata + + + +### When to Build a Writer + +- You need to index custom ENS-compatible contracts +- You want to index a new chain not covered by ENSIndexer +- You need specialized indexing logic for your use case +- You want to add custom tables to the ENSIndexer Schema + +## Building a Reader + +A reader is responsible for: +1. Querying ENSDb (ENSNode Schema for discovery, ENSIndexer Schema for data) +2. Transforming database results for your application +3. Serving data via your preferred interface (API, CLI, dashboard, etc.) + + + +### When to Build a Reader + +- You need a specialized API for your application +- You're building analytics or dashboard tools +- You're creating a CLI for ENS operations +- You need real-time streaming of ENS state changes + +## Schema Compliance + +To be ENSDb-compliant, your implementation must follow the standard schema definitions: + + + +Required metadata structure for tracking ENSIndexer instances + + +Modular schema with 5 sub-schemas: ensv2, protocol-acceleration, registrars, subgraph, tokenscope + + +Shared RPC cache structure (if using Ponder-based indexing) + + + +See [Database Schemas](/ensdb/concepts/database-schemas/) for complete schema documentation. + +## Language Support + +You can build ENSDb integrations in **any programming language** with a PostgreSQL driver: + +| Language | Popular Drivers | Use Cases | +|----------|-----------------|-----------| +| **TypeScript** | `pg`, `drizzle-orm`, `kysely` | APIs, dashboards, web apps | +| **Python** | `psycopg2`, `asyncpg`, `sqlalchemy` | Analytics, ML, data pipelines | +| **Go** | `pgx`, `database/sql`, `sqlx` | High-performance services, CLIs | +| **Rust** | `tokio-postgres`, `sqlx`, `diesel` | Systems programming, performance | +| **Java** | `JDBC`, `jOOQ`, `Hibernate` | Enterprise applications | +| **Ruby** | `pg`, `ActiveRecord` | Web applications | + +## SDKs and Tools + +### Official SDK + +The [ENSDb SDK](/ensdb/usage/ensdb-sdk/) (`@ensnode/ensdb-sdk`) provides: +- Schema definitions (Drizzle ORM) +- TypeScript types for all tables +- Reader/Writer client classes +- Schema validation utilities + +### Building Without the SDK + +You can build ENSDb integrations without the TypeScript SDK: + +1. **Study the schema** — Use [Database Schemas](/ensdb/concepts/database-schemas/) as reference +2. **Implement in your language** — Create equivalent schema definitions +3. **Follow the conventions** — Use the same table names, column types, and relationships +4. **Test for compatibility** — Verify your implementation works with existing readers/writers + +## Integration Architecture Patterns + +### Pattern 1: Co-located Writer and Reader + +Writer and reader run in the same process, sharing a database connection: + +```mermaid +flowchart TB + subgraph App["Your Application"] + direction LR + Writer["Writer
(Indexer)"] + Reader["Reader
(API)"] + end + + ENSDb["ENSDb
(SQLite)"] + + Writer --> ENSDb + Reader --> ENSDb +``` + +**Best for**: Local development, testing, embedded applications + +### Pattern 2: Separate Writer and Reader + +Writer and reader are separate processes connecting to a shared ENSDb: + +```mermaid +flowchart TB + Writer["Writer
(Indexer)"] + Reader["Reader
(API)"] + ENSDb["ENSDb
(PostgreSQL)"] + + Writer --> ENSDb + Reader --> ENSDb +``` + +**Best for**: Production deployments, microservices, scaled architectures + +### Pattern 3: Multiple Readers + +One writer, multiple specialized readers: + +```mermaid +flowchart TB + Writer["Writer
(Indexer)"] + ReaderGraphQL["Reader
(GraphQL)"] + ReaderAnalytics["Reader
(Analytics)"] + ReaderCLI["Reader
(CLI)"] + ENSDb["ENSDb"] + + Writer --> ReaderGraphQL + Writer --> ReaderAnalytics + Writer --> ReaderCLI + + ReaderGraphQL --> ENSDb + ReaderAnalytics --> ENSDb + ReaderCLI --> ENSDb +``` + +**Best for**: Multi-team organizations, diverse use cases + +### Pattern 4: Chain-Specific Writers + +Multiple writers indexing different chains, one shared ENSDb: + +```mermaid +flowchart TB + WriterMainnet["Writer
(Mainnet)"] + WriterL2["Writer
(L2 Chain)"] + ENSDb["ENSDb
(Shared)"] + Reader["Reader
(Multi-Chain API)"] + + WriterMainnet --> ENSDb + WriterL2 --> ENSDb + ENSDb --> Reader +``` + +**Best for**: Multi-chain ENS deployments, aggregated APIs + +## Compliance Checklist + +Before deploying your integration, verify: + +### For Writers + +- [ ] Creates ENSIndexer Schema with dynamic name +- [ ] Creates/updates ENSNode Schema metadata table +- [ ] Follows ENSIndexer Schema table definitions (all 5 sub-schemas) +- [ ] Updates indexing status in ENSNode metadata during operation +- [ ] Handles backfill vs following states appropriately (indexes) +- [ ] Supports schema versioning + +### For Readers + +- [ ] Queries ENSNode Schema for schema discovery +- [ ] Reads from ENSIndexer Schema for data queries +- [ ] Handles multiple ENSIndexer Schemas (multi-tenancy) +- [ ] Respects indexing status (warns if querying during backfill) +- [ ] Validates schema version compatibility + +## Next Steps + + +1. **Understand the schemas** + + Read [Database Schemas](/ensdb/concepts/database-schemas/) to understand the complete data model. + +2. **Choose your language** + + Pick a language with good PostgreSQL support for your use case. + +3. **Build a reader first** + + Start by querying an existing ENSDb to understand the data model. + +4. **Consider the SDK** + + For TypeScript, use `@ensnode/ensdb-sdk`. For other languages, port the schema definitions. + +5. **Test for compatibility** + + Ensure your implementation works with existing ENSDb tools. + + +## Getting Help + +- **[GitHub Discussions](https://github.com/namehash/ensnode/discussions)** — Ask questions about building integrations +- **[Discord](https://discord.gg/ensnode)** — Chat with the community +- **[Issue Tracker](https://github.com/namehash/ensnode/issues)** — Report bugs or request features + +## Related Documentation + + + + + + diff --git a/docs/ensnode.io/src/content/docs/ensdb/integrations/reader.mdx b/docs/ensnode.io/src/content/docs/ensdb/integrations/reader.mdx new file mode 100644 index 0000000000..30d5579066 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/integrations/reader.mdx @@ -0,0 +1,596 @@ +--- +title: Build a Custom Reader +description: Build applications that query ENSDb to serve ENS data. Learn schema discovery, query patterns, and how to build APIs, dashboards, CLIs, and more. +sidebar: + label: Build a Reader + order: 3 +--- + +import { Aside, Tabs, TabItem, Card, CardGrid } from '@astrojs/starlight/components'; + +A **reader** queries ENSDb and serves data to applications. This guide explains how to build custom readers that follow the ENSDb open standard. + +## What a Reader Does + +```mermaid +flowchart LR + ENSDb["ENSDb Instance
(PostgreSQL)"] + Reader["Your Reader
(Custom App)"] + Apps["End Users"] + + ENSDb -->|1. Discover Schemas| Reader + ENSDb -->|2. Query Data| Reader + Reader -->|3. Transform| Reader + Reader -->|4. Serve| Apps +``` + +1. **Discover** — Query ENSNode Schema to find available ENSIndexer Schemas +2. **Query** — Read from ENSIndexer Schema tables +3. **Transform** — Format data for your specific use case +4. **Serve** — Deliver via API, dashboard, CLI, or other interface + +## Types of Readers + +You can build various types of readers depending on your needs: + + + +REST, GraphQL, or gRPC APIs that serve ENS data to web and mobile apps. + + +Real-time analytics dashboards with visualizations and metrics. + + +Command-line tools for ENS operations and scripting. + + +SDKs and client libraries that wrap ENSDb queries. + + +Real-time event processing and data pipelines. + + +Data feeds for machine learning and analytics models. + + + +## Architecture Overview + +Your reader will primarily interact with: + +```mermaid +erDiagram + READER["Your Reader"] ||--o{ ENSNODE_METADATA : "discovers via" + READER ||--o{ ENSINDEXER_SCHEMA : "reads from" + + ENSNODE_METADATA { + text ens_indexer_schema_name + text key + jsonb value + } + + ENSINDEXER_SCHEMA["ensindexer_* Schema"] { + v1_domains table + v2_domains table + registrations table + events table + ... + } +``` + +### Two-Phase Query Pattern + +All readers follow a two-phase pattern: + +```mermaid +sequenceDiagram + participant Reader + participant ENSNode + participant ENSIndexer + + Reader->>+ENSNode: SELECT schema_name FROM metadata + ENSNode-->>-Reader: ensindexer_mainnet, ensindexer_l2, ... + + Reader->>+ENSIndexer: SELECT * FROM v1_domains + ENSIndexer-->>-Reader: Domain data +``` + +## Implementation Guide + +### Step 1: Set Up Your Project + +Create a new project with PostgreSQL connectivity: + + + +```bash +mkdir my-ensdb-reader +cd my-ensdb-reader +npm init -y +npm install pg @ensnode/ensdb-sdk express +``` + + +```bash +mkdir my-ensdb-reader +cd my-ensdb-reader +python -m venv venv +source venv/bin/activate +pip install psycopg2-binary fastapi uvicorn +``` + + +```bash +mkdir my-ensdb-reader +cd my-ensdb-reader +go mod init my-ensdb-reader +go get github.com/jackc/pgx/v5 +go get github.com/gin-gonic/gin +``` + + +```bash +mkdir my-ensdb-reader +cd my-ensdb-reader +cargo init +# Add tokio-postgres and axum to Cargo.toml +``` + + + +### Step 2: Connect to PostgreSQL + + + +```typescript +import { Pool } from 'pg'; + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +// Or use ENSDb SDK +import { EnsDbReader } from '@ensnode/ensdb-sdk'; + +const reader = new EnsDbReader( + process.env.DATABASE_URL!, + 'ensindexer_mainnet' // Schema to query +); +``` + + +```python +import psycopg2 +from psycopg2.extras import RealDictCursor + +conn = psycopg2.connect( + host="localhost", + database="ensdb", + user="postgres", + password="password" +) +``` + + +```go +import ( + "context" + "github.com/jackc/pgx/v5/pgxpool" +) + +pool, err := pgxpool.New(context.Background(), "postgresql://user:pass@localhost/ensdb") +if err != nil { + log.Fatal(err) +} +defer pool.Close() +``` + + +```rust +use tokio_postgres::Client; + +let (client, connection) = tokio_postgres::connect( + "host=localhost user=postgres dbname=ensdb", + tokio_postgres::NoTls, +).await?; +``` + + + +### Step 3: Discover Available Schemas + +Query ENSNode Schema to find available ENSIndexer instances: + + + +```typescript +// Using plain SQL +const result = await pool.query(` + SELECT DISTINCT ens_indexer_schema_name + FROM ensnode.metadata + ORDER BY ens_indexer_schema_name +`); + +const schemas = result.rows.map(r => r.ens_indexer_schema_name); +console.log('Available schemas:', schemas); +// ['ensindexer_mainnet', 'ensindexer_l2', 'ensindexer_custom'] +``` + + +```python +cursor = conn.cursor(cursor_factory=RealDictCursor) +cursor.execute(""" + SELECT DISTINCT ens_indexer_schema_name + FROM ensnode.metadata + ORDER BY ens_indexer_schema_name +""") +schemas = [row['ens_indexer_schema_name'] for row in cursor.fetchall()] +print(f"Available schemas: {schemas}") +``` + + +```go +rows, err := pool.Query(context.Background(), ` + SELECT DISTINCT ens_indexer_schema_name + FROM ensnode.metadata + ORDER BY ens_indexer_schema_name +`) +if err != nil { + log.Fatal(err) +} +defer rows.Close() + +var schemas []string +for rows.Next() { + var schema string + rows.Scan(&schema) + schemas = append(schemas, schema) +} +``` + + +```rust +let rows = client.query( + "SELECT DISTINCT ens_indexer_schema_name FROM ensnode.metadata ORDER BY ens_indexer_schema_name", + &[], +).await?; + +let schemas: Vec = rows.iter() + .map(|row| row.get(0)) + .collect(); +``` + + + +### Step 4: Check Indexing Status + +Before querying, verify the indexer is in a good state: + + + +```typescript +const statusResult = await pool.query(` + SELECT value->>'status' as status, + value->>'progress' as progress + FROM ensnode.metadata + WHERE ens_indexer_schema_name = $1 + AND key = 'ensindexer_indexing_status' +`, ['ensindexer_mainnet']); + +const status = statusResult.rows[0]; +if (status.status !== 'following') { + console.warn(`Warning: Indexer is in ${status.status} state (${status.progress}% complete)`); + console.warn('Query performance may be degraded'); +} +``` + + + + + +### Step 5: Query Domain Data + +Fetch domains from an ENSIndexer Schema: + + + +```typescript +// Query v1 domains +const domains = await pool.query(` + SELECT id, parent_id, owner_id, label_hash + FROM ensindexer_mainnet.v1_domains + WHERE owner_id = $1 + LIMIT 100 +`, ['\\x1234567890abcdef1234567890abcdef12345678']); + +// Query v2 domains with registry info +const v2Domains = await pool.query(` + SELECT d.id, d.token_id, d.owner_id, r.chain_id + FROM ensindexer_mainnet.v2_domains d + JOIN ensindexer_mainnet.registries r ON d.registry_id = r.id + WHERE d.owner_id = $1 +`, ['\\x1234567890abcdef1234567890abcdef12345678']); +``` + + +```python +# Query domains with label resolution +cursor.execute(""" + SELECT d.id, d.owner_id, l.interpreted as label + FROM ensindexer_mainnet.v1_domains d + LEFT JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash + WHERE d.owner_id = %s + LIMIT 100 +"", ('\\x1234567890abcdef1234567890abcdef12345678',)) + +domains = cursor.fetchall() +for domain in domains: + print(f"Domain: {domain['id']}, Label: {domain['label']}") +``` + + +```go +rows, err := pool.Query(context.Background(), ` + SELECT id, parent_id, owner_id, label_hash + FROM ensindexer_mainnet.v1_domains + WHERE owner_id = $1 + LIMIT 100 +`, ownerAddress) +if err != nil { + log.Fatal(err) +} +defer rows.Close() + +for rows.Next() { + var domain struct { + ID string + ParentID string + OwnerID string + LabelHash []byte + } + rows.Scan(&domain.ID, &domain.ParentID, &domain.OwnerID, &domain.LabelHash) + // Process domain... +} +``` + + + +### Step 6: Build Your Interface + +Wrap your queries in an API, CLI, or other interface: + + + +```typescript +import express from 'express'; + +const app = express(); + +// Get domains by owner +app.get('/api/domains/:owner', async (req, res) => { + const owner = req.params.owner; + + const result = await pool.query(` + SELECT d.id, d.parent_id, l.interpreted as name + FROM ensindexer_mainnet.v1_domains d + LEFT JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash + WHERE d.owner_id = $1 + `, ['\\x' + owner.replace('0x', '')]); + + res.json({ + owner, + domains: result.rows + }); +}); + +// Get registration info +app.get('/api/domains/:domainId/registration', async (req, res) => { + const domainId = req.params.domainId; + + const result = await pool.query(` + SELECT r.*, ra.timestamp + FROM ensindexer_mainnet.registrations r + JOIN ensindexer_mainnet.registrar_actions ra ON r.event_id = ra.event_ids[1] + WHERE r.domain_id = $1 + ORDER BY r.registration_index DESC + LIMIT 1 + `, [domainId]); + + res.json(result.rows[0]); +}); + +app.listen(3000, () => { + console.log('ENS API listening on port 3000'); +}); +``` + + +```python +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + +class Domain(BaseModel): + id: str + name: str | None + owner_id: str + +@app.get("/api/domains/{owner}") +async def get_domains(owner: str): + cursor = conn.cursor(cursor_factory=RealDictCursor) + cursor.execute(""" + SELECT d.id, l.interpreted as name, d.owner_id + FROM ensindexer_mainnet.v1_domains d + LEFT JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash + WHERE d.owner_id = %s + """, (owner,)) + + domains = cursor.fetchall() + return {"owner": owner, "domains": domains} + +@app.get("/api/domains/{domain_id}/registration") +async def get_registration(domain_id: str): + cursor = conn.cursor(cursor_factory=RealDictCursor) + cursor.execute(""" + SELECT r.* + FROM ensindexer_mainnet.registrations r + WHERE r.domain_id = %s + ORDER BY r.registration_index DESC + LIMIT 1 + """, (domain_id,)) + + return cursor.fetchone() +``` + + +```python +import click +import psycopg2 +from psycopg2.extras import RealDictCursor + +@click.group() +def cli(): + """ENSDb CLI - Query ENS data from the command line""" + pass + +@cli.command() +@click.argument('owner') +def domains(owner): + """Get domains owned by an address""" + conn = psycopg2.connect(database='ensdb') + cursor = conn.cursor(cursor_factory=RealDictCursor) + + cursor.execute(""" + SELECT d.id, l.interpreted as label + FROM ensindexer_mainnet.v1_domains d + LEFT JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash + WHERE d.owner_id = %s + """, (owner,)) + + for domain in cursor.fetchall(): + click.echo(f"{domain['id']}: {domain['label']}") + +@cli.command() +@click.argument('domain_id') +def registration(domain_id): + """Get registration info for a domain""" + conn = psycopg2.connect(database='ensdb') + cursor = conn.cursor(cursor_factory=RealDictCursor) + + cursor.execute(""" + SELECT * FROM ensindexer_mainnet.registrations + WHERE domain_id = %s + ORDER BY registration_index DESC + LIMIT 1 + """, (domain_id,)) + + reg = cursor.fetchone() + click.echo(f"Registration: {reg}") + +if __name__ == '__main__': + cli() +``` + + + +## Query Patterns + +### Multi-Schema Queries + +Query across multiple ENSIndexer Schemas: + +```sql +-- Union domains from multiple chains +SELECT 'mainnet' as chain, id, owner_id +FROM ensindexer_mainnet.v1_domains +WHERE owner_id = '\x1234...' + +UNION ALL + +SELECT 'base' as chain, id, owner_id +FROM ensindexer_base.v1_domains +WHERE owner_id = '\x1234...'; +``` + +### Name Resolution + +Resolve a name to its records: + +```sql +-- Find domain by label +SELECT d.id, d.owner_id +FROM ensindexer_mainnet.v1_domains d +JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash +WHERE l.interpreted = 'vitalik'; + +-- Get resolver records for a domain +SELECT rr.name, rar.coin_type, rar.value as address +FROM ensindexer_mainnet.resolver_records rr +LEFT JOIN ensindexer_mainnet.resolver_address_records rar + ON rr.chain_id = rar.chain_id + AND rr.address = rar.address + AND rr.node = rar.node +WHERE rr.node = '\x1234...'; +``` + +### Event History + +Get event history for a domain: + +```sql +SELECT e.*, de.domain_id +FROM ensindexer_mainnet.events e +JOIN ensindexer_mainnet.domain_events de ON e.id = de.event_id +WHERE de.domain_id = '\x1234...' +ORDER BY e.block_number DESC, e.log_index DESC +LIMIT 100; +``` + +## Best Practices + +### Query Performance + +1. **Check indexing status** before heavy queries +2. **Use LIMIT** for exploratory queries +3. **Filter early** with WHERE clauses on indexed columns +4. **Use EXPLAIN ANALYZE** to debug slow queries + +### Connection Management + +- Use connection pooling for production workloads +- Set appropriate pool sizes (typically 10-50 connections) +- Handle connection failures gracefully + +### Error Handling + +- Handle missing schemas gracefully (discover before querying) +- Handle missing tables (check schema version) +- Handle slow queries during backfill + +## Multi-Language Examples + +See the [Usage Guides](/ensdb/usage/) for complete examples in: +- TypeScript (with and without SDK) +- Python +- Go +- Rust + +## Testing Your Reader + +Verify your reader works correctly: + +1. **Schema discovery** — Finds all available ENSIndexer Schemas +2. **Status checking** — Handles backfill vs following states +3. **Data queries** — Returns correct domain, registration, and event data +4. **Error handling** — Gracefully handles missing data +5. **Performance** — Queries complete in acceptable time + +## Related Documentation + +- **[Database Schemas](/ensdb/concepts/database-schemas/)** — Complete table reference +- **[Querying Guide](/ensdb/usage/querying/)** — SQL patterns and examples +- **[ENSDb SDK](/ensdb/usage/ensdb-sdk/)** — TypeScript SDK reference +- **[Use Cases](/ensdb/use-cases/)** — Real-world reader examples diff --git a/docs/ensnode.io/src/content/docs/ensdb/integrations/writer.mdx b/docs/ensnode.io/src/content/docs/ensdb/integrations/writer.mdx new file mode 100644 index 0000000000..902ee3886a --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/integrations/writer.mdx @@ -0,0 +1,452 @@ +--- +title: Build a Custom Writer +description: Build an indexer that writes ENS data to ENSDb following the open standard. Learn the ENSNode Schema requirements, ENSIndexer Schema structure, and indexing lifecycle. +sidebar: + label: Build a Writer + order: 2 +--- + +import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; + +A **writer** indexes onchain ENS data and writes it to an ENSDb instance. This guide explains how to build a custom writer that follows the ENSDb open standard. + +## What a Writer Does + +```mermaid +flowchart LR + Onchain["Onchain ENS State
(Ethereum)"] + Writer["Your Writer
(Custom Indexer)"] + ENSDb["ENSDb Instance
(PostgreSQL)"] + + Onchain -->|1. Read Events| Writer + Writer -->|2. Transform| Writer + Writer -->|3. Write Data| ENSDb + Writer -->|4. Update Metadata| ENSDb +``` + +1. **Read** — Connect to an Ethereum node and subscribe to ENS-related events +2. **Transform** — Convert onchain data to ENSDb schema format +3. **Write** — Insert data into ENSIndexer Schema tables +4. **Metadata** — Update ENSNode Schema with indexing status and configuration + +## Architecture Overview + +Your writer will interact with two schemas: + +```mermaid +erDiagram + WRITER["Your Writer"] ||--o{ ENSNODE_METADATA : "updates" + WRITER ||--|| ENSINDEXER_SCHEMA : "creates & writes" + + ENSNODE_METADATA { + text ens_indexer_schema_name PK + text key PK + text value_version + jsonb value + } + + ENSINDEXER_SCHEMA["ensindexer_* Schema"] { + text schema_name + timestamp created_at + } +``` + +### ENSNode Schema Interactions + +Your writer must: +- **Create** the ENSNode Schema on first run (if it doesn't exist) +- **Register** itself in the `metadata` table with: + - `ensdb_version`: Your schema version + - `ensindexer_public_config`: Your public configuration + - `ensindexer_indexing_status`: Current indexing state + +### ENSIndexer Schema Interactions + +Your writer must: +- **Create** a schema with a unique name (e.g., `ensindexer_mycustom`) +- **Create** all tables defined in the 5 sub-schemas +- **Maintain** indexes appropriately (dropped during backfill, created during following) + +## Implementation Guide + +### Step 1: Set Up Your Project + +Create a new project with PostgreSQL connectivity: + + + +```bash +mkdir my-ensdb-writer +cd my-ensdb-writer +npm init -y +npm install pg @ensnode/ensdb-sdk viem +``` + + +```bash +mkdir my-ensdb-writer +cd my-ensdb-writer +python -m venv venv +source venv/bin/activate +pip install psycopg2-binary web3 +``` + + +```bash +mkdir my-ensdb-writer +cd my-ensdb-writer +go mod init my-ensdb-writer +go get github.com/jackc/pgx/v5 +go get github.com/ethereum/go-ethereum +``` + + + +### Step 2: Connect to PostgreSQL + + + +```typescript +import { Pool } from 'pg'; + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +// Or use ENSDb SDK +import { EnsDbWriter } from '@ensnode/ensdb-sdk'; + +const writer = new EnsDbWriter( + process.env.DATABASE_URL!, + 'ensindexer_mycustom' // Your schema name +); +``` + + +```python +import psycopg2 +from psycopg2.extras import RealDictCursor + +conn = psycopg2.connect( + host="localhost", + database="ensdb", + user="postgres", + password="password" +) +``` + + +```go +import ( + "context" + "github.com/jackc/pgx/v5/pgxpool" +) + +pool, err := pgxpool.New(context.Background(), "postgresql://user:pass@localhost/ensdb") +if err != nil { + log.Fatal(err) +} +defer pool.Close() +``` + + + +### Step 3: Initialize ENSNode Schema + +Create the ENSNode Schema and migrations table if they don't exist: + + + +```typescript +// Using SDK - handles migrations automatically +await writer.migrateEnsNodeSchema('./migrations/ensnode'); +``` + + +```sql +-- Create ENSNode schema +CREATE SCHEMA IF NOT EXISTS ensnode; + +-- Create metadata table +CREATE TABLE IF NOT EXISTS ensnode.metadata ( + ens_indexer_schema_name TEXT NOT NULL, + key TEXT NOT NULL, + value_version TEXT NOT NULL, + value JSONB NOT NULL, + PRIMARY KEY (ens_indexer_schema_name, key) +); +``` + + + +### Step 4: Create ENSIndexer Schema + +Create your dynamic schema with all required tables: + +```sql +-- Create your schema +CREATE SCHEMA IF NOT EXISTS ensindexer_mycustom; + +-- Create tables from all 5 sub-schemas (ensv2, protocol-acceleration, registrars, subgraph, tokenscope) +-- See Database Schemas reference for complete DDL: /ensdb/concepts/database-schemas/ +``` + + + +### Step 5: Register Your Writer + +Insert metadata about your indexer: + +```sql +-- Register schema version +INSERT INTO ensnode.metadata (ens_indexer_schema_name, key, value_version, value) +VALUES ( + 'ensindexer_mycustom', + 'ensdb_version', + '1.0.0', + '"1.0.0"'::jsonb +); + +-- Register public configuration +INSERT INTO ensnode.metadata (ens_indexer_schema_name, key, value_version, value) +VALUES ( + 'ensindexer_mycustom', + 'ensindexer_public_config', + '1.0.0', + '{ + "chains": ["mainnet"], + "plugins": ["ensv2"], + "version": "1.0.0" + }'::jsonb +); + +-- Register initial indexing status +INSERT INTO ensnode.metadata (ens_indexer_schema_name, key, value_version, value) +VALUES ( + 'ensindexer_mycustom', + 'ensindexer_indexing_status', + '1.0.0', + '{ + "status": "backfill", + "progress": 0, + "chains": {} + }'::jsonb +); +``` + +### Step 6: Implement Indexing Logic + +Connect to an Ethereum node and process events: + + + +```typescript +import { createPublicClient, http, parseAbi } from 'viem'; +import { mainnet } from 'viem/chains'; + +const client = createPublicClient({ + chain: mainnet, + transport: http(process.env.ETHEREUM_RPC_URL), +}); + +// Example: Index ENS Registry events +const ensRegistryAbi = parseAbi([ + 'event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner)', +]); + +// Get historical logs +const logs = await client.getLogs({ + address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', + event: ensRegistryAbi[0], + fromBlock: 0n, + toBlock: 'latest', +}); + +// Transform and insert +for (const log of logs) { + const node = log.args.node; + const label = log.args.label; + const owner = log.args.owner; + + // Transform to ENSDb format + const domainId = `${node}`; + const labelHash = `${label}`; + + // Insert into your schema + await pool.query(` + INSERT INTO ensindexer_mycustom.v1_domains (id, parent_id, owner_id, label_hash) + VALUES ($1, $2, $3, $4) + ON CONFLICT (id) DO UPDATE SET + owner_id = EXCLUDED.owner_id + `, [domainId, node, owner, labelHash]); +} +``` + + + +### Step 7: Update Indexing Status + +Periodically update the metadata with progress: + +```sql +UPDATE ensnode.metadata +SET value = '{ + "status": "backfill", + "progress": 45, + "chains": { + "1": { + "latestIndexedBlock": 18500000, + "targetBlock": 21000000 + } + } +}'::jsonb +WHERE ens_indexer_schema_name = 'ensindexer_mycustom' + AND key = 'ensindexer_indexing_status'; +``` + +When caught up to chain head: + +```sql +UPDATE ensnode.metadata +SET value = '{ + "status": "following", + "progress": 100, + "chains": { + "1": { + "latestIndexedBlock": 21000000 + } + } +}'::jsonb +WHERE ens_indexer_schema_name = 'ensindexer_mycustom' + AND key = 'ensindexer_indexing_status'; +``` + +### Step 8: Handle Index Management + +Drop indexes during backfill for performance: + +```sql +-- During backfill +DROP INDEX IF EXISTS ensindexer_mycustom.v1_domains_by_owner; +``` + +Create indexes when following for query performance: + +```sql +-- When following +CREATE INDEX v1_domains_by_owner +ON ensindexer_mycustom.v1_domains(owner_id); +``` + +## Schema Versioning + +Your writer must track schema versions: + +1. **Compute a checksum** of your schema definition +2. **Store it** in ENSNode metadata +3. **Validate** on startup that the database matches expected version + + + +```typescript +import { getDrizzleSchemaChecksum } from '@ensnode/ensdb-sdk'; +import * as schema from '@ensnode/ensdb-sdk/ensindexer-abstract'; + +const checksum = getDrizzleSchemaChecksum(schema); +// Store in metadata +await writer.upsertEnsDbVersion(checksum); +``` + + + +## Best Practices + +### Error Handling + +- Use transactions for multi-table writes +- Implement idempotent inserts (ON CONFLICT) +- Log errors but continue indexing when possible + +### Performance + +- Batch inserts when possible (100-1000 rows per batch) +- Drop indexes during backfill +- Use prepared statements for repeated queries + +### State Management + +- Persist last indexed block to survive restarts +- Handle chain reorganizations by rewinding and re-indexing +- Update indexing status frequently enough for monitoring + +## Complete Example + +Here's a minimal but complete writer example in TypeScript: + +```typescript +import { EnsDbWriter } from '@ensnode/ensdb-sdk'; +import { createPublicClient, http } from 'viem'; +import { mainnet } from 'viem/chains'; + +class CustomEnsDbWriter { + private writer: EnsDbWriter; + private client: ReturnType; + + constructor(ensDbUrl: string, schemaName: string, rpcUrl: string) { + this.writer = new EnsDbWriter(ensDbUrl, schemaName); + this.client = createPublicClient({ + chain: mainnet, + transport: http(rpcUrl), + }); + } + + async initialize(): Promise { + // Run ENSNode Schema migrations + await this.writer.migrateEnsNodeSchema('./migrations'); + + // Register configuration + await this.writer.upsertEnsIndexerPublicConfig({ + chains: ['mainnet'], + plugins: ['custom'], + }); + + // Set initial status + await this.writer.upsertIndexingStatusSnapshot({ + status: 'backfill', + progress: 0, + }); + } + + async run(): Promise { + // Start indexing... + // (Implementation depends on your specific requirements) + } +} + +// Usage +const writer = new CustomEnsDbWriter( + 'postgresql://localhost/ensdb', + 'ensindexer_mycustom', + 'https://mainnet.example.com' +); + +await writer.initialize(); +await writer.run(); +``` + +## Testing Your Writer + +Verify your writer follows the standard: + +1. **Schema creation** — ENSIndexer Schema exists with all tables +2. **Metadata registration** — ENSNode metadata has your entries +3. **Data insertion** — Data appears in correct tables +4. **Reader compatibility** — Existing readers can query your data + +## Related Documentation + +- **[Database Schemas](/ensdb/concepts/database-schemas/)** — Complete schema reference +- **[Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle/)** — How indexing phases work +- **[ENSIndexer Contributing](/ensindexer/contributing/)** — Reference implementation diff --git a/docs/ensnode.io/src/content/docs/ensdb/operations/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/operations/index.mdx new file mode 100644 index 0000000000..c3bb0cfce2 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/operations/index.mdx @@ -0,0 +1,324 @@ +--- +title: ENSNode Operations +description: Operational guidance for ENSNode operators, including multitenancy strategies, ENS Namespace isolation, cost optimization, and backup/restore procedures. +sidebar: + label: Operations + order: 7 +--- + +import { Aside, Card, CardGrid } from '@astrojs/starlight/components'; + +This guide covers operational considerations for running ENSNode in production, with a focus on cost optimization, resource isolation, and efficient deployment strategies. + +## Multitenancy and Resource Sharing + +An ENSDb instance is **multi-tenant** by design — it can store data from multiple [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) (tenants) that share common infrastructure while maintaining data isolation. + +### What Tenants Share + +| Resource | Sharing Model | Purpose | +|----------|---------------|---------| +| Ponder Schema (`ponder_sync`) | Shared across all tenants | RPC cache to reduce redundant blockchain calls | +| ENSNode Schema (`ensnode`) | Shared across all tenants | Metadata tracking for all connected indexers | +| PostgreSQL Server Resources | Shared (CPU, memory, I/O) | Database engine and connection handling | + +### What Tenants Own + +Each tenant (ENSIndexer instance) has: +- **Dedicated ENSIndexer Schema** — fully isolated data namespace (e.g., `ensindexer_mainnet`, `ensindexer_base`) +- **Independent indexing lifecycle** — can start, stop, or restart without affecting other tenants +- **Own configuration and status** — tracked separately in the ENSNode Metadata Table + + + +## ENS Namespace Isolation + +The most critical operational decision is how to handle [ENS Namespaces](/ensindexer/usage/ens-namespaces). An ENS Namespace (like "mainnet" or "sepolia") defines which chains an ENSIndexer instance indexes. + +### The Problem: Mixed Namespaces + +When a single ENSDb instance hosts ENSIndexer instances for **different namespaces** (e.g., some indexing "mainnet", others indexing "sepolia"): + +1. **The Ponder Schema caches RPC data for ALL indexed chains** +2. **Mainnet data dominates the cache** — due to significantly more blocks, events, and contract state +3. **Testnet data becomes a tiny fraction** — even if you're primarily interested in testnets + +```mermaid +flowchart TB + subgraph Mixed["Mixed ENSDb Instance
(mainnet + sepolia tenants)"] + direction TB + Ponder["Ponder Schema (ponder_sync)"] + MainnetCache["99%+ mainnet RPC cache"] + SepoliaCache["<1% sepolia RPC cache"] + MainnetIndexer["ENSIndexer Schema: ensindexer_mainnet"] + SepoliaIndexer["ENSIndexer Schema: ensindexer_sepolia"] + + Ponder --> MainnetCache + Ponder --> SepoliaCache + end + + style MainnetCache fill:#ffcccc + style SepoliaCache fill:#ccffcc +``` + +### The Solution: Namespace-Per-Instance + +For lean, cost-effective testnet operations, use **separate ENSDb instances** per ENS Namespace: + +| ENSDb Instance | ENS Namespace | Tenants | Ponder Schema Contents | +|---------------|---------------|---------|----------------------| +| `ensdb_mainnet` | mainnet | Mainnet indexers | Mainnet chains only | +| `ensdb_sepolia` | sepolia | Sepolia indexers | Testnet chains only | + + + +## Cost Optimization Strategies + +### 1. Dedicated Testnet Instances + +If you primarily need testnet data, deploy a dedicated `ensdb_sepolia` instance: + +```bash +# Separate PostgreSQL databases +psql postgresql://localhost:5432/ensdb_mainnet # Production +psql postgresql://localhost:5432/ensdb_sepolia # Testing/development +``` + +**Benefits:** +- **Smaller snapshots** — tens of GB instead of hundreds +- **Faster restore** — minutes instead of hours +- **Lower storage costs** — no mainnet bloat +- **Reusable RPC cache** — when you restore, the testnet cache is already primed + +### 2. Snapshot Strategy + +Take **namespace-specific ENSDb snapshots** for efficient backup and restore: + +| Snapshot Type | Contents | Use Case | +|--------------|----------|----------| +| `ensdb_mainnet_full` | Complete mainnet ENSDb | Production deployments | +| `ensdb_sepolia_full` | Complete sepolia ENSDb | Development, CI/CD, testing | + + + +### 3. Development Environment + +For local development or CI pipelines: + +1. Download the latest `ensdb_sepolia` snapshot +2. Import into a local PostgreSQL instance +3. Connect ENSApi to query testnet data + +```bash +# Download snapshot (URL is illustrative — service not yet available) +# curl -O https://snapshots.ensnode.io/ensdb_sepolia_latest.sql.gz + +# Restore to local database +# gunzip < ensdb_sepolia_latest.sql.gz | psql postgresql://localhost:5432/ensdb_sepolia + +# Start ENSApi pointing to the restored ENSDb +# DATABASE_URL=postgresql://localhost:5432/ensdb_sepolia pnpm start +``` + +## Deployment Patterns + +### Pattern 1: Single-Instance Multi-Tenant (Production) + +One ENSDb instance hosting multiple mainnet chain indexers: + +```mermaid +flowchart TB + subgraph Pattern1["ensdb_mainnet"] + direction TB + Mainnet["ensindexer_mainnet
(Ethereum mainnet ENS)"] + Base["ensindexer_base
(Base L2 ENS)"] + Linea["ensindexer_linea
(Linea L2 ENS)"] + DNS["ensindexer_3dns
(3DNS on mainnet)"] + end +``` + +**When to use:** Production environments indexing multiple chains within the same ENS Namespace. + +### Pattern 2: Namespace-Isolated Instances (Recommended) + +Separate ENSDb instances per ENS Namespace: + +```mermaid +flowchart TB + subgraph PGServer["PostgreSQL Server"] + direction TB + subgraph MainnetDB["ensdb_mainnet
(mainnet namespace only)"] + Mainnet["ensindexer_mainnet"] + Base["ensindexer_base"] + Linea["ensindexer_linea"] + end + + subgraph SepoliaDB["ensdb_sepolia
(sepolia namespace only)"] + Sepolia["ensindexer_sepolia"] + BaseSepolia["ensindexer_base_sepolia"] + end + end +``` + +**When to use:** When you need efficient testnet deployments or want precise control over snapshot boundaries. + +### Pattern 3: Environment-Based Isolation + +Separate ENSDb instances per deployment environment: + +```mermaid +flowchart TB + subgraph PGServer["PostgreSQL Server"] + direction TB + Prod["ensdb_production
(mainnet, full history)"] + Staging["ensdb_staging
(mainnet, recent history only)"] + Dev["ensdb_development
(sepolia, for testing)"] + end +``` + +**When to use:** When different environments have different data freshness requirements. + +## Backup and Restore Procedures + +### Taking a Snapshot + +A complete ENSDb snapshot includes all schemas: + +```bash +# Full database dump (all schemas) +pg_dump -Fc postgresql://host:5432/ensdb_mainnet > ensdb_mainnet_$(date +%Y%m%d).dump + +# Verify size +ls -lh ensdb_mainnet_*.dump +``` + +### Restoring from Snapshot + +```bash +# Create fresh database +createdb postgresql://host:5432/ensdb_restored + +# Restore from dump +pg_restore -d postgresql://host:5432/ensdb_restored ensdb_mainnet_20240115.dump + +# Verify tenants are present +psql postgresql://host:5432/ensdb_restored -c "SELECT DISTINCT ens_indexer_schema_name FROM ensnode.metadata;" +``` + +### Selective Restore (Advanced) + +If you only need specific tenants from a snapshot, you can restore specific schemas: + +```bash +# Restore only specific ENSIndexer Schema and required system schemas +pg_restore \ + --schema=ponder_sync \ + --schema=ensnode \ + --schema=ensindexer_base \ + -d postgresql://host:5432/ensdb_base_only \ + ensdb_mainnet_20240115.dump +``` + + + +## Monitoring and Alerts + +### Key Metrics + +| Metric | Source | Alert Threshold | +|--------|--------|-----------------| +| Ponder Schema size | `pg_total_relation_size('ponder_sync.*')` | > 80% of disk | +| ENSIndexer lag | `ensnode.metadata` ensindexer_indexing_status | > 100 blocks behind | +| Active tenants | `COUNT(DISTINCT ens_indexer_schema_name)` | Unexpected drop | +| Disk utilization | PostgreSQL system stats | > 85% | + +### Health Check Query + +```sql +-- Check all tenant statuses in an ENSDb instance +SELECT + ens_indexer_schema_name, + value->>'status' as status, + value->>'lastSyncedBlock' as last_block, + value->>'chainId' as chain_id +FROM ensnode.metadata +WHERE key = 'ensindexer_indexing_status'; +``` + +## Troubleshooting + +### Issue: ENSDb Instance Growing Too Large + +**Symptoms:** Disk usage increasing rapidly, slow queries, backup failures. + +**Diagnosis:** +```sql +-- Check Ponder Schema size by table +SELECT + schemaname, + tablename, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size +FROM pg_tables +WHERE schemaname = 'ponder_sync' +ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC; +``` + +**Solutions:** +1. **Separate namespaces** — Move testnet indexers to a dedicated `ensdb_sepolia` instance +2. **Verify tenant configurations** — Ensure all tenants are intentionally included +3. **Consider schema-specific restore** — If some tenants are no longer needed + +### Issue: High RPC Costs Despite Cache + +**Symptoms:** RPC usage higher than expected, cache hit rate low. + +**Possible causes:** +- Mixed namespaces diluting cache effectiveness +- Too many ENSDb instances with isolated Ponder Schemas (no sharing) +- Infrequent queries causing cache eviction + +### Issue: Slow Indexer Restart + +**Symptoms:** Indexer takes hours to resume after restart. + +**Diagnosis:** +```sql +-- Check if Ponder Schema has required cache entries +SELECT COUNT(*) FROM ponder_sync.blocks WHERE chain_id = 1; +``` + +**Solution:** Ensure you're restoring from a snapshot with a primed Ponder Schema, not starting from scratch. + +## Best Practices Summary + +1. **Isolate by ENS Namespace** — Separate `ensdb_mainnet` and `ensdb_sepolia` instances +2. **Share within Namespace** — Use multitenancy for multiple chains in the same namespace +3. **Snapshot strategically** — Take namespace-specific snapshots for efficient restore +4. **Monitor Ponder Schema size** — It's your primary indicator of resource usage +5. **Document tenant configurations** — Know which ENSIndexer instances are writing to each ENSDb instance + +## Related Documentation + + + +Deep dive into Ponder Schema, ENSNode Schema, and ENSIndexer Schemas + + +How multitenancy works and how schemas relate + + +How ENSIndexer uses namespaces to determine which chains to index + + diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/index.mdx new file mode 100644 index 0000000000..ba74a4fa36 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/index.mdx @@ -0,0 +1,101 @@ +--- +title: ENSDb SDK +description: TypeScript utilities for type-safe ENSDb access. +sidebar: + label: ENSDb SDK + order: 2 +--- + +import { LinkCard } from '@astrojs/starlight/components'; + +The [ENSDb SDK](/ensdb/concepts/glossary#ensdb-sdk) (`@ensnode/ensdb-sdk`) provides TypeScript utilities for working with ENSDb. While ENSDb is a standard PostgreSQL database usable from any language with a PostgreSQL driver, the SDK offers type-safe access and convenience methods for TypeScript. + +For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). + +:::note +You don't need the SDK to use ENSDb. See [Querying Guide](/ensdb/usage/querying) for language-agnostic SQL examples. +::: + +## Installation + +```bash +npm install @ensnode/ensdb-sdk +``` + +## Package Structure + +The SDK exports: + +| Export | Purpose | +|--------|---------| +| `EnsDbReader` | Read-only queries for ENSDb | +| `EnsDbWriter` | Write interface for metadata and migrations | +| [Schema Definitions](/ensdb/concepts/glossary#schema-definition) | Drizzle schemas for ENSNode and ENSIndexer | +| `getDrizzleSchemaChecksum` | Compute [Schema Checksum](/ensdb/concepts/glossary#schema-checksum) for schema definitions | + +## Schema Definitions + +The SDK exports Drizzle [Schema Definitions](/ensdb/concepts/glossary#schema-definition) for ENSDb schemas: + +### Abstract ENSIndexer Schema + +```typescript +import * as abstractEnsIndexerSchema from '@ensnode/ensdb-sdk/ensindexer-abstract'; + +// Contains table definitions for: +// - v1_domains, v2_domains +// - labels, accounts +// - registrations, renewals +// - events, domain_events, resolver_events +// - permissions tables +// ... and more +``` + +:::note +The abstract ENSIndexer Schema is called "abstract" because tables don't reference a specific [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name). Use `EnsDbReader` or `EnsDbWriter` to get a "concrete" schema bound to a specific name. +::: + +### ENSNode Schema + +```typescript +import * as ensNodeSchema from '@ensnode/ensdb-sdk/ensnode'; + +// Contains the metadata table definition +const { metadata } = ensNodeSchema; +``` + +## ENSDb Reader + + + +## ENSDb Writer + + + +## Schema Checksums + +Compute a checksum for a Drizzle schema definition: + +```typescript +import { getDrizzleSchemaChecksum } from '@ensnode/ensdb-sdk'; +import * as schema from '@ensnode/ensdb-sdk/ensindexer-abstract'; + +const checksum = getDrizzleSchemaChecksum(schema); +// Returns: 10-character checksum string +``` + +Use this to detect when [Schema Definitions](/ensdb/concepts/glossary#schema-definition) change. + +## Related Documentation + +- **[Querying Guide](/ensdb/usage/querying)** — SQL examples for any language +- **[Database Schemas](/ensdb/concepts/database-schemas)** — Schema structure and tables +- **[Glossary](/ensdb/concepts/glossary)** — [ENSDb SDK](/ensdb/concepts/glossary#ensdb-sdk), [Reader](/ensdb/concepts/glossary#ensdb-reader), [Writer](/ensdb/concepts/glossary#ensdb-writer) definitions diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/reader.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/reader.mdx new file mode 100644 index 0000000000..de75dbbfe1 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/reader.mdx @@ -0,0 +1,160 @@ +--- +title: ENSDb Reader +description: Type-safe querying for ENSNode Metadata and ENSIndexer data. +sidebar: + label: ENSDb Reader + order: 3 +--- + +The [ENSDb Reader](/ensdb/concepts/glossary#ensdb-reader) provides a convenience interface for querying ENSDb data. It wraps Drizzle ORM with ENSDb-specific knowledge. + +For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). + +## Purpose + +The [ENSDb Reader](/ensdb/concepts/glossary#ensdb-reader): + +- Queries [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) records +- Provides typed access to indexed ENS data via Drizzle ORM +- Exposes the underlying Drizzle client for complex queries + +## Constructor + +```typescript +import { EnsDbReader } from '@ensnode/ensdb-sdk'; + +const reader = new EnsDbReader( + 'postgresql://user:password@host:5432/ensdb', // ENSDb connection string + 'ensindexer_mainnet' // ENSIndexer Schema Name +); +``` + +Parameters: +- `ensDbUrl` — PostgreSQL connection string for the [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance) +- `ensIndexerSchemaName` — The [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name) to query + +## Properties + +### `ensDb` + +The underlying Drizzle client for building custom queries: + +```typescript +const db = reader.ensDb; + +// Build custom queries +const results = await db + .select() + .from(reader.ensIndexerSchema.v1_domains) + .where(eq(reader.ensIndexerSchema.v1_domains.owner_id, '0x...')); +``` + +### `ensIndexerSchema` + +The "concrete" [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) definition for use in queries: + +```typescript +// Access tables from the ENSIndexer Schema +const { v1_domains, v2_domains, labels } = reader.ensIndexerSchema; +``` + +### `ensIndexerSchemaName` + +The [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name) used by this reader: + +```typescript +console.log(reader.ensIndexerSchemaName); // 'ensindexer_mainnet' +``` + +### `ensNodeSchema` + +The [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) definition: + +```typescript +// Access the metadata table +const { metadata } = reader.ensNodeSchema; +``` + +## Methods + +### `getEnsDbVersion()` + +Get the ENSDb version for this [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance): + +```typescript +const version = await reader.getEnsDbVersion(); +// Returns: string | undefined +``` + +### `getEnsIndexerPublicConfig()` + +Get the public configuration for this [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance): + +```typescript +const config = await reader.getEnsIndexerPublicConfig(); +// Returns: EnsIndexerPublicConfig | undefined +``` + +### `getIndexingStatusSnapshot()` + +Get the [Indexing Status](/ensdb/concepts/glossary#indexing-status) snapshot: + +```typescript +const status = await reader.getIndexingStatusSnapshot(); +// Returns: CrossChainIndexingStatusSnapshot | undefined + +if (status) { + console.log(status.status); // 'backfill' | 'following' | etc. +} +``` + +## Querying ENSIndexer Data + +For querying indexed data (domains, labels, etc.), use the Drizzle client with the `ensIndexerSchema`: + +```typescript +import { eq } from 'drizzle-orm'; + +// Query v1 domains +const v1Domains = await reader.ensDb + .select() + .from(reader.ensIndexerSchema.v1_domains) + .where(eq(reader.ensIndexerSchema.v1_domains.owner_id, '0x1234...')) + .limit(10); + +// Query v2 domains +const v2Domains = await reader.ensDb + .select() + .from(reader.ensIndexerSchema.v2_domains) + .where(eq(reader.ensIndexerSchema.v2_domains.registry_id, 'some-registry-id')); + +// Query labels +const label = await reader.ensDb + .select() + .from(reader.ensIndexerSchema.labels) + .where(eq(reader.ensIndexerSchema.labels.label_hash, '0xabc123...')); +``` + +## Multiple ENSIndexer Schemas + +To query data from multiple [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema), create separate readers: + +```typescript +const mainnetReader = new EnsDbReader(ensDbUrl, 'ensindexer_mainnet'); +const l2Reader = new EnsDbReader(ensDbUrl, 'ensindexer_l2'); + +// Query each schema +const mainnetDomains = await mainnetReader.ensDb + .select() + .from(mainnetReader.ensIndexerSchema.v1_domains); + +const l2Domains = await l2Reader.ensDb + .select() + .from(l2Reader.ensIndexerSchema.v1_domains); +``` + +## Related Documentation + +- **[ENSDb Writer](/ensdb/usage/ensdb-sdk/writer/)** — Write interface for ENSDb +- **[Database Schemas](/ensdb/concepts/database-schemas)** — Table definitions +- **[Querying Guide](/ensdb/usage/querying)** — SQL examples for any language diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/writer.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/writer.mdx new file mode 100644 index 0000000000..068111266f --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/writer.mdx @@ -0,0 +1,146 @@ +--- +title: ENSDb Writer +description: Write interface for ENSDb initialization and metadata updates. +sidebar: + label: ENSDb Writer + order: 4 +--- + +The [ENSDb Writer](/ensdb/concepts/glossary#ensdb-writer) provides an interface for writing to ENSDb. It extends [ENSDb Reader](/ensdb/usage/ensdb-sdk/reader/) with write capabilities, and is primarily used by [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance). + +For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). + +## Purpose + +The [ENSDb Writer](/ensdb/concepts/glossary#ensdb-writer): + +- Runs database migrations for [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) +- Updates [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) records +- Inherits all read capabilities from [ENSDb Reader](/ensdb/usage/ensdb-sdk/reader/) + +:::caution +The ENSDb Writer is intended for use by [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance). Most ENSDb consumers only need the [ENSDb Reader](/ensdb/usage/ensdb-sdk/reader/). +::: + +## Constructor + +```typescript +import { EnsDbWriter } from '@ensnode/ensdb-sdk'; + +const writer = new EnsDbWriter( + 'postgresql://user:password@host:5432/ensdb', // ENSDb connection string + 'ensindexer_mainnet' // ENSIndexer Schema Name +); +``` + +Parameters: +- `ensDbUrl` — PostgreSQL connection string for the [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance) +- `ensIndexerSchemaName` — The [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name) for this instance + +:::note +EnsDbWriter extends EnsDbReader, so you can use all reader methods like `getEnsDbVersion()`, `getEnsIndexerPublicConfig()`, and `getIndexingStatusSnapshot()` on a writer instance. +::: + +## Methods + +### `migrateEnsNodeSchema(migrationsDirPath)` + +Execute pending database migrations for [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema): + +```typescript +await writer.migrateEnsNodeSchema('./migrations/ensnode'); +``` + +Parameters: +- `migrationsDirPath` — File path to the directory containing database migration files + +This creates or updates the [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) and its tables. + +### `upsertEnsDbVersion(ensDbVersion)` + +Upsert the ENSDb version for this [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance): + +```typescript +await writer.upsertEnsDbVersion('1.2.3'); +``` + +Creates or updates the record with key `ensdb_version` in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table). + +### `upsertEnsIndexerPublicConfig(config)` + +Upsert the public configuration for this [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance): + +```typescript +await writer.upsertEnsIndexerPublicConfig({ + chains: ['mainnet', 'base'], + // ... other config +}); +``` + +Creates or updates the record with key `ensindexer_public_config` in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table). + +### `upsertIndexingStatusSnapshot(status)` + +Upsert the [Indexing Status](/ensdb/concepts/glossary#indexing-status) snapshot: + +```typescript +await writer.upsertIndexingStatusSnapshot({ + status: 'following', + progress: 100, + // ... other status fields +}); +``` + +Creates or updates the record with key `ensindexer_indexing_status` in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table). + +## ENSNode Metadata Keys + +The [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) uses these keys: + +| Key | Method | Description | +|-----|--------|-------------| +| `ensdb_version` | `upsertEnsDbVersion()` | ENSDb version string | +| `ensindexer_public_config` | `upsertEnsIndexerPublicConfig()` | ENSIndexer public configuration | +| `ensindexer_indexing_status` | `upsertIndexingStatusSnapshot()` | [Indexing Status](/ensdb/concepts/glossary#indexing-status) snapshot | + +## Use by ENSIndexer + +[ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) use the ENSDb Writer during: + +1. **Startup** — Run migrations for [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) +2. **Registration** — Upsert public config to [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) +3. **Indexing** — Update [Indexing Status](/ensdb/concepts/glossary#indexing-status) periodically +4. **Completion** — Record final status + +## Migration Pattern + +```typescript +import { EnsDbWriter } from '@ensnode/ensdb-sdk'; + +const writer = new EnsDbWriter(ensDbUrl, mySchemaName); + +// Run ENSNode Schema migrations +await writer.migrateEnsNodeSchema('./migrations/ensnode'); + +// Register this ENSIndexer instance +await writer.upsertEnsIndexerPublicConfig(myPublicConfig); + +// During indexing - update status periodically +await writer.upsertIndexingStatusSnapshot({ + status: 'backfill', + progress: 50, +}); + +// When following +await writer.upsertIndexingStatusSnapshot({ + status: 'following', + progress: 100, +}); +``` + +## Related Documentation + +- **[ENSDb Reader](/ensdb/usage/ensdb-sdk/reader/)** — Read interface (inherited by Writer) +- **[ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema)** — Schema structure +- **[ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table)** — Metadata storage +- **[Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle)** — Status transitions diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx new file mode 100644 index 0000000000..c5faf57e9b --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx @@ -0,0 +1,137 @@ +--- +title: Using ENSDb +description: Practical guides for querying and integrating with ENSDb. +sidebar: + label: Usage + order: 4 +--- + +import { LinkCard } from '@astrojs/starlight/components'; + +This section provides practical guides for working with ENSDb. Since ENSDb is a standard PostgreSQL database, you can use any PostgreSQL client in any programming language. + +For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). + +## Querying ENSDb + + + +## TypeScript SDK + + + +## Language Examples + +ENSDb works with any PostgreSQL client. Here are connection examples for common languages: + +### Python + +```python +import psycopg2 + +conn = psycopg2.connect( + host="localhost", + database="ensdb", + user="postgres", + password="password" +) + +cursor = conn.cursor() + +# Discover [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema) +cursor.execute(""" + SELECT DISTINCT ens_indexer_schema_name + FROM ensnode.metadata +""") +schemas = cursor.fetchall() + +# Query indexed data +cursor.execute(""" + SELECT * FROM ensindexer_abc123.v1_domains + LIMIT 10 +""") +domains = cursor.fetchall() +``` + +### JavaScript (Node.js) + +```javascript +const { Pool } = require('pg'); + +const pool = new Pool({ + host: 'localhost', + database: 'ensdb', + user: 'postgres', + password: 'password', +}); + +// Discover ENSIndexer Schemas +const { rows: schemas } = await pool.query(` + SELECT DISTINCT ens_indexer_schema_name + FROM ensnode.metadata +`); + +// Query indexed data +const { rows: domains } = await pool.query(` + SELECT * FROM ensindexer_abc123.v1_domains + LIMIT 10 +`); +``` + +### Go + +```go +package main + +import ( + "database/sql" + "fmt" + _ "github.com/lib/pq" +) + +func main() { + connStr := "host=localhost dbname=ensdb user=postgres password=password sslmode=disable" + db, err := sql.Open("postgres", connStr) + if err != nil { + panic(err) + } + defer db.Close() + + // Discover ENSIndexer Schemas + rows, err := db.Query(` + SELECT DISTINCT ens_indexer_schema_name + FROM ensnode.metadata + `) + // ... +} +``` + +### Rust + +```rust +use postgres::{Client, NoTls}; + +fn main() { + let mut client = Client::connect( + "host=localhost dbname=ensdb user=postgres password=password", + NoTls, + ).unwrap(); + + // Discover ENSIndexer Schemas + for row in client.query( + "SELECT DISTINCT ens_indexer_schema_name FROM ensnode.metadata", + &[], + ).unwrap() { + let schema_name: String = row.get(0); + println!("{}", schema_name); + } +} +``` diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/querying.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/querying.mdx new file mode 100644 index 0000000000..71e660038e --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/usage/querying.mdx @@ -0,0 +1,290 @@ +--- +title: Querying ENSDb +description: SQL examples for discovering schemas and querying indexed ENS data. +sidebar: + label: Querying Guide + order: 1 +--- + +Query ENSDb using any PostgreSQL client with SQL. This guide provides practical examples for common query patterns. + +For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). + +## Connection + +Connect to an ENSDb instance using standard PostgreSQL connection parameters: + +```bash +psql postgresql://user:password@host:5432/ensdb +``` + +Connection parameters: + +| Parameter | Description | +|-----------|-------------| +| `host` | ENSDb server hostname | +| `port` | PostgreSQL port (default: 5432) | +| `database` | Database name (typically `ensdb`) | +| `user` | Database user | +| `password` | Database password | + +## Discovering ENSIndexer Schemas + +Before querying indexed data, discover which [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema) exist in the [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance): + +### List All ENSIndexer Schemas + +```sql +SELECT DISTINCT ens_indexer_schema_name +FROM ensnode.metadata +ORDER BY ens_indexer_schema_name; +``` + +Result: + +``` +ens_indexer_schema_name +──────────────────────── +ensindexer_mainnet +ensindexer_l2 +ensindexer_custom +``` + +### Get Schema Details + +```sql +SELECT + ens_indexer_schema_name, + key, + value_version, + value +FROM ensnode.metadata +WHERE ens_indexer_schema_name = 'ensindexer_mainnet' +ORDER BY key; +``` + +### Check Indexing Status + +```sql +SELECT + ens_indexer_schema_name, + value->>'status' as status, + value->>'progress' as progress +FROM ensnode.metadata +WHERE key = 'ensindexer_indexing_status'; +``` + +Result: + +``` +ens_indexer_schema_name status progress +──────────────────────── ─────── ───────── +ensindexer_mainnet following 100% +ensindexer_l2 backfill 45% +``` + +:::tip[Performance] +Queries are faster when ENSIndexer is in `following` status ([indexes](/ensdb/concepts/glossary#database-objects) are created). During `backfill`, queries may be slower because [indexes](/ensdb/concepts/glossary#database-objects) are dropped to optimize write throughput. +::: + +## Querying ENSIndexer Data + +Once you know the [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name), query its tables: + +### List Tables + +```sql +SELECT table_name +FROM information_schema.tables +WHERE table_schema = 'ensindexer_mainnet'; +``` + +Result: + +``` +table_name +────────────────────── +v1_domains +v2_domains +labels +accounts +registrations +renewals +events +domain_events +... +``` + +### Query Domains + +```sql +-- ENSv1 domains +SELECT + id, + parent_id, + owner_id, + label_hash +FROM ensindexer_mainnet.v1_domains +LIMIT 10; +``` + +```sql +-- ENSv2 domains +SELECT + id, + registry_id, + owner_id, + label_hash, + token_id +FROM ensindexer_mainnet.v2_domains +LIMIT 10; +``` + +### Query by Owner + +```sql +-- Domains owned by address +SELECT * +FROM ensindexer_mainnet.v1_domains +WHERE owner_id = '\x1234567890abcdef1234567890abcdef12345678'; +``` + +### Query Labels (Name Healing) + +```sql +-- Look up interpreted label for a labelhash +SELECT label_hash, interpreted +FROM ensindexer_mainnet.labels +WHERE label_hash = '\x_af2caa...03cc'; + +-- Search labels by interpreted text +SELECT label_hash, interpreted +FROM ensindexer_mainnet.labels +WHERE interpreted LIKE '%vitalik%'; +``` + +### Query Registrations + +```sql +SELECT + r.id, + r.domain_id, + r.type, + r.start, + r.expiry, + r.registrant_id +FROM ensindexer_mainnet.registrations r +WHERE r.expiry > EXTRACT(EPOCH FROM NOW()) +ORDER BY r.expiry ASC +LIMIT 20; +``` + +### Query Events + +```sql +-- Recent events for a domain +SELECT e.* +FROM ensindexer_mainnet.events e +JOIN ensindexer_mainnet.domain_events de ON e.id = de.event_id +WHERE de.domain_id = '0x...' +ORDER BY e.block_number DESC +LIMIT 10; +``` + +## Cross-Schema Queries + +Query across multiple [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema) in one query: + +### Union Data from Multiple Schemas + +```sql +SELECT 'mainnet' as source, id, owner_id +FROM ensindexer_mainnet.v1_domains +WHERE owner_id = '\x1234...' + +UNION ALL + +SELECT 'l2' as source, id, owner_id +FROM ensindexer_l2.v1_domains +WHERE owner_id = '\x1234...'; +``` + +### Compare Schema Content + +```sql +SELECT + 'mainnet' as schema, + COUNT(*) as domain_count +FROM ensindexer_mainnet.v1_domains + +UNION ALL + +SELECT + 'l2' as schema, + COUNT(*) as domain_count +FROM ensindexer_l2.v1_domains; +``` + +## Querying Ponder Schema + +The [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) (`ponder_sync`) contains RPC cache data. It's primarily used internally by [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance), but can be queried for debugging: + +```sql +SELECT table_name +FROM information_schema.tables +WHERE table_schema = 'ponder_sync'; +``` + +:::note +The [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) structure is defined by Ponder and may change between Ponder versions. It's not intended for direct querying by ENSDb consumers. +::: + +## Performance Tips + +### Use Indexes + +Queries are fastest when an [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) is in `following` [Indexing Status](/ensdb/concepts/glossary#indexing-status). Check before running heavy queries: + +```sql +SELECT value->>'status' as status +FROM ensnode.metadata +WHERE ens_indexer_schema_name = 'ensindexer_mainnet' + AND key = 'ensindexer_indexing_status'; +``` + +### Use LIMIT + +Always limit results during exploration: + +```sql +SELECT * FROM ensindexer_mainnet.v1_domains LIMIT 10; +``` + +### Use WHERE Clauses + +Filter early to reduce data scanned: + +```sql +-- Good: filters on indexed column +SELECT * FROM ensindexer_mainnet.v1_domains +WHERE owner_id = '\x1234...'; + +-- Slow: scans full table +SELECT * FROM ensindexer_mainnet.v1_domains +WHERE label_hash NOT IN (SELECT label_hash FROM ensindexer_mainnet.labels); +``` + +### Use EXPLAIN ANALYZE + +Check query plans for slow queries: + +```sql +EXPLAIN ANALYZE +SELECT * FROM ensindexer_mainnet.v1_domains WHERE owner_id = '\x1234...'; +``` + +## Related Documentation + +- **[ENSDb SDK](/ensdb/concepts/glossary#ensdb-sdk)** — TypeScript utilities for type-safe queries +- **[Database Schemas](/ensdb/concepts/database-schemas)** — Table definitions +- **[Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle)** — Why query performance varies diff --git a/docs/ensnode.io/src/content/docs/ensdb/use-cases/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/use-cases/index.mdx new file mode 100644 index 0000000000..46f89a93a2 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/use-cases/index.mdx @@ -0,0 +1,502 @@ +--- +title: ENSDb Use Cases +description: Real-world examples of what you can build with ENSDb. Analytics dashboards, CLIs, custom APIs, data pipelines, and more. +sidebar: + label: Use Cases + order: 5 +--- + +import { Card, CardGrid, Aside, Tabs, TabItem } from '@astrojs/starlight/components'; + +ENSDb unlocks a new universe of ENS applications. Here are real-world use cases you can build today. + +## Use Case Categories + + + +Real-time analytics, market intelligence, and visualization dashboards with sub-second query latency. + + +Specialized GraphQL, REST, or gRPC APIs tailored to specific use cases or applications. + + +CLIs, SDKs, and developer utilities for ENS operations and data exploration. + + +ETL pipelines, data warehouses, search indexes, and real-time streaming systems. + + +ML models for name valuation, trend prediction, and pattern recognition. + + +Real-time monitoring of ENS state changes, expiration alerts, and event notifications. + + + +--- + +## Analytics & Dashboards + +Build analytics dashboards that exceed what's possible on platforms like Dune. + +### Why ENSDb Over Dune? + +| Capability | Dune | ENSDb | +|------------|------|-------| +| Query latency | Seconds to minutes | Sub-second | +| Data freshness | Hours delayed | Real-time | +| Complex joins | Limited | Full SQL power | +| Custom aggregations | Constrained | Unlimited | +| Private data | No | Yes | +| Cost at scale | Expensive | Predictable | + +### Example: Domain Ownership Analytics + +Track ownership concentration, identify whales, analyze trends: + +```sql +-- Top domain owners by count +SELECT owner_id, COUNT(*) as domain_count +FROM ensindexer_mainnet.v1_domains +GROUP BY owner_id +ORDER BY domain_count DESC +LIMIT 100; + +-- Ownership over time +SELECT + DATE_TRUNC('month', to_timestamp(e.timestamp)) as month, + COUNT(DISTINCT d.owner_id) as unique_owners, + COUNT(*) as new_domains +FROM ensindexer_mainnet.events e +JOIN ensindexer_mainnet.domain_events de ON e.id = de.event_id +JOIN ensindexer_mainnet.v1_domains d ON de.domain_id = d.id +WHERE e.selector = '\x0x...' -- NewOwner event +GROUP BY month +ORDER BY month; +``` + +### Example: Registration Market Trends + +Analyze registration patterns, pricing trends, and market activity: + +```sql +-- Daily registration volume with pricing +SELECT + DATE_TRUNC('day', to_timestamp(ra.timestamp)) as day, + COUNT(*) as registrations, + AVG(ra.total) as avg_cost_wei, + SUM(ra.total) as total_revenue_wei +FROM ensindexer_mainnet.registrar_actions ra +WHERE ra.type = 'registration' + AND ra.timestamp > EXTRACT(EPOCH FROM NOW() - INTERVAL '30 days') +GROUP BY day +ORDER BY day DESC; + +-- Premium vs standard registrations +SELECT + CASE + WHEN ra.premium > 0 THEN 'premium' + ELSE 'standard' + END as type, + COUNT(*) as count, + AVG(ra.total) as avg_cost +FROM ensindexer_mainnet.registrar_actions ra +WHERE ra.type = 'registration' +GROUP BY type; +``` + +### Tools to Build With + +- **Grafana** — Dashboards and alerting +- **Metabase** — Business intelligence +- **Apache Superset** — Modern data exploration +- **Custom React/Vue dashboards** — Tailored interfaces + +--- + +## Custom APIs + +Build specialized APIs for specific use cases. + +### Example: Domain Search API + +A fast search API for finding available or registered names: + +```typescript +// Express.js example +app.get('/api/search', async (req, res) => { + const { query, tld = 'eth' } = req.query; + + // Search labels + const labels = await pool.query(` + SELECT + l.interpreted, + EXISTS( + SELECT 1 FROM ensindexer_mainnet.v1_domains d + WHERE d.label_hash = l.label_hash + ) as registered + FROM ensindexer_mainnet.labels l + WHERE l.interpreted ILIKE $1 + LIMIT 100 + `, [`%${query}%`]); + + // Check exact match availability + const exact = await pool.query(` + SELECT d.id, d.owner_id, r.expiry + FROM ensindexer_mainnet.v1_domains d + LEFT JOIN ensindexer_mainnet.registrations r ON d.id = r.domain_id + JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash + WHERE l.interpreted = $1 + `, [query]); + + res.json({ + suggestions: labels.rows, + exactMatch: exact.rows[0] || null, + available: exact.rows.length === 0 + }); +}); +``` + +### Example: ENS Profile API + +Aggregate all ENS data for an address: + +```sql +-- Get complete profile for an address +WITH domains AS ( + SELECT d.id, l.interpreted as name + FROM ensindexer_mainnet.v1_domains d + JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash + WHERE d.owner_id = '\x1234...' +), +records AS ( + SELECT + rr.node, + rr.name as primary_name, + jsonb_object_agg(rar.coin_type::text, rar.value) as addresses, + jsonb_object_agg(rtr.key, rtr.value) as texts + FROM ensindexer_mainnet.resolver_records rr + LEFT JOIN ensindexer_mainnet.resolver_address_records rar + ON (rr.chain_id, rr.address, rr.node) = (rar.chain_id, rar.address, rar.node) + LEFT JOIN ensindexer_mainnet.resolver_text_records rtr + ON (rr.chain_id, rr.address, rr.node) = (rtr.chain_id, rtr.address, rtr.node) + WHERE rr.node IN (SELECT id FROM domains) + GROUP BY rr.node, rr.name +) +SELECT + d.id, + d.name, + r.primary_name, + r.addresses, + r.texts +FROM domains d +LEFT JOIN records r ON d.id = r.node; +``` + +--- + +## Developer Tools + +Build CLIs and developer utilities. + +### Example: ENS CLI + +A command-line tool for ENS operations: + +```bash +# Check domain info +$ ensdb domain vitalik.eth +Owner: 0x1234... +Resolver: 0x5678... +Records: + - ETH: 0x1234... + - BTC: bc1q... + - com.twitter: @vitalikbuterin +Expiry: 2025-12-31 + +# List domains by owner +$ ensdb list 0x1234... --format json +[{"name": "vitalik.eth", ...}, ...] + +# Check expiration dates +$ ensdb expiring --within 30d --format table +Name Expiry Days Left +vitalik.eth 2025-12-31 365 +... + +# Export data +$ ensdb export --owner 0x1234... --format csv > my_domains.csv +``` + +### Implementation + + + +```python +import click +import psycopg2 +from psycopg2.extras import RealDictCursor + +@click.group() +def cli(): + """ENS CLI - Query ENS data from ENSDb""" + pass + +@cli.command() +@click.argument('name') +def domain(name): + """Get information about a domain""" + conn = psycopg2.connect(database='ensdb') + cursor = conn.cursor(cursor_factory=RealDictCursor) + + # Normalize name to get node + cursor.execute(""" + SELECT d.id, d.owner_id, r.expiry + FROM ensindexer_mainnet.v1_domains d + LEFT JOIN ensindexer_mainnet.registrations r ON d.id = r.domain_id + JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash + WHERE l.interpreted = %s + """, (name.replace('.eth', ''),)) + + domain = cursor.fetchone() + if not domain: + click.echo(f"Domain {name} not found") + return + + click.echo(f"Domain: {name}") + click.echo(f"Owner: {domain['owner_id']}") + click.echo(f"Expiry: {domain['expiry']}") + +@cli.command() +@click.argument('owner') +@click.option('--format', default='table', type=click.Choice(['table', 'json'])) +def list(owner, format): + """List domains owned by an address""" + # Implementation... + +if __name__ == '__main__': + cli() +``` + + + +--- + +## Data Pipelines + +Integrate ENS data into your existing infrastructure. + +### Example: Webhook Pipeline + +Trigger webhooks on ENS state changes: + +```python +import asyncio +import asyncpg +import aiohttp +from datetime import datetime + +async def webhook_pipeline(): + conn = await asyncpg.connect('postgresql://localhost/ensdb') + + # Track last processed block + last_block = load_checkpoint() + + async with conn.add_listener('ens_events') as queue: + async for event in queue: + # New event detected + event_data = json.loads(event.payload) + + # Fetch full event details + row = await conn.fetchrow(""" + SELECT e.*, de.domain_id + FROM ensindexer_mainnet.events e + JOIN ensindexer_mainnet.domain_events de ON e.id = de.event_id + WHERE e.id = $1 + """, event_data['event_id']) + + # Send webhook + async with aiohttp.ClientSession() as session: + await session.post( + 'https://your-app.com/webhooks/ens', + json={ + 'event': row['selector'], + 'domain': row['domain_id'], + 'block': row['block_number'], + 'tx': row['transaction_hash'], + } + ) + +asyncio.run(webhook_pipeline()) +``` + +### Example: Data Warehouse Sync + +Sync ENS data to your data warehouse: + +```python +# Daily sync job +from google.cloud import bigquery + +def sync_to_warehouse(): + # Query ENSDb + ens_data = query_ensdb(""" + SELECT + d.id, + l.interpreted as label, + d.owner_id, + r.expiry, + e.timestamp as created_at + FROM ensindexer_mainnet.v1_domains d + JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash + LEFT JOIN ensindexer_mainnet.registrations r ON d.id = r.domain_id + LEFT JOIN ensindexer_mainnet.events e ON d.id = ( + SELECT domain_id FROM ensindexer_mainnet.domain_events + WHERE event_id = r.event_id + ) + WHERE d.created_at > %s + """, (yesterday,)) + + # Load to BigQuery/Snowflake/Redshift + client = bigquery.Client() + table = client.dataset('ens').table('domains') + + errors = client.insert_rows(table, ens_data) + if errors: + print(f"Errors: {errors}") +``` + +--- + +## AI & Machine Learning + +Train models on complete ENS datasets. + +### Example: Name Valuation Model + +Predict the value of an ENS name: + +```python +import pandas as pd +from sklearn.ensemble import RandomForestRegressor + +# Extract features from ENSDb +df = pd.read_sql(""" + SELECT + l.interpreted, + LENGTH(l.interpreted) as length, + CASE WHEN l.interpreted ~ '^[0-9]+$' THEN 1 ELSE 0 END as is_numeric, + CASE WHEN l.interpreted ~ '^[a-z]+$' THEN 1 ELSE 0 END as is_alpha, + -- Add more features... + ra.total as price_wei + FROM ensindexer_mainnet.labels l + JOIN ensindexer_mainnet.v1_domains d ON l.label_hash = d.label_hash + JOIN ensindexer_mainnet.registrar_actions ra ON d.id = ra.node + WHERE ra.type = 'registration' + AND ra.total IS NOT NULL +"", conn) + +# Train model +X = df[['length', 'is_numeric', 'is_alpha']] +y = df['price_wei'] + +model = RandomForestRegressor() +model.fit(X, y) + +# Predict +prediction = model.predict([[5, 0, 1]]) # 5-letter alpha name +print(f"Predicted value: {prediction[0]} wei") +``` + +--- + +## Monitoring & Alerts + +Build real-time monitoring systems. + +### Example: Expiration Monitor + +Alert users before their names expire: + +```python +from datetime import datetime, timedelta +import smtplib + +def check_expiring_names(): + conn = psycopg2.connect(database='ensdb') + cursor = conn.cursor(cursor_factory=RealDictCursor) + + # Find names expiring in next 30 days + cursor.execute(""" + SELECT + l.interpreted as name, + d.owner_id, + rl.expires_at, + EXTRACT(DAY FROM to_timestamp(rl.expires_at) - NOW()) as days_left + FROM ensindexer_mainnet.registration_lifecycles rl + JOIN ensindexer_mainnet.labels l ON rl.node = ( + SELECT id FROM ensindexer_mainnet.v1_domains + WHERE label_hash = l.label_hash LIMIT 1 + ) + JOIN ensindexer_mainnet.v1_domains d ON rl.node = d.id + WHERE rl.expires_at < EXTRACT(EPOCH FROM NOW() + INTERVAL '30 days') + AND rl.expires_at > EXTRACT(EPOCH FROM NOW()) + ORDER BY rl.expires_at + """) + + expiring = cursor.fetchall() + + # Group by owner and send alerts + by_owner = {} + for name in expiring: + owner = name['owner_id'] + if owner not in by_owner: + by_owner[owner] = [] + by_owner[owner].append(name) + + for owner, names in by_owner.items(): + send_expiration_alert(owner, names) + +def send_expiration_alert(owner, names): + # Send email/push notification... + pass +``` + +--- + +## Success Stories + +Here are examples of what teams could build with ENSDb: + +:::note +The following are illustrative examples of potential use cases. These specific implementations may not exist yet. +::: + +### Analytics Platform +> "We built a real-time ENS analytics platform with sub-second query latency. What used to take 30 seconds on Dune now happens instantly." + +### Domain Marketplace +> "Our marketplace uses a custom reader to show name availability, pricing history, and ownership records in real-time." + +### Portfolio Tracker +> "Users connect their wallet and see all their ENS names, expiration dates, and renewal costs in one dashboard." + +### Research Tool +> "Academic researchers use ENSDb to study ENS adoption patterns, name distribution, and market dynamics." + +--- + +## Getting Started + +Pick a use case and start building: + +1. **For Analytics** — Start with SQL queries in [Querying Guide](/ensdb/usage/querying/) +2. **For APIs** — Build a [Custom Reader](/ensdb/integrations/reader/) +3. **For CLIs** — Use Python + Click or Go + Cobra +4. **For Pipelines** — Set up change data capture with PostgreSQL logical replication + +## Related Documentation + +- **[Building Integrations](/ensdb/integrations/)** — Build custom readers and writers +- **[Querying Guide](/ensdb/usage/querying/)** — SQL patterns and examples +- **[Database Schemas](/ensdb/concepts/database-schemas/)** — Complete schema reference diff --git a/docs/ensnode.io/src/styles/starlight.css b/docs/ensnode.io/src/styles/starlight.css index 05b677b047..0e26f97508 100644 --- a/docs/ensnode.io/src/styles/starlight.css +++ b/docs/ensnode.io/src/styles/starlight.css @@ -87,10 +87,12 @@ max-width: var(--max-width-page); margin-inline: auto; } + nav.sidebar .sidebar-pane { position: sticky; height: calc(100vh - var(--sl-nav-height)); } + div.main-frame { padding-inline-start: initial; flex: 1; @@ -121,7 +123,7 @@ a[rel="prev"]:hover { color: var(--sl-color-accent); } -.content-panel + .content-panel { +.content-panel+.content-panel { border-top: 0; padding-top: 0; } @@ -136,14 +138,15 @@ a[rel="prev"]:hover { color: var(--sl-color-accent-high); } -.sl-markdown-content a:hover:not(:where(.not-content *)) > span { +.sl-markdown-content a:hover:not(:where(.not-content *))>span { color: var(--sl-color-accent); } -.sl-markdown-content :is(h1, h2, h3, h4, h5, h6) > a { +.sl-markdown-content :is(h1, h2, h3, h4, h5, h6)>a { color: var(--sl-color-white); display: inline; text-decoration: none; + &:hover { text-decoration: underline; } @@ -156,11 +159,17 @@ a[rel="prev"]:hover { .sl-markdown-content ul { list-style: disc; color: var(--sl-color-text); + list-style-position: inside; +} + +.sl-markdown-content ul ul { + margin-left: 1rem; } .sl-markdown-content ol { list-style: decimal; color: var(--sl-color-text); + list-style-position: inside; } .sl-markdown-content ol li::marker, @@ -179,7 +188,7 @@ a[rel="prev"]:hover { justify-content: space-between; margin-top: 3rem; - & > a { + &>a { flex-basis: unset; flex-grow: unset; width: fit-content; @@ -197,7 +206,7 @@ a[rel="prev"]:hover { margin-left: auto; } - & > span { + &>span { font-size: 0.75rem; .link-title { @@ -205,7 +214,7 @@ a[rel="prev"]:hover { } } - & > svg { + &>svg { align-self: self-end; color: var(--pagination-arrow-color); font-size: 1.25rem; @@ -213,7 +222,7 @@ a[rel="prev"]:hover { } } - & > a:hover > svg { + &>a:hover>svg { color: var(--sl-color-white); } } @@ -225,20 +234,22 @@ a[rel="prev"]:hover { } /* Styling for the list items to prepare for lines */ -#starlight__sidebar > div > ul > li { - position: relative; /* For absolute positioning of ::before */ +#starlight__sidebar>div>ul>li { + position: relative; + /* For absolute positioning of ::before */ } /* * Add padding-left only to items that will get a line, to make space for it. * Note, including :not(:last-child) to de-indent ENSAdmin. */ -#starlight__sidebar > div > ul > li:not(:first-child):not(:last-child) { - padding-left: 20px; /* Space for the line and content */ +#starlight__sidebar>div>ul>li:not(:first-child):not(:last-child) { + padding-left: 20px; + /* Space for the line and content */ } /* Add margin-bottom for the gap to all LIs except the very last one in the UL */ -#starlight__sidebar > div > ul > li:not(:last-child) { +#starlight__sidebar>div>ul>li:not(:last-child) { margin-bottom: var(--sidebar-item-gap); } @@ -246,17 +257,21 @@ a[rel="prev"]:hover { * Vertical line segment for relevant list items * Note, including :not(:last-child) to de-indent ENSAdmin. */ -#starlight__sidebar > div > ul > li:not(:first-child):not(:last-child)::before { +#starlight__sidebar>div>ul>li:not(:first-child):not(:last-child)::before { content: ""; position: absolute; - left: 8px; /* X-position of the vertical line */ + left: 8px; + /* X-position of the vertical line */ top: 0; - width: 1px; /* Thickness of the vertical line */ - background-color: var(--sl-color-hairline-light); /* Line color */ - height: 100%; /* Default height, covers the item itself */ + width: 1px; + /* Thickness of the vertical line */ + background-color: var(--sl-color-hairline-light); + /* Line color */ + height: 100%; + /* Default height, covers the item itself */ } -#starlight__sidebar > div > ul > li:not(:first-child):not(:last-child)::before { +#starlight__sidebar>div>ul>li:not(:first-child):not(:last-child)::before { height: calc(100% + var(--sidebar-item-gap)); } @@ -265,7 +280,8 @@ a[rel="prev"]:hover { /* Styles to replace the default Starlight sidebar icon with a custom image */ .starlight-sidebar-topics-icon svg { - display: none; /* Hide the default SVG icon */ + display: none; + /* Hide the default SVG icon */ } /* Default sidebar icon (parent) */ From f2764cb38c76bddfc9d6b61d4d5d87378dfa4775 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Apr 2026 07:44:06 +0200 Subject: [PATCH 2/3] Clean up temp pages --- .../docs/ensdb/concepts/architecture.mdx | 365 ----- .../docs/ensdb/concepts/database-schemas.mdx | 1216 ----------------- .../ensdb/concepts/indexing-lifecycle.mdx | 170 --- .../content/docs/ensdb/integrations/index.mdx | 289 ---- .../docs/ensdb/integrations/reader.mdx | 596 -------- .../docs/ensdb/integrations/writer.mdx | 452 ------ .../content/docs/ensdb/operations/index.mdx | 324 ----- .../docs/ensdb/usage/ensdb-sdk/index.mdx | 101 -- .../docs/ensdb/usage/ensdb-sdk/reader.mdx | 160 --- .../docs/ensdb/usage/ensdb-sdk/writer.mdx | 146 -- .../src/content/docs/ensdb/usage/index.mdx | 137 -- .../src/content/docs/ensdb/usage/querying.mdx | 290 ---- .../content/docs/ensdb/use-cases/index.mdx | 502 ------- 13 files changed, 4748 deletions(-) delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/concepts/indexing-lifecycle.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/integrations/index.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/integrations/reader.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/integrations/writer.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/operations/index.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/index.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/reader.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/writer.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/querying.mdx delete mode 100644 docs/ensnode.io/src/content/docs/ensdb/use-cases/index.mdx diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx deleted file mode 100644 index 51883e47e2..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx +++ /dev/null @@ -1,365 +0,0 @@ ---- -title: ENSDb Architecture -description: How ENSDb works as a bi-directional open standard, including the writer/reader pattern, PostgreSQL server/instance architecture, and data flow. -sidebar: - label: Architecture - order: 3 ---- - -import { Aside } from '@astrojs/starlight/components'; - -ENSDb is a **bi-directional open standard** for ENS integration. This page explains the architecture: how data flows from onchain to your applications, and how ENSDb instances are served from PostgreSQL servers. - -For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). - -## The ENSDb Open Standard - -ENSDb defines a standard way to store and query ENS data in a PostgreSQL database. The standard is **implementation-agnostic** — anyone can build writers, readers, or both. - -### Key Concepts - -| Term | Definition | -|------|------------| -| **ENSDb** | An open standard defining schemas, rules, and constraints for ENS data in a PostgreSQL database | -| **ENSDb Instance** | A PostgreSQL database that follows the ENSDb standard | -| **PostgreSQL Server** | A running PostgreSQL process that can serve multiple ENSDb instances (databases) | - -### Bi-Directional Integration Pattern - -```mermaid -flowchart TB - subgraph WriteSide["Write Side (Any Implementation)"] - Onchain["Onchain ENS State
Ethereum / L2s"] - Writers["Writers"] - EI[ENSIndexer] - CW[Custom Writer] - FW[Future Writers] - end - - subgraph ENSDb["ENSDb Standard
(PostgreSQL Database)"] - PS[(Ponder Schema)] - NS[(ENSNode Schema)] - IS[(ENSIndexer Schema)] - end - - subgraph ReadSide["Read Side (Any Implementation)"] - Readers["Readers"] - EA[ENSApi] - CR[Custom API] - DA[Dashboard] - CLI[CLI Tool] - end - - Onchain -->|Index| Writers - EI -->|Write| ENSDb - CW -->|Write| ENSDb - FW -->|Write| ENSDb - - ENSDb -->|Query| Readers - ENSDb -->|Query| EA - ENSDb -->|Query| CR - ENSDb -->|Query| DA - ENSDb -->|Query| CLI -``` - -**Writers** index onchain ENS data and write to ENSDb. **Readers** query ENSDb and serve data to applications. Both can be built in any programming language with PostgreSQL support. - -## PostgreSQL Server vs ENSDb Instance - -Understanding the relationship between servers and instances is key to deploying ENSDb: - -### [PostgreSQL Server](/ensdb/concepts/glossary#postgresql-server) - -A **[PostgreSQL server](/ensdb/concepts/glossary#postgresql-server)** is a running PostgreSQL process that can host multiple databases: - -```mermaid -flowchart TB - subgraph PGServer["PostgreSQL Server (localhost:5432)"] - direction TB - Mainnet["ensdb_mainnet
← ENSDb instance: Production environment"] - Testnet["ensdb_testnet
← ENSDb instance: Pre-production environment"] - Devnet["ensdb_devnet
← ENSDb instance: Staging / local development"] - Other["other_db
← Non-ENSDb database"] - end -``` - -### [ENSDb Instance](/ensdb/concepts/glossary#ensdb-instance) - -An **[ENSDb instance](/ensdb/concepts/glossary#ensdb-instance)** is a single PostgreSQL database that follows the [ENSDb](/ensdb/concepts/glossary#ensdb) standard: - -```mermaid -flowchart TB - subgraph PGServer["PostgreSQL Server"] - direction TB - - subgraph Instance1["ENSDb Instance: ensdb_mainnet"] - Ponder1["ponder_sync
(Ponder Schema)"] - ENSNode1["ensnode
(ENSNode Schema)"] - Indexer1["ensindexer_mainnet
(ENSIndexer Schema)"] - end - - subgraph Instance2["ENSDb Instance: ensdb_testnet"] - Ponder2["ponder_sync
(Ponder Schema)"] - ENSNode2["ensnode
(ENSNode Schema)"] - Indexer2["ensindexer_testnet
(ENSIndexer Schema)"] - end - end -``` - - - -## Instance Structure - -An [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance) — a single PostgreSQL database — contains exactly: - -- **1 [Ponder Schema](/ensdb/concepts/glossary#ponder-schema)** — named `ponder_sync` (fixed) -- **1 [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema)** — named `ensnode` (fixed) -- **1+ [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema)** — dynamic names, one per writer - -```mermaid -flowchart TB - subgraph PGServer["PostgreSQL Server"] - subgraph ENSDb["ENSDb Instance (Database)"] - Ponder["ponder_sync
(Ponder Schema)"] - ENSNode["ensnode
(ENSNode Schema)
metadata table"] - - subgraph Indexers["ENSIndexer Schemas"] - Foo["ensindexer_foo
v1_domains, v2_domains
labels, ..."] - Bar["ensindexer_bar
v1_domains, v2_domains
labels, ..."] - More["ensindexer_...
..."] - end - end - end - - ENSNode -->|discovers| Foo - ENSNode -->|discovers| Bar - ENSNode -->|discovers| More -``` - -## Schema Relationships - -### ENSNode Metadata → ENSIndexer Schemas - -The [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) links to ENSIndexer Schemas via the `ens_indexer_schema_name` column: - -```mermaid -erDiagram - ENSNODE_METADATA ||--o{ ENSINDEXER_SCHEMA : "tracks" - - ENSNODE_METADATA { - text ens_indexer_schema_name PK - text key PK - text value_version - jsonb value - } - - ENSINDEXER_SCHEMA["ensindexer_* Schema"] { - text schema_name - timestamp created_at - } -``` - -```sql --- [Schema Discovery](/ensdb/concepts/glossary#schema-discovery): find all ENSIndexer Schemas -SELECT DISTINCT ens_indexer_schema_name -FROM ensnode.metadata; - --- Result example: --- ensindexer_schema_name --- ──────────────────────── --- ensindexer_mainnet --- ensindexer_base --- ensindexer_custom -``` - -Each writer has at least one row in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table), with its `ens_indexer_schema_name` pointing to the [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) it owns. - -### Ponder Schema → ENSIndexer Schemas - -All writers share the [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) for RPC caching: - -```mermaid -flowchart TB - WriterA["Writer A
(ENSIndexer Mainnet)"] - WriterB["Writer B
(ENSIndexer Base)"] - WriterC["Writer C
(Custom Writer)"] - Ponder["Ponder Schema
(ponder_sync)"] - RPC["RPC Cache"] - - WriterA -->|reads/writes| Ponder - WriterB -->|reads/writes| Ponder - WriterC -->|reads/writes| Ponder - Ponder -->|caches| RPC -``` - -When any writer performs an RPC call, the result is cached in the [Ponder Schema](/ensdb/concepts/glossary#ponder-schema). Subsequent requests for the same data (from any writer) use the cached result, reducing RPC costs. - -## Data Flow - -### Indexing Flow (Writers) - -1. **Writer** starts indexing from onchain -2. Reads onchain data via RPC (cached in **[Ponder Schema](/ensdb/concepts/glossary#ponder-schema)**) -3. Transforms data according to ENSDb schema -4. Writes transformed data to its **[ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema)** -5. Updates [Indexing Status](/ensdb/concepts/glossary#indexing-status) in **[ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table)** - -```mermaid -flowchart TB - Onchain["Onchain Data
Ethereum / L2s"] - RPC["RPC Node"] - Ponder["Ponder Schema
(ponder_sync)
cached RPC"] - Writer["Writer
(ENSIndexer / Custom)"] - ISchema1["ENSIndexer Schema
(write)"] - ISchema2["ENSIndexer Schema
(write)"] - Metadata["ENSNode Metadata
(status)"] - - Onchain -->|RPC Request| RPC - RPC -->|cached RPC| Ponder - Ponder -->|cached response| Writer - Writer --> ISchema1 - Writer --> ISchema2 - Writer --> Metadata -``` - -### Query Flow (Readers) - -1. **Reader** connects to an ENSDb instance (PostgreSQL database) -2. Queries [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) to discover [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema) -3. Queries specific [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) for data -4. Transforms and serves data to applications - -```mermaid -flowchart LR - Reader["Reader
(ENSApi / Custom)"] - ENSDb["ENSDb Instance
(PostgreSQL DB)"] - ENSNode["ENSNode Schema
(discovery)"] - ENSIndexer["ENSIndexer Schema
(data)"] - App["Application
User Interface"] - - Reader -->|Connect| ENSDb - Reader -->|1. Discover| ENSNode - Reader -->|2. Query| ENSIndexer - Reader -->|3. Serve| App -``` - -```sql --- 1. Connect to specific ENSDb instance --- psql postgresql://host:5432/ensdb_mainnet - --- 2. Discover available schemas in this instance -SELECT ens_indexer_schema_name, value -FROM ensnode.metadata -WHERE key = 'ensindexer_indexing_status'; - --- 3. Query data from a specific ENSIndexer Schema -SELECT * FROM ensindexer_mainnet.v1_domains -WHERE owner_id = '\x1234...'; -``` - -## [Multi-Tenant ENSDb](/ensdb/concepts/glossary#multi-tenant-ensdb): Multiple ENSIndexer Instances Per ENSDb Instance - -An ENSDb instance is **multi-tenant** because a single database can store data from multiple [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) (tenants), each operating independently: - -| Writer | Owns Schema | Purpose | -|--------|-------------|---------| -| ENSIndexer Mainnet | `ensindexer_mainnet` | Ethereum mainnet ENS data | -| ENSIndexer Base | `ensindexer_base` | Base L2 ENS data | -| Custom Writer | `ensindexer_custom` | Custom indexing logic | - -Each tenant (ENSIndexer instance): -- Has its own [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) — isolated data namespace -- Shares the [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) — shared RPC cache across all tenants -- Has its own row in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) — tracked status per tenant - -Multitenancy enables: -- **Separate indexing per chain** — mainnet, L2s, testnets as independent tenants -- **Independent operation** — one tenant can restart while others continue unaffected -- **Custom indexing** — specialized tenants for specific use cases -- **[Schema Version](/ensdb/concepts/glossary#schema-version) evolution** — different tenants can use different schema versions - -## Multi-Instance: Multiple ENSDb Instances Per Server - -A single PostgreSQL server can serve multiple ENSDb instances for different environments: - -```mermaid -flowchart TB - subgraph PGServer["PostgreSQL Server (localhost:5432)"] - direction TB - - subgraph Instance1["ENSDb Instance: ensdb_mainnet
(Production)"] - schemas1["Ponder + ENSNode + ENSIndexer Schemas"] - end - - subgraph Instance2["ENSDb Instance: ensdb_testnet
(Pre-production)"] - schemas2["Ponder + ENSNode + ENSIndexer Schemas"] - end - - subgraph Instance3["ENSDb Instance: ensdb_devnet
(Staging / Local Dev)"] - schemas3["Ponder + ENSNode + ENSIndexer Schemas"] - end - end - - Reader["Reader
(Multi-Instance)"] --> Instance1 - Reader --> Instance2 - Reader --> Instance3 -``` - -This enables: -- **Cost efficiency** — One PostgreSQL server, multiple ENS datasets -- **Organization** — Separate production, staging, and test data -- **Multi-chain aggregation** — Query across instances for cross-chain views - -## Scalability Patterns - -ENSDb can scale to handle massive workloads: - -### Multiple Readers Per Instance - -Any number of readers can query the same ENSDb instance: - -```mermaid -flowchart TB - ENSDb["ENSDb Instance
(ensdb_mainnet)"] - - Reader1["ENSApi #1"] - Reader2["Custom API"] - Reader3["Dashboard"] - Reader4["CLI"] - - ENSDb --> Reader1 - ENSDb --> Reader2 - ENSDb --> Reader3 - ENSDb --> Reader4 -``` - -### Read Replicas - -Distribute read load across PostgreSQL replicas: - -```mermaid -flowchart TB - Primary["Primary ENSDb Instance"] - - Replica1["Read Replica"] - Replica2["Read Replica"] - - Primary -->|Streaming Replication| Replica1 - Primary -->|Streaming Replication| Replica2 -``` - -### Future: ENS Sync Engine - -The upcoming ENS Sync Engine will enable: -- Real-time event streaming from PostgreSQL WAL -- Cache invalidation automation -- Continuous sync between ENSDb instances without running writers - -## Related Concepts - -- **[Glossary](/ensdb/concepts/glossary)** — Definitions for all terms used here -- **[Database Schemas](/ensdb/concepts/database-schemas)** — Deep dive on each schema type -- **[Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle)** — How indexing phases affect database behavior -- **[Building Integrations](/ensdb/integrations/)** — Build custom writers and readers diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx deleted file mode 100644 index 44feaa4d54..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx +++ /dev/null @@ -1,1216 +0,0 @@ ---- -title: Database Schemas -description: Detailed explanation of ENSDb schemas, including Ponder Schema, ENSNode Schema, and the modular ENSIndexer Schema with all its sub-schemas. -sidebar: - label: Database Schemas - order: 4 ---- - -import { Aside } from '@astrojs/starlight/components'; - -ENSDb organizes data using PostgreSQL [database schemas](/ensdb/concepts/glossary#database-schema). Each schema serves a specific purpose and has specific ownership and naming rules. - -For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). - -## Schema Types Overview - -| Schema | Name | Fixed Name? | Owner | Purpose | -|--------|------|-------------|-------|---------| -| [Ponder Schema](#ponder-schema) | `ponder_sync` | Yes | Shared | RPC cache | -| [ENSNode Schema](#ensnode-schema) | `ensnode` | Yes | ENSNode instance | Metadata | -| [ENSIndexer Schema](#ensindexer-schema) | Dynamic | No | [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) | Indexed data | - -## Ponder Schema - -The [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) caches RPC requests and responses to optimize indexing performance. - -### Properties - -- **Name:** `ponder_sync` (fixed) -- **Created by:** Ponder (external tool) -- **Shared by:** All [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) connected to the [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance) -- **[Schema Definition](/ensdb/concepts/glossary#schema-definition):** Defined by Ponder - -### Purpose - -When an [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) indexes onchain data, it makes RPC calls to blockchain nodes. The [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) caches these calls — including block headers, contract storage slots, call results, and event logs — so that: - -1. Multiple [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) don't make duplicate RPC calls -2. Restarting an [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) doesn't require re-fetching cached data -3. RPC costs are reduced - -### Behavior - -- **Do not drop manually** — If dropped, Ponder will recreate it, but with increased RPC costs during the backfill period -- Schema is managed by Ponder, not by ENSNode - -### RPC Cache and ENS Namespaces - -The Ponder Schema caches RPC data **only for the chains being indexed** by ENSIndexer instances connected to the ENSDb instance. This has important implications for ENS Namespaces: - -| Setup | RPC Cache Contents | -|-------|-------------------| -| All ENSIndexer instances index "mainnet" namespace | Mainnet chains only | -| All ENSIndexer instances index "sepolia" namespace | Testnet chains only | -| Mixed namespaces (mainnet + sepolia instances) | Both mainnet and testnet chains | - -**Critical consideration:** In a mixed-namespace setup, approximately 95%+ of cached RPC data will be for mainnet chains due to the significantly larger onchain history and state. This makes the Ponder Schema inefficient for testnet-only operations — the cache will be dominated by mainnet data while testnet data constitutes a tiny fraction. - -**Recommendation:** For a lean ENSNode setup dedicated to testnets, use a **separate ENSDb instance** for the "sepolia" namespace. This ensures: -- The Ponder Schema contains **only testnet RPC cache** -- ENSDb snapshots are **significantly smaller** (no mainnet bloat) -- You can **precisely snapshot and restore** testnet state -- **Substantial RPC cost savings** (hundreds to thousands of dollars) when spinning up new testnet instances, as the cache is already primed with relevant testnet data - -Conversely, mixing mainnet and sepolia in a single ENSDb instance pollutes the cache with mostly mainnet data, making it unsuitable for efficient testnet-only deployments. - -## ENSNode Schema - -The [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) stores metadata about [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) and their [Indexing Status](/ensdb/concepts/glossary#indexing-status). - -### Properties - -- **Name:** `ensnode` (fixed) -- **Created by:** First ENSIndexer instance to connect -- **Schema Definition:** `@ensnode/ensdb-sdk/ensnode` - -### Multi-Tenancy: Tracking Multiple ENSIndexer Schemas - -A single ENSNode Schema tracks metadata for **multiple** ENSIndexer Schemas within the same ENSDb instance. This enables multi-tenant indexing where different chains, configurations, or use cases can be indexed independently. - -```mermaid -erDiagram - ENSNODE_METADATA ||--o{ ENSINDEXER_MAINNET : "tracks" - ENSNODE_METADATA ||--o{ ENSINDEXER_L2 : "tracks" - ENSNODE_METADATA ||--o{ ENSINDEXER_CUSTOM : "tracks" - - ENSNODE_METADATA { - text ens_indexer_schema_name PK - text key PK - text value_version - jsonb value - } - - ENSINDEXER_MAINNET["ensindexer_mainnet Schema"] { - text schema_name "ensindexer_mainnet" - } - - ENSINDEXER_L2["ensindexer_l2 Schema"] { - text schema_name "ensindexer_l2" - } - - ENSINDEXER_CUSTOM["ensindexer_custom Schema"] { - text schema_name "ensindexer_custom" - } -``` - -### Contents - -The ENSNode Schema contains a single table: [ENSNode Metadata Table](#ensnode-metadata-table). - -### ENSNode Metadata Table - -| Column | Type | Description | -|--------|------|-------------| -| `ens_indexer_schema_name` | `text` | Name of the [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) this record belongs to | -| `key` | `text` | Type of metadata record | -| `value_version` | `text` | [ENSNode Metadata Value Version](/ensdb/concepts/glossary#ensnode-metadata-value-version) | -| `value` | `jsonb` | The metadata content | - -**Primary Key:** (`ens_indexer_schema_name`, `key`) - -### Common Metadata Keys - -| Key | Purpose | -|-----|---------| -| `ensindexer_indexing_status` | Current [Indexing Status](/ensdb/concepts/glossary#indexing-status) and progress | -| `ensindexer_public_config` | Public configuration of the [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) | -| `ensdb_version` | [ENSDb version](/ensdb/concepts/glossary#ensdb-sdk) information for this instance | - -### Behavior - -- **Do not drop manually** — If dropped, ENSNode cannot track [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) -- The first [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) to connect creates the schema and migrations table -- Each [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) writes its own row(s) with its [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name) - -### Schema Discovery - -Query [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) to discover all [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema): - -```sql -SELECT DISTINCT ens_indexer_schema_name -FROM ensnode.metadata; -``` - -## ENSIndexer Schema - -Each [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) owns an [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) where it writes indexed ENS data. - -### Properties - -- **Name:** Dynamic, determined by ENSIndexer instance configuration -- **Created by:** Ponder app running inside the ENSIndexer instance -- **Schema Definition:** `@ensnode/ensdb-sdk/ensindexer-abstract` - -### Modular Sub-Schema Architecture - -The ENSIndexer Schema is **modular** — it's composed of five distinct sub-schemas that each serve a specific purpose: - -| Sub-Schema | SDK Path | Purpose | Key Entities | -|------------|----------|---------|--------------| -| **ensv2** | `/ensindexer-abstract/ensv2.schema` | Core ENS domain and event indexing | Domains, Events, Registrations, Labels | -| **protocol-acceleration** | `/ensindexer-abstract/protocol-acceleration.schema` | Resolution acceleration and resolver records | Resolvers, Reverse Names, Domain-Resolver Relations | -| **registrars** | `/ensindexer-abstract/registrars.schema` | Registration lifecycle tracking | Subregistries, Registration Lifecycles, Registrar Actions | -| **subgraph** | `/ensindexer-abstract/subgraph.schema` | Backward compatibility with legacy ENS Subgraph | Subgraph-compatible entities and events | -| **tokenscope** | `/ensindexer-abstract/tokenscope.schema` | NFT market data and token tracking | Name Sales, Name Tokens | - -These sub-schemas work together to provide a complete picture of ENS onchain state while maintaining separation of concerns. All five sub-schemas exist within a single ENSIndexer Schema namespace. - -```mermaid -flowchart TB - subgraph ENSIndexerSchema["ENSIndexer Schema (Dynamic Name)"] - subgraph ENSv2["ensv2 Sub-Schema"] - v1d[v1_domains] - v2d[v2_domains] - reg[registries] - lbl[labels] - acc[accounts] - evt[events] - dom_evt[domain_events] - reg_tbl[registrations] - rnl[renewals] - perm[permissions] - end - - subgraph Protocol["protocol-acceleration Sub-Schema"] - res[resolvers] - rr[resolver_records] - rar[resolver_address_records] - rtr[resolver_text_records] - drr[domain_resolver_relations] - rnr[reverse_name_records] - mn[migrated_nodes] - end - - subgraph Registrars["registrars Sub-Schema"] - subreg[subregistries] - rl[registration_lifecycles] - ra[registrar_actions] - ram[internal_registrar_action_metadata] - end - - subgraph Subgraph["subgraph Sub-Schema"] - sgd[subgraph_domains] - sga[subgraph_accounts] - sgr[subgraph_registrations] - sgres[subgraph_resolvers] - sgwd[subgraph_wrapped_domains] - sge[subgraph_*_events] - end - - subgraph TokenScope["tokenscope Sub-Schema"] - ns[name_sales] - nt[name_tokens] - end - end -``` - -### Naming - -[ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) names are dynamic. The name is determined by the [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) based on its configuration. - -Examples: `ensindexer_0`, `ensindexer_mainnet`, `ensindexer_abc123` - -### Index Behavior - -Indexes on [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) tables are created/dropped based on [Indexing Status](/ensdb/concepts/glossary#indexing-status): - -| Status | [Indexes](/ensdb/concepts/glossary#database-objects) | Reason | -|--------|---------|--------| -| Backfill | Dropped | Optimize write throughput for historical data | -| Following | Created | Optimize read queries for live data | - -See [Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle) for details. - ---- - -## Sub-Schema Reference - -### ensv2 Sub-Schema - -The **ensv2** sub-schema provides the core ENS protocol indexing functionality. It tracks domains (both ENSv1 and ENSv2), events, registrations, renewals, and labels. - -```mermaid -erDiagram - V1_DOMAINS ||--o{ V1_DOMAINS : "parent/children" - V1_DOMAINS ||--o{ REGISTRATIONS : "has" - V1_DOMAINS ||--|| LABELS : "labelHash" - V1_DOMAINS ||--|| ACCOUNTS : "owner" - V1_DOMAINS ||--o{ DOMAIN_EVENTS : "events" - - V2_DOMAINS ||--|| REGISTRIES : "registryId" - V2_DOMAINS ||--o{ REGISTRATIONS : "has" - V2_DOMAINS ||--|| LABELS : "labelHash" - V2_DOMAINS ||--|| ACCOUNTS : "owner" - V2_DOMAINS ||--o{ DOMAIN_EVENTS : "events" - V2_DOMAINS ||--|| REGISTRIES : "subregistryId" - - REGISTRIES ||--o{ V2_DOMAINS : "has" - - EVENTS ||--o{ DOMAIN_EVENTS : "tracked_by" - EVENTS ||--o{ RESOLVER_EVENTS : "tracked_by" - EVENTS ||--o{ PERMISSIONS_EVENTS : "tracked_by" - - DOMAIN_EVENTS }o--|| V1_DOMAINS : "domainId" - DOMAIN_EVENTS }o--|| V2_DOMAINS : "domainId" - - REGISTRATIONS ||--o{ RENEWALS : "has" - REGISTRATIONS ||--|| ACCOUNTS : "registrant" - REGISTRATIONS ||--|| EVENTS : "eventId" - - RENEWALS ||--|| REGISTRATIONS : "registration" - RENEWALS ||--|| EVENTS : "eventId" - - PERMISSIONS ||--o{ PERMISSIONS_RESOURCE : "resources" - PERMISSIONS ||--o{ PERMISSIONS_USER : "users" - - PERMISSIONS_RESOURCE ||--|| PERMISSIONS : "permissions" - PERMISSIONS_USER ||--|| PERMISSIONS : "permissions" - PERMISSIONS_USER ||--|| ACCOUNTS : "user" - - LABELS ||--o{ V1_DOMAINS : "domains" - LABELS ||--o{ V2_DOMAINS : "domains" -``` - -#### Table: `events` - -Core event log table storing all onchain event metadata. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Ponder's event.id | -| `chain_id` | `integer` | Not Null, Indexed | Chain ID (EIP-155) | -| `block_number` | `bigint` | Not Null | Block number | -| `block_hash` | `bytea` | Not Null | Block hash | -| `timestamp` | `bigint` | Not Null, Indexed | Block timestamp (Unix seconds) | -| `transaction_hash` | `bytea` | Not Null | Transaction hash | -| `transaction_index` | `integer` | Not Null | Transaction index in block | -| `from` | `bytea` | Not Null, Indexed | Transaction sender | -| `to` | `bytea` | Nullable | Transaction recipient (null for contract deployment) | -| `address` | `bytea` | Not Null | Contract address that emitted the event | -| `log_index` | `integer` | Not Null | Log index in transaction | -| `selector` | `bytea` | Not Null, Indexed | Event selector (topic0) | -| `topics` | `bytea[]` | Not Null | All event topics | -| `data` | `bytea` | Not Null | Event data payload | - -**Indexes:** -- Primary Key: `id` -- `bySelector`: `selector` -- `byFrom`: `from` -- `byTimestamp`: `timestamp` - -#### Table: `domain_events` - -Join table linking domains to their events. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `domain_id` | `text` | Not Null, PK | Domain ID (namehash) | -| `event_id` | `text` | Not Null, PK | Event ID | - -**Primary Key:** (`domain_id`, `event_id`) - -#### Table: `resolver_events` - -Join table linking resolvers to their events. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `resolver_id` | `text` | Not Null, PK | Resolver ID | -| `event_id` | `text` | Not Null, PK | Event ID | - -**Primary Key:** (`resolver_id`, `event_id`) - -#### Table: `permissions_events` - -Join table linking permissions to their events. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `permissions_id` | `text` | Not Null, PK | Permissions ID | -| `event_id` | `text` | Not Null, PK | Event ID | - -**Primary Key:** (`permissions_id`, `event_id`) - -#### Table: `accounts` - -Ethereum accounts that interact with ENS. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `bytea` | Primary Key | Ethereum address | - -#### Table: `registries` - -ENSv2 registries (both root and subregistries). - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Registry ID (CAIP-10 format: chainId:address) | -| `chain_id` | `integer` | Not Null | Chain ID | -| `address` | `bytea` | Not Null | Registry contract address | - -**Indexes:** -- Primary Key: `id` -- `byId`: Unique index on (`chain_id`, `address`) - -#### Table: `v1_domains` - -ENSv1 domain records (flat namespace, keyed by node). - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Domain ID (node/namehash) | -| `parent_id` | `text` | Not Null, Indexed | Parent domain ID | -| `owner_id` | `bytea` | Nullable, Indexed | Effective owner address | -| `label_hash` | `bytea` | Not Null, Indexed | Label hash | -| `root_registry_owner_id` | `bytea` | Nullable | ENSv1 Registry owner (zeroAddress = null) | - -**Indexes:** -- Primary Key: `id` -- `byParent`: `parent_id` -- `byOwner`: `owner_id` -- `byLabelHash`: `label_hash` - -#### Table: `v2_domains` - -ENSv2 domain records (registry-based namespace). - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Domain ID | -| `token_id` | `bigint` | Not Null | ERC721 token ID | -| `registry_id` | `text` | Not Null, Indexed | Parent registry ID | -| `subregistry_id` | `text` | Nullable, Indexed | Subregistry ID (if domain is a registry) | -| `owner_id` | `bytea` | Nullable, Indexed | Domain owner address | -| `label_hash` | `bytea` | Not Null, Indexed | Label hash | - -**Indexes:** -- Primary Key: `id` -- `byRegistry`: `registry_id` -- `bySubregistry`: `subregistry_id` (where not null) -- `byOwner`: `owner_id` -- `byLabelHash`: `label_hash` - -#### Enum: `registration_type` - -Registration types supported by the schema. - -| Value | Description | -|-------|-------------| -| `NameWrapper` | NameWrapper registration | -| `BaseRegistrar` | BaseRegistrar registration | -| `ThreeDNS` | 3DNS registration | -| `ENSv2RegistryRegistration` | ENSv2 Registry registration | -| `ENSv2RegistryReservation` | ENSv2 Registry reservation | - -#### Table: `registrations` - -Domain registration records (polymorphic across types). - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Registration ID (domainId:registrationIndex) | -| `domain_id` | `text` | Not Null, Indexed | Domain ID | -| `registration_index` | `integer` | Not Null | Index of this registration for the domain | -| `type` | `registration_type` | Not Null | Registration type | -| `start` | `bigint` | Not Null | Registration start timestamp | -| `expiry` | `bigint` | Nullable | Registration expiry timestamp | -| `grace_period` | `bigint` | Nullable | Grace period duration (for BaseRegistrar) | -| `registrar_chain_id` | `integer` | Not Null | Registrar chain ID | -| `registrar_address` | `bytea` | Not Null | Registrar contract address | -| `registrant_id` | `bytea` | Nullable | Registrant address | -| `unregistrant_id` | `bytea` | Nullable | Previous registrant (on transfer) | -| `referrer` | `bytea` | Nullable | Encoded referrer data | -| `fuses` | `integer` | Nullable | NameWrapper fuses | -| `base` | `bigint` | Nullable | Base cost in wei | -| `premium` | `bigint` | Nullable | Premium cost in wei | -| `wrapped` | `boolean` | Default: false | Whether registration is wrapped | -| `event_id` | `text` | Not Null | Event that created this registration | - -**Indexes:** -- Primary Key: `id` -- `byId`: Unique index on (`domain_id`, `registration_index`) - -#### Table: `latest_registration_indexes` - -Tracks the latest registration index for each domain. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `domain_id` | `text` | Primary Key | Domain ID | -| `registration_index` | `integer` | Not Null | Latest registration index | - -#### Table: `renewals` - -Domain renewal records. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Renewal ID (domainId:registrationIndex:renewalIndex) | -| `domain_id` | `text` | Not Null | Domain ID | -| `registration_index` | `integer` | Not Null | Registration index | -| `renewal_index` | `integer` | Not Null | Index of this renewal for the registration | -| `duration` | `bigint` | Not Null | Renewal duration in seconds | -| `referrer` | `bytea` | Nullable | Encoded referrer data | -| `base` | `bigint` | Nullable | Base cost in wei | -| `premium` | `bigint` | Nullable | Premium cost in wei | -| `event_id` | `text` | Not Null | Event that created this renewal | - -**Indexes:** -- Primary Key: `id` -- `byId`: Unique index on (`domain_id`, `registration_index`, `renewal_index`) - -#### Table: `latest_renewal_indexes` - -Tracks the latest renewal index for each registration. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `domain_id` | `text` | Not Null, PK | Domain ID | -| `registration_index` | `integer` | Not Null, PK | Registration index | -| `renewal_index` | `integer` | Not Null | Latest renewal index | - -**Primary Key:** (`domain_id`, `registration_index`) - -#### Table: `permissions` - -Permission manager contracts (ERC-7715 style). - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Permissions ID (chainId:address) | -| `chain_id` | `integer` | Not Null | Chain ID | -| `address` | `bytea` | Not Null | Contract address | - -**Indexes:** -- Primary Key: `id` -- `byId`: Unique index on (`chain_id`, `address`) - -#### Table: `permissions_resources` - -Permission resources within a permission manager. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Resource ID (chainId:address:resource) | -| `chain_id` | `integer` | Not Null | Chain ID | -| `address` | `bytea` | Not Null | Permission manager address | -| `resource` | `bigint` | Not Null | Resource identifier | - -**Indexes:** -- Primary Key: `id` -- `byId`: Unique index on (`chain_id`, `address`, `resource`) - -#### Table: `permissions_users` - -Permission user assignments. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | User assignment ID (chainId:address:resource:user) | -| `chain_id` | `integer` | Not Null | Chain ID | -| `address` | `bytea` | Not Null | Permission manager address | -| `resource` | `bigint` | Not Null | Resource identifier | -| `user` | `bytea` | Not Null | User address | -| `roles` | `bigint` | Not Null | Roles bitmap | - -**Indexes:** -- Primary Key: `id` -- `byId`: Unique index on (`chain_id`, `address`, `resource`, `user`) - -#### Table: `labels` - -Label hash to interpreted label mapping (rainbow table for name healing). - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `label_hash` | `bytea` | Primary Key | Label hash | -| `interpreted` | `text` | Not Null, Indexed | Interpreted label text | - -**Indexes:** -- Primary Key: `label_hash` -- `byInterpreted`: `interpreted` - -#### Table: `registry_canonical_domains` - -Tracks canonical domain references for registries (temporary table). - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `registry_id` | `text` | Primary Key | Registry ID | -| `domain_id` | `text` | Not Null | Canonical domain ID | - ---- - -### protocol-acceleration Sub-Schema - -The **protocol-acceleration** sub-schema provides data structures that accelerate ENS resolution and track resolver relationships. - -```mermaid -erDiagram - RESOLVERS ||--o{ RESOLVER_RECORDS : "has" - RESOLVERS { - text id PK - integer chain_id - bytea address - } - - RESOLVER_RECORDS ||--o{ RESOLVER_ADDRESS_RECORDS : "address_records" - RESOLVER_RECORDS ||--o{ RESOLVER_TEXT_RECORDS : "text_records" - RESOLVER_RECORDS { - text id PK - integer chain_id - bytea address - bytea node - text name - } - - RESOLVER_ADDRESS_RECORDS { - integer chain_id PK - bytea address PK - bytea node PK - bigint coin_type PK - text value - } - - RESOLVER_TEXT_RECORDS { - integer chain_id PK - bytea address PK - bytea node PK - text key PK - text value - } - - DOMAIN_RESOLVER_RELATIONS }o--|| RESOLVERS : "resolver" - DOMAIN_RESOLVER_RELATIONS { - integer chain_id PK - bytea address PK - bytea domain_id PK - bytea resolver - } - - REVERSE_NAME_RECORDS ||--|| ACCOUNTS : "address" - REVERSE_NAME_RECORDS { - bytea address PK - bigint coin_type PK - text value - } - - MIGRATED_NODES { - bytea node PK - } -``` - -#### Table: `reverse_name_records` - -ENSIP-19 reverse name records indexed by account and coin type. - - - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `address` | `bytea` | Not Null, PK | Account address | -| `coin_type` | `bigint` | Not Null, PK | SLIP-44 coin type | -| `value` | `text` | Not Null | Reverse name record value | - -**Primary Key:** (`address`, `coin_type`) - -#### Table: `domain_resolver_relations` - -Domain-to-resolver mappings for accelerated lookups. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `chain_id` | `integer` | Not Null, PK | Chain ID | -| `address` | `bytea` | Not Null, PK | Registry address | -| `domain_id` | `bytea` | Not Null, PK | Domain ID (node) | -| `resolver` | `bytea` | Not Null | Resolver contract address | - -**Primary Key:** (`chain_id`, `address`, `domain_id`) - -#### Table: `resolvers` - -Resolver contracts that have emitted events. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Resolver ID (chainId:address) | -| `chain_id` | `integer` | Not Null | Chain ID | -| `address` | `bytea` | Not Null | Resolver contract address | - -**Indexes:** -- Primary Key: `id` -- `byId`: Unique index on (`chain_id`, `address`) - -#### Table: `resolver_records` - -Resolver records for specific nodes. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Record ID (chainId:resolver:node) | -| `chain_id` | `integer` | Not Null | Chain ID | -| `address` | `bytea` | Not Null | Resolver address | -| `node` | `bytea` | Not Null | Node (namehash) | -| `name` | `text` | Nullable | ENSIP-3 name record value | - -**Indexes:** -- Primary Key: `id` -- `byId`: Unique index on (`chain_id`, `address`, `node`) - - - -#### Table: `resolver_address_records` - -Address records (ENSIP-9) within resolver records. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `chain_id` | `integer` | Not Null, PK | Chain ID | -| `address` | `bytea` | Not Null, PK | Resolver address | -| `node` | `bytea` | Not Null, PK | Node (namehash) | -| `coin_type` | `bigint` | Not Null, PK | SLIP-44 coin type | -| `value` | `text` | Not Null | Address record value (interpreted) | - -**Primary Key:** (`chain_id`, `address`, `node`, `coin_type`) - -#### Table: `resolver_text_records` - -Text records (ENSIP-5) within resolver records. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `chain_id` | `integer` | Not Null, PK | Chain ID | -| `address` | `bytea` | Not Null, PK | Resolver address | -| `node` | `bytea` | Not Null, PK | Node (namehash) | -| `key` | `text` | Not Null, PK | Text record key | -| `value` | `text` | Not Null | Text record value | - -**Primary Key:** (`chain_id`, `address`, `node`, `key`) - -#### Table: `migrated_nodes` - -Tracks nodes that have migrated from RegistryOld to the new Registry. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `node` | `bytea` | Primary Key | Migrated node (namehash) | - - - ---- - -### registrars Sub-Schema - -The **registrars** sub-schema tracks registration lifecycles and registrar actions across different registrar implementations. - -```mermaid -erDiagram - SUBREGISTRIES ||--o{ REGISTRATION_LIFECYCLES : "manages" - SUBREGISTRIES { - text subregistry_id PK - bytea node - } - - REGISTRATION_LIFECYCLES ||--o{ REGISTRAR_ACTIONS : "actions" - REGISTRATION_LIFECYCLES { - bytea node PK - text subregistry_id - bigint expires_at - } - - REGISTRAR_ACTIONS ||--o{ EVENTS : "eventIds" - REGISTRAR_ACTIONS { - text id PK - registrar_action_type type - text subregistry_id - bytea node - bigint incremental_duration - bigint base_cost - bigint premium - bigint total - bytea registrant - bytea encoded_referrer - bytea decoded_referrer - bigint block_number - bigint timestamp - bytea transaction_hash - text[] event_ids - } -``` - -#### Table: `subregistries` - -Subregistry contracts that manage subname registrations. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `subregistry_id` | `text` | Primary Key | Subregistry ID (CAIP-10: chainId:address) | -| `node` | `bytea` | Not Null, Unique | Node this subregistry manages subnames of | - -**Indexes:** -- Primary Key: `subregistry_id` -- `uniqueNode`: Unique index on `node` - -#### Table: `registration_lifecycles` - -Tracks the current registration lifecycle for each name. - - - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `node` | `bytea` | Primary Key | Domain node (namehash) | -| `subregistry_id` | `text` | Not Null | Subregistry managing this registration | -| `expires_at` | `bigint` | Not Null | Expiration timestamp | - -**Indexes:** -- Primary Key: `node` -- `bySubregistry`: `subregistry_id` - -#### Enum: `registrar_action_type` - -Types of registrar actions. - -| Value | Description | -|-------|-------------| -| `registration` | New registration | -| `renewal` | Renewal/extension | - -#### Table: `registrar_actions` - -Logical registrar actions aggregated from multiple events. - - - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Action ID (Ponder checkpoint string) | -| `type` | `registrar_action_type` | Not Null | Action type | -| `subregistry_id` | `text` | Not Null | Subregistry ID | -| `node` | `bytea` | Not Null | Domain node | -| `incremental_duration` | `bigint` | Not Null | Duration added to registration | -| `base_cost` | `bigint` | Nullable | Base cost in wei | -| `premium` | `bigint` | Nullable | Premium cost in wei | -| `total` | `bigint` | Nullable | Total cost in wei (base + premium) | -| `registrant` | `bytea` | Not Null | Action initiator address | -| `encoded_referrer` | `bytea` | Nullable | Raw 32-byte referrer value | -| `decoded_referrer` | `bytea` | Nullable | Decoded referrer address | -| `block_number` | `bigint` | Not Null | Block number | -| `timestamp` | `bigint` | Not Null | Block timestamp | -| `transaction_hash` | `bytea` | Not Null | Transaction hash | -| `event_ids` | `text[]` | Not Null | Contributing event IDs | - -**Indexes:** -- Primary Key: `id` -- `byDecodedReferrer`: `decoded_referrer` -- `byTimestamp`: `timestamp` - -#### Table: `_ensindexer_registrar_action_metadata` - -Internal metadata for aggregating registrar action data across events. - - - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `metadata_type` | `_ensindexer_registrar_action_metadata_type` | Primary Key | Metadata type | -| `logical_event_key` | `text` | Not Null | Key for grouping events (domainId:transactionHash) | -| `logical_event_id` | `text` | Not Null | Current logical action ID being built | - ---- - -### subgraph Sub-Schema - -The **subgraph** sub-schema provides backward compatibility with the legacy ENS Subgraph data model. When paired with `@ensnode/ponder-subgraph`, it enables a fully subgraph-compatible GraphQL API. - -```mermaid -erDiagram - SUBGRAPH_DOMAINS ||--o{ SUBGRAPH_REGISTRATIONS : "has" - SUBGRAPH_DOMAINS ||--|| SUBGRAPH_RESOLVERS : "resolver" - SUBGRAPH_DOMAINS ||--o{ SUBGRAPH_WRAPPED_DOMAINS : "wrapped" - SUBGRAPH_DOMAINS ||--|| SUBGRAPH_ACCOUNTS : "owner" - SUBGRAPH_DOMAINS ||--|| SUBGRAPH_ACCOUNTS : "registrant" - SUBGRAPH_DOMAINS ||--|| SUBGRAPH_ACCOUNTS : "wrapped_owner" - SUBGRAPH_DOMAINS ||--|| SUBGRAPH_ACCOUNTS : "resolved_address" - SUBGRAPH_DOMAINS ||--o{ SUBGRAPH_DOMAINS : "parent/subdomains" - - SUBGRAPH_ACCOUNTS ||--o{ SUBGRAPH_DOMAINS : "domains" - SUBGRAPH_ACCOUNTS ||--o{ SUBGRAPH_WRAPPED_DOMAINS : "wrapped_domains" - SUBGRAPH_ACCOUNTS ||--o{ SUBGRAPH_REGISTRATIONS : "registrations" - - SUBGRAPH_RESOLVERS ||--o{ SUBGRAPH_ADDR_CHANGED : "events" - SUBGRAPH_RESOLVERS ||--o{ SUBGRAPH_TEXT_CHANGED : "events" - SUBGRAPH_RESOLVERS ||--o{ SUBGRAPH_CONTENTHASH_CHANGED : "events" - - SUBGRAPH_REGISTRATIONS ||--o{ SUBGRAPH_NAME_REGISTERED : "events" - SUBGRAPH_REGISTRATIONS ||--o{ SUBGRAPH_NAME_RENEWED : "events" -``` - -#### Table: `subgraph_domains` - -Subgraph-compatible domain entity. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `bytea` | Primary Key | Namehash | -| `name` | `text` | Nullable | Domain name (subgraph-interpreted or normalized) | -| `label_name` | `text` | Nullable | Label name | -| `labelhash` | `bytea` | Nullable, Indexed | Label hash | -| `parent_id` | `bytea` | Nullable, Indexed | Parent domain namehash | -| `subdomain_count` | `integer` | Not Null, Default: 0 | Number of subdomains | -| `resolved_address_id` | `bytea` | Nullable, Indexed | Resolved address | -| `resolver_id` | `text` | Nullable | Resolver ID | -| `ttl` | `bigint` | Nullable | Time-to-live | -| `is_migrated` | `boolean` | Not Null, Default: false | Migration status | -| `created_at` | `bigint` | Not Null | Creation timestamp | -| `owner_id` | `bytea` | Not Null, Indexed | Owner address | -| `registrant_id` | `bytea` | Nullable, Indexed | Registrant address | -| `wrapped_owner_id` | `bytea` | Nullable, Indexed | Wrapped owner address | -| `expiry_date` | `bigint` | Nullable | Expiration date | - -**Indexes:** -- Primary Key: `id` -- `byLabelhash`: `labelhash` -- `byParentId`: `parent_id` -- `byOwnerId`: `owner_id` -- `byRegistrantId`: `registrant_id` -- `byWrappedOwnerId`: `wrapped_owner_id` -- `byResolvedAddressId`: `resolved_address_id` - - - -#### Table: `subgraph_accounts` - -Subgraph-compatible account entity. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `bytea` | Primary Key | Ethereum address | - -#### Table: `subgraph_resolvers` - -Subgraph-compatible resolver entity. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Resolver ID (domainId:address) | -| `domain_id` | `bytea` | Not Null, Indexed | Domain ID | -| `address` | `bytea` | Not Null | Resolver address | -| `addr_id` | `bytea` | Nullable | Current addr record | -| `content_hash` | `text` | Nullable | Contenthash | -| `texts` | `text[]` | Nullable | Observed text record keys | -| `coin_types` | `bigint[]` | Nullable | Observed coin types | - -**Indexes:** -- Primary Key: `id` -- `byDomainId`: `domain_id` - -#### Table: `subgraph_registrations` - -Subgraph-compatible registration entity. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `bytea` | Primary Key | Registration ID | -| `domain_id` | `bytea` | Not Null, Indexed | Domain ID | -| `registration_date` | `bigint` | Not Null | Registration timestamp | -| `expiry_date` | `bigint` | Not Null | Expiry timestamp | -| `cost` | `bigint` | Nullable | Registration cost | -| `registrant_id` | `bytea` | Not Null, Indexed | Registrant address | -| `label_name` | `text` | Nullable | Label name | - -**Indexes:** -- Primary Key: `id` -- `byDomainId`: `domain_id` -- `byRegistrationDate`: `registration_date` -- `byExpiryDate`: `expiry_date` - -#### Table: `subgraph_wrapped_domains` - -Subgraph-compatible wrapped domain entity. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `bytea` | Primary Key | Wrapped domain ID | -| `domain_id` | `bytea` | Not Null, Indexed | Domain ID | -| `expiry_date` | `bigint` | Not Null | Expiry timestamp | -| `fuses` | `integer` | Not Null | Fuses bitmap | -| `owner_id` | `bytea` | Not Null | Owner address | -| `name` | `text` | Nullable | DNS-encoded name | - -**Indexes:** -- Primary Key: `id` -- `byDomainId`: `domain_id` - -#### Domain Event Tables - -All domain events share a common structure: - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Event ID | -| `block_number` | `integer` | Not Null | Block number | -| `transaction_id` | `bytea` | Not Null | Transaction hash | -| `domain_id` | `bytea` | Not Null, Indexed | Domain ID | - -**Tables:** -- `subgraph_transfers` — Domain ownership transfers (`owner_id`) -- `subgraph_new_owners` — New owner events (`owner_id`, `parent_domain_id`) -- `subgraph_new_resolvers` — Resolver set events (`resolver_id`) -- `subgraph_new_ttls` — TTL change events (`ttl`) -- `subgraph_wrapped_transfers` — Wrapped domain transfers (`owner_id`) -- `subgraph_name_wrapped` — Name wrapped events (`name`, `fuses`, `owner_id`, `expiry_date`) -- `subgraph_name_unwrapped` — Name unwrapped events (`owner_id`) -- `subgraph_fuses_set` — Fuses set events (`fuses`) -- `subgraph_expiry_extended` — Expiry extension events (`expiry_date`) - -#### Registration Event Tables - -Registration events include: - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Event ID | -| `block_number` | `integer` | Not Null | Block number | -| `transaction_id` | `bytea` | Not Null | Transaction hash | -| `registration_id` | `bytea` | Not Null, Indexed | Registration ID | - -**Tables:** -- `subgraph_name_registered` — Registration events (`registrant_id`, `expiry_date`) -- `subgraph_name_renewed` — Renewal events (`expiry_date`) -- `subgraph_name_transferred` — Registration transfer events (`new_owner_id`) - -#### Resolver Event Tables - -Resolver events include: - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Event ID | -| `block_number` | `integer` | Not Null | Block number | -| `transaction_id` | `bytea` | Not Null | Transaction hash | -| `resolver_id` | `text` | Not Null, Indexed | Resolver ID | - -**Tables:** -- `subgraph_addr_changed` — Addr record changes (`addr_id`) -- `subgraph_multicoin_addr_changed` — Multicoin addr changes (`coin_type`, `addr`) -- `subgraph_name_changed` — Name record changes (`name`) -- `subgraph_abi_changed` — ABI changes (`content_type`) -- `subgraph_pubkey_changed` — Pubkey changes (`x`, `y`) -- `subgraph_text_changed` — Text record changes (`key`, `value`) -- `subgraph_contenthash_changed` — Contenthash changes (`hash`) -- `subgraph_interface_changed` — Interface changes (`interface_id`, `implementer`) -- `subgraph_authorisation_changed` — Authorisation changes (`owner`, `target`, `is_authorized`) -- `subgraph_version_changed` — Version changes (`version`) - ---- - -### tokenscope Sub-Schema - -The **tokenscope** sub-schema tracks NFT sales and token ownership for ENS names in secondary markets. - -```mermaid -erDiagram - NAME_TOKENS ||--o{ NAME_SALES : "sold_via" - NAME_TOKENS { - text id PK "CAIP-19 Asset ID" - bytea domain_id - integer chain_id - bytea contract_address - bigint token_id - text asset_namespace "erc721/erc1155" - text asset_id "CAIP-19" - bytea owner - text mint_status "minted/burned" - } - - NAME_SALES ||--|| NAME_TOKENS : "token_sold" - NAME_SALES { - text id PK "{chainId}-{blockNumber}-{logIndex}" - integer chain_id - bigint block_number - integer log_index - bytea transaction_hash - bytea order_hash "Seaport order" - bytea contract_address - bigint token_id - text asset_namespace - text asset_id - bytea domain_id - bytea buyer - bytea seller - text currency "ETH/USDC/DAI" - bigint amount - bigint timestamp - } -``` - -#### Table: `name_sales` - -NFT sale records from secondary markets. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | Sale ID (format: `{chainId}-{blockNumber}-{logIndex}`) | -| `chain_id` | `integer` | Not Null | Chain ID | -| `block_number` | `bigint` | Not Null | Block number | -| `log_index` | `integer` | Not Null | Log index | -| `transaction_hash` | `bytea` | Not Null | Transaction hash | -| `order_hash` | `bytea` | Not Null | Seaport order hash | -| `contract_address` | `bytea` | Not Null | NFT contract address | -| `token_id` | `bigint` | Not Null | Token ID | -| `asset_namespace` | `text` | Not Null | `erc721` or `erc1155` | -| `asset_id` | `text` | Not Null | CAIP-19 Asset ID | -| `domain_id` | `bytea` | Not Null, Indexed | Domain namehash | -| `buyer` | `bytea` | Not Null, Indexed | Buyer address | -| `seller` | `bytea` | Not Null, Indexed | Seller address | -| `currency` | `text` | Not Null | Payment currency (`ETH`, `USDC`, `DAI`) | -| `amount` | `bigint` | Not Null | Amount in smallest unit | -| `timestamp` | `bigint` | Not Null | Block timestamp | - -**Indexes:** -- Primary Key: `id` -- `idx_domainId`: `domain_id` -- `idx_assetId`: `asset_id` -- `idx_buyer`: `buyer` -- `idx_seller`: `seller` -- `idx_timestamp`: `timestamp` - - - -#### Table: `name_tokens` - -ENS name NFT tracking. - -| Column | Type | Constraints | Description | -|--------|------|-------------|-------------| -| `id` | `text` | Primary Key | CAIP-19 Asset ID | -| `domain_id` | `bytea` | Not Null, Indexed | Domain namehash | -| `chain_id` | `integer` | Not Null | Chain ID | -| `contract_address` | `bytea` | Not Null | NFT contract address | -| `token_id` | `bigint` | Not Null | Token ID | -| `asset_namespace` | `text` | Not Null | `erc721` or `erc1155` | -| `owner` | `bytea` | Not Null, Indexed | Current owner (zeroAddress if burned) | -| `mint_status` | `text` | Not Null | `minted` or `burned` | - -**Indexes:** -- Primary Key: `id` -- `idx_domainId`: `domain_id` -- `idx_owner`: `owner` - - - ---- - -## Cross-Sub-Schema Relationships - -While each sub-schema serves a distinct purpose, tables across sub-schemas are related to provide a complete ENS data model: - -| From Sub-Schema | Table | Relationship | To Sub-Schema | Table | Via | -|-----------------|-------|--------------|---------------|-------|-----| -| ensv2 | `v1_domains` | has | registrars | `registrations` | `domain_id` | -| ensv2 | `v2_domains` | has | registrars | `registrations` | `domain_id` | -| ensv2 | `v1_domains` | maps to | subgraph | `subgraph_domains` | `id` (node) | -| ensv2 | `v2_domains` | maps to | subgraph | `subgraph_domains` | `id` (node) | -| protocol-acceleration | `resolver_records` | resolves | ensv2 | `v1_domains` | `node` | -| protocol-acceleration | `resolver_records` | resolves | ensv2 | `v2_domains` | `node` | -| tokenscope | `name_tokens` | represents | ensv2 | `v1_domains` | `domain_id` | -| tokenscope | `name_tokens` | represents | ensv2 | `v2_domains` | `domain_id` | -| registrars | `registration_lifecycles` | tracks | ensv2 | `v1_domains` | `node` | - -## Multiple Instances - -Multiple [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema) can exist in one [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance), each containing all five sub-schemas: - -```mermaid -flowchart TB - subgraph ENSDb["ENSDb Instance"] - ENSNODE["ensnode.metadata
(tracks all)"] - - subgraph Mainnet["ensindexer_mainnet"] - M1["ensv2.*"] - M2["protocol-acceleration.*"] - M3["registrars.*"] - M4["subgraph.*"] - M5["tokenscope.*"] - end - - subgraph L2["ensindexer_l2"] - L1["ensv2.*"] - L2a["protocol-acceleration.*"] - L3["registrars.*"] - L4["subgraph.*"] - L5["tokenscope.*"] - end - - subgraph Custom["ensindexer_custom"] - C1["ensv2.*"] - C2["protocol-acceleration.*"] - C3["registrars.*"] - C4["subgraph.*"] - C5["tokenscope.*"] - end - end - - ENSNODE -->|tracks| Mainnet - ENSNODE -->|tracks| L2 - ENSNODE -->|tracks| Custom -``` - -Each [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance): -- Owns exactly one [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) with all five sub-schemas -- Has its own [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name) -- Can use a different [Schema Definition](/ensdb/concepts/glossary#schema-definition) version -- Has its own row in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) - -## Schema Versioning - -Each schema has a [Schema Version](/ensdb/concepts/glossary#schema-version) that changes when the [Schema Definition](/ensdb/concepts/glossary#schema-definition) changes. - -| Schema | Version Stored In | -|--------|------------------| -| [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) | Stored in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) as a metadata key | -| [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) | [ENSIndexer Schema Version](/ensdb/concepts/glossary#ensindexer-schema-version) (stored in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) `value` column) | - -[Schema Versions](/ensdb/concepts/glossary#schema-version) enable programmatic detection of schema compatibility. A consumer can verify that their code expects the correct schema structure before querying. - -See [Glossary: Schema Version](/ensdb/concepts/glossary#schema-version) for the concept definition. - -## Related Concepts - -- **[Glossary](/ensdb/concepts/glossary)** — All terminology definitions -- **[Architecture](/ensdb/concepts/architecture)** — How schemas relate and data flows -- **[Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle)** — How indexing affects database behavior diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/indexing-lifecycle.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/indexing-lifecycle.mdx deleted file mode 100644 index 1904099ecb..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/concepts/indexing-lifecycle.mdx +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: Indexing Lifecycle -description: How ENSIndexer processes data, from Backfill to Following, and how it affects database behavior. -sidebar: - label: Indexing Lifecycle - order: 5 ---- - -An [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) processes onchain data in distinct phases. Understanding the [Indexing Status](/ensdb/concepts/glossary#indexing-status) lifecycle helps explain database behavior changes, particularly around index creation and deletion. - -For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). - -## Indexing Status - -An [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) is always in one of two [Indexing Status](/ensdb/concepts/glossary#indexing-status) states: - -| Status | Description | -|--------|-------------| -| **backfill** | Processing historical events from genesis to current block | -| **following** | Caught up to chain tip, processing new events as they occur | - -The current status is tracked in the [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table). - -## Lifecycle Flow - -```mermaid -flowchart TD - Start["ENSIndexer Start"] --> Backfill - Backfill["Backfill
Process Historical Events"] -->|Caught up to chain tip| Following - Following["Following
Process New Events"] -->|Restart| Backfill -``` - -### Backfill Phase - -During backfill, ENSIndexer processes all historical events from the beginning of the chain: - -1. Scans blocks from genesis to current block -2. For each event matching the indexing filter: - - Reads cached RPC from Ponder Schema (or fetches and caches) - - Transforms data according to indexing logic - - Writes to ENSIndexer Schema -3. Updates indexing status in ENSNode Metadata - -**Database behavior during backfill:** - -- **[Indexes](/ensdb/concepts/glossary#database-objects) are dropped** on [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) tables -- Reason: Writing millions of rows with indexes is significantly slower -- Trade-off: Queries are slower during backfill, but backfill completes faster - -### Following Phase - -Once caught up to the chain tip, an [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) transitions to following: - -1. Listens for new blocks -2. For each new event matching the indexing filter: - - Reads cached RPC from [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) (or fetches and caches) - - Transforms data according to indexing logic - - Writes to [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) -3. Updates [Indexing Status](/ensdb/concepts/glossary#indexing-status) in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) - -**Database behavior during following:** - -- **[Indexes](/ensdb/concepts/glossary#database-objects) are created** on [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) tables -- Reason: Read queries should be fast for serving API requests -- Trade-off: Writes are slightly slower, but queries are fast - -## Index Management - -The transition between Backfill and Following triggers index management: - -```mermaid -flowchart LR - subgraph BackfillState["Status: Backfill"] - SchemaNoIndex1["ENSIndexer Schema
(no indexes)"] - end - - subgraph FollowingState["Status: Following"] - SchemaWithIndex1["ENSIndexer Schema
(has indexes)"] - end - - subgraph FollowingState2["Status: Following"] - SchemaWithIndex2["ENSIndexer Schema
(has indexes)"] - end - - subgraph BackfillState2["Status: Backfill"] - SchemaNoIndex2["ENSIndexer Schema
(no indexes)"] - end - - SchemaNoIndex1 -->|create indexes| SchemaWithIndex1 - SchemaWithIndex2 -->|drop indexes| SchemaNoIndex2 -``` - -### Why This Matters - -**For query consumers:** -- Queries during Backfill will be slower (no indexes) -- Consider waiting for Following status before running heavy queries -- Check [Indexing Status](/ensdb/concepts/glossary#indexing-status) via [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) - -```sql -SELECT value->>'status' as status -FROM ensnode.metadata -WHERE ens_indexer_schema_name = 'ensindexer_abc123' - AND key = 'ensindexer_indexing_status'; -``` - -**For database operators:** -- Backfill generates high write load -- Following generates moderate write load + read load -- Plan resource allocation accordingly - -## Restart Behavior - -When an ENSIndexer instance restarts: - -1. **If previously Following:** - - Drops indexes on [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) - - Enters Backfill to verify/repair any missed events - - Re-creates indexes when caught up to Following - -2. **Reasons for restart:** - - Configuration change - - Chain reorganization - - Software update - - Manual intervention - -The [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) RPC cache persists across restarts, reducing the cost of re-backfilling. - -## Querying During Lifecycle - -### Checking Status - -```sql -SELECT - ens_indexer_schema_name, - value->>'status' as status, - value->>'progress' as progress -FROM ensnode.metadata -WHERE key = 'ensindexer_indexing_status'; -``` - -### Safe Querying - -For production queries, verify the [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) is in Following [Indexing Status](/ensdb/concepts/glossary#indexing-status): - -```sql --- Only query if following -DO $$ -DECLARE - status text; -BEGIN - SELECT value->>'status' INTO status - FROM ensnode.metadata - WHERE ens_indexer_schema_name = 'ensindexer_abc123' - AND key = 'ensindexer_indexing_status'; - - IF status != 'following' THEN - RAISE EXCEPTION 'ENSIndexer not in following status'; - END IF; -END $$; - --- Now safe to run indexed queries -SELECT * FROM ensindexer_abc123.v1_domains WHERE ...; -``` - -## Related Concepts - -- **[Glossary](/ensdb/concepts/glossary)** — [Indexing Status](/ensdb/concepts/glossary#indexing-status), [ENSIndexer Instance](/ensdb/concepts/glossary#ensindexer-instance) definitions -- **[Database Schemas](/ensdb/concepts/database-schemas)** — [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) structure -- **[Architecture](/ensdb/concepts/architecture)** — Data flow through ENSDb diff --git a/docs/ensnode.io/src/content/docs/ensdb/integrations/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/integrations/index.mdx deleted file mode 100644 index b2accbabcf..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/integrations/index.mdx +++ /dev/null @@ -1,289 +0,0 @@ ---- -title: Building ENSDb Integrations -description: Build custom writers and readers that follow the ENSDb open standard. Create indexing solutions, APIs, analytics tools, and more in any programming language. -sidebar: - label: Overview - order: 6 ---- - -import { LinkCard, Aside, Card, CardGrid, Steps } from '@astrojs/starlight/components'; - -ENSDb is an **open standard** — anyone can build applications that write to or read from an ENSDb instance. This guide explains how to build custom integrations that follow the standard. - -## The Writer/Reader Pattern - -ENSDb follows a **bi-directional integration pattern**: - -```mermaid -flowchart TB - subgraph WriteSide["Write Side"] - Onchain["Onchain ENS State"] - Writer["Writer Implementation
(You Build This)"] - end - - subgraph ENSDb["ENSDb Standard
(PostgreSQL Database)"] - PS[(Ponder Schema)] - NS[(ENSNode Schema)] - IS[(ENSIndexer Schema)] - end - - subgraph ReadSide["Read Side"] - Reader["Reader Implementation
(You Build This)"] - Apps["Applications"] - end - - Onchain -->|Index| Writer - Writer -->|Write| ENSDb - ENSDb -->|Read| Reader - Reader -->|Serve| Apps -``` - -| Role | Responsibility | Example Implementations | -|------|----------------|------------------------| -| **Writer** | Indexes onchain ENS data and writes to ENSDb | ENSIndexer, Custom Indexers | -| **Reader** | Queries ENSDb and serves data to applications | ENSApi, Custom APIs, Dashboards | - - - -## Building a Writer - -A writer is responsible for: -1. Reading onchain ENS events -2. Transforming data according to ENSDb schema definitions -3. Writing to an ENSIndexer Schema -4. Updating ENSNode Schema metadata - - - -### When to Build a Writer - -- You need to index custom ENS-compatible contracts -- You want to index a new chain not covered by ENSIndexer -- You need specialized indexing logic for your use case -- You want to add custom tables to the ENSIndexer Schema - -## Building a Reader - -A reader is responsible for: -1. Querying ENSDb (ENSNode Schema for discovery, ENSIndexer Schema for data) -2. Transforming database results for your application -3. Serving data via your preferred interface (API, CLI, dashboard, etc.) - - - -### When to Build a Reader - -- You need a specialized API for your application -- You're building analytics or dashboard tools -- You're creating a CLI for ENS operations -- You need real-time streaming of ENS state changes - -## Schema Compliance - -To be ENSDb-compliant, your implementation must follow the standard schema definitions: - - - -Required metadata structure for tracking ENSIndexer instances - - -Modular schema with 5 sub-schemas: ensv2, protocol-acceleration, registrars, subgraph, tokenscope - - -Shared RPC cache structure (if using Ponder-based indexing) - - - -See [Database Schemas](/ensdb/concepts/database-schemas/) for complete schema documentation. - -## Language Support - -You can build ENSDb integrations in **any programming language** with a PostgreSQL driver: - -| Language | Popular Drivers | Use Cases | -|----------|-----------------|-----------| -| **TypeScript** | `pg`, `drizzle-orm`, `kysely` | APIs, dashboards, web apps | -| **Python** | `psycopg2`, `asyncpg`, `sqlalchemy` | Analytics, ML, data pipelines | -| **Go** | `pgx`, `database/sql`, `sqlx` | High-performance services, CLIs | -| **Rust** | `tokio-postgres`, `sqlx`, `diesel` | Systems programming, performance | -| **Java** | `JDBC`, `jOOQ`, `Hibernate` | Enterprise applications | -| **Ruby** | `pg`, `ActiveRecord` | Web applications | - -## SDKs and Tools - -### Official SDK - -The [ENSDb SDK](/ensdb/usage/ensdb-sdk/) (`@ensnode/ensdb-sdk`) provides: -- Schema definitions (Drizzle ORM) -- TypeScript types for all tables -- Reader/Writer client classes -- Schema validation utilities - -### Building Without the SDK - -You can build ENSDb integrations without the TypeScript SDK: - -1. **Study the schema** — Use [Database Schemas](/ensdb/concepts/database-schemas/) as reference -2. **Implement in your language** — Create equivalent schema definitions -3. **Follow the conventions** — Use the same table names, column types, and relationships -4. **Test for compatibility** — Verify your implementation works with existing readers/writers - -## Integration Architecture Patterns - -### Pattern 1: Co-located Writer and Reader - -Writer and reader run in the same process, sharing a database connection: - -```mermaid -flowchart TB - subgraph App["Your Application"] - direction LR - Writer["Writer
(Indexer)"] - Reader["Reader
(API)"] - end - - ENSDb["ENSDb
(SQLite)"] - - Writer --> ENSDb - Reader --> ENSDb -``` - -**Best for**: Local development, testing, embedded applications - -### Pattern 2: Separate Writer and Reader - -Writer and reader are separate processes connecting to a shared ENSDb: - -```mermaid -flowchart TB - Writer["Writer
(Indexer)"] - Reader["Reader
(API)"] - ENSDb["ENSDb
(PostgreSQL)"] - - Writer --> ENSDb - Reader --> ENSDb -``` - -**Best for**: Production deployments, microservices, scaled architectures - -### Pattern 3: Multiple Readers - -One writer, multiple specialized readers: - -```mermaid -flowchart TB - Writer["Writer
(Indexer)"] - ReaderGraphQL["Reader
(GraphQL)"] - ReaderAnalytics["Reader
(Analytics)"] - ReaderCLI["Reader
(CLI)"] - ENSDb["ENSDb"] - - Writer --> ReaderGraphQL - Writer --> ReaderAnalytics - Writer --> ReaderCLI - - ReaderGraphQL --> ENSDb - ReaderAnalytics --> ENSDb - ReaderCLI --> ENSDb -``` - -**Best for**: Multi-team organizations, diverse use cases - -### Pattern 4: Chain-Specific Writers - -Multiple writers indexing different chains, one shared ENSDb: - -```mermaid -flowchart TB - WriterMainnet["Writer
(Mainnet)"] - WriterL2["Writer
(L2 Chain)"] - ENSDb["ENSDb
(Shared)"] - Reader["Reader
(Multi-Chain API)"] - - WriterMainnet --> ENSDb - WriterL2 --> ENSDb - ENSDb --> Reader -``` - -**Best for**: Multi-chain ENS deployments, aggregated APIs - -## Compliance Checklist - -Before deploying your integration, verify: - -### For Writers - -- [ ] Creates ENSIndexer Schema with dynamic name -- [ ] Creates/updates ENSNode Schema metadata table -- [ ] Follows ENSIndexer Schema table definitions (all 5 sub-schemas) -- [ ] Updates indexing status in ENSNode metadata during operation -- [ ] Handles backfill vs following states appropriately (indexes) -- [ ] Supports schema versioning - -### For Readers - -- [ ] Queries ENSNode Schema for schema discovery -- [ ] Reads from ENSIndexer Schema for data queries -- [ ] Handles multiple ENSIndexer Schemas (multi-tenancy) -- [ ] Respects indexing status (warns if querying during backfill) -- [ ] Validates schema version compatibility - -## Next Steps - - -1. **Understand the schemas** - - Read [Database Schemas](/ensdb/concepts/database-schemas/) to understand the complete data model. - -2. **Choose your language** - - Pick a language with good PostgreSQL support for your use case. - -3. **Build a reader first** - - Start by querying an existing ENSDb to understand the data model. - -4. **Consider the SDK** - - For TypeScript, use `@ensnode/ensdb-sdk`. For other languages, port the schema definitions. - -5. **Test for compatibility** - - Ensure your implementation works with existing ENSDb tools. - - -## Getting Help - -- **[GitHub Discussions](https://github.com/namehash/ensnode/discussions)** — Ask questions about building integrations -- **[Discord](https://discord.gg/ensnode)** — Chat with the community -- **[Issue Tracker](https://github.com/namehash/ensnode/issues)** — Report bugs or request features - -## Related Documentation - - - - - - diff --git a/docs/ensnode.io/src/content/docs/ensdb/integrations/reader.mdx b/docs/ensnode.io/src/content/docs/ensdb/integrations/reader.mdx deleted file mode 100644 index 30d5579066..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/integrations/reader.mdx +++ /dev/null @@ -1,596 +0,0 @@ ---- -title: Build a Custom Reader -description: Build applications that query ENSDb to serve ENS data. Learn schema discovery, query patterns, and how to build APIs, dashboards, CLIs, and more. -sidebar: - label: Build a Reader - order: 3 ---- - -import { Aside, Tabs, TabItem, Card, CardGrid } from '@astrojs/starlight/components'; - -A **reader** queries ENSDb and serves data to applications. This guide explains how to build custom readers that follow the ENSDb open standard. - -## What a Reader Does - -```mermaid -flowchart LR - ENSDb["ENSDb Instance
(PostgreSQL)"] - Reader["Your Reader
(Custom App)"] - Apps["End Users"] - - ENSDb -->|1. Discover Schemas| Reader - ENSDb -->|2. Query Data| Reader - Reader -->|3. Transform| Reader - Reader -->|4. Serve| Apps -``` - -1. **Discover** — Query ENSNode Schema to find available ENSIndexer Schemas -2. **Query** — Read from ENSIndexer Schema tables -3. **Transform** — Format data for your specific use case -4. **Serve** — Deliver via API, dashboard, CLI, or other interface - -## Types of Readers - -You can build various types of readers depending on your needs: - - - -REST, GraphQL, or gRPC APIs that serve ENS data to web and mobile apps. - - -Real-time analytics dashboards with visualizations and metrics. - - -Command-line tools for ENS operations and scripting. - - -SDKs and client libraries that wrap ENSDb queries. - - -Real-time event processing and data pipelines. - - -Data feeds for machine learning and analytics models. - - - -## Architecture Overview - -Your reader will primarily interact with: - -```mermaid -erDiagram - READER["Your Reader"] ||--o{ ENSNODE_METADATA : "discovers via" - READER ||--o{ ENSINDEXER_SCHEMA : "reads from" - - ENSNODE_METADATA { - text ens_indexer_schema_name - text key - jsonb value - } - - ENSINDEXER_SCHEMA["ensindexer_* Schema"] { - v1_domains table - v2_domains table - registrations table - events table - ... - } -``` - -### Two-Phase Query Pattern - -All readers follow a two-phase pattern: - -```mermaid -sequenceDiagram - participant Reader - participant ENSNode - participant ENSIndexer - - Reader->>+ENSNode: SELECT schema_name FROM metadata - ENSNode-->>-Reader: ensindexer_mainnet, ensindexer_l2, ... - - Reader->>+ENSIndexer: SELECT * FROM v1_domains - ENSIndexer-->>-Reader: Domain data -``` - -## Implementation Guide - -### Step 1: Set Up Your Project - -Create a new project with PostgreSQL connectivity: - - - -```bash -mkdir my-ensdb-reader -cd my-ensdb-reader -npm init -y -npm install pg @ensnode/ensdb-sdk express -``` - - -```bash -mkdir my-ensdb-reader -cd my-ensdb-reader -python -m venv venv -source venv/bin/activate -pip install psycopg2-binary fastapi uvicorn -``` - - -```bash -mkdir my-ensdb-reader -cd my-ensdb-reader -go mod init my-ensdb-reader -go get github.com/jackc/pgx/v5 -go get github.com/gin-gonic/gin -``` - - -```bash -mkdir my-ensdb-reader -cd my-ensdb-reader -cargo init -# Add tokio-postgres and axum to Cargo.toml -``` - - - -### Step 2: Connect to PostgreSQL - - - -```typescript -import { Pool } from 'pg'; - -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, -}); - -// Or use ENSDb SDK -import { EnsDbReader } from '@ensnode/ensdb-sdk'; - -const reader = new EnsDbReader( - process.env.DATABASE_URL!, - 'ensindexer_mainnet' // Schema to query -); -``` - - -```python -import psycopg2 -from psycopg2.extras import RealDictCursor - -conn = psycopg2.connect( - host="localhost", - database="ensdb", - user="postgres", - password="password" -) -``` - - -```go -import ( - "context" - "github.com/jackc/pgx/v5/pgxpool" -) - -pool, err := pgxpool.New(context.Background(), "postgresql://user:pass@localhost/ensdb") -if err != nil { - log.Fatal(err) -} -defer pool.Close() -``` - - -```rust -use tokio_postgres::Client; - -let (client, connection) = tokio_postgres::connect( - "host=localhost user=postgres dbname=ensdb", - tokio_postgres::NoTls, -).await?; -``` - - - -### Step 3: Discover Available Schemas - -Query ENSNode Schema to find available ENSIndexer instances: - - - -```typescript -// Using plain SQL -const result = await pool.query(` - SELECT DISTINCT ens_indexer_schema_name - FROM ensnode.metadata - ORDER BY ens_indexer_schema_name -`); - -const schemas = result.rows.map(r => r.ens_indexer_schema_name); -console.log('Available schemas:', schemas); -// ['ensindexer_mainnet', 'ensindexer_l2', 'ensindexer_custom'] -``` - - -```python -cursor = conn.cursor(cursor_factory=RealDictCursor) -cursor.execute(""" - SELECT DISTINCT ens_indexer_schema_name - FROM ensnode.metadata - ORDER BY ens_indexer_schema_name -""") -schemas = [row['ens_indexer_schema_name'] for row in cursor.fetchall()] -print(f"Available schemas: {schemas}") -``` - - -```go -rows, err := pool.Query(context.Background(), ` - SELECT DISTINCT ens_indexer_schema_name - FROM ensnode.metadata - ORDER BY ens_indexer_schema_name -`) -if err != nil { - log.Fatal(err) -} -defer rows.Close() - -var schemas []string -for rows.Next() { - var schema string - rows.Scan(&schema) - schemas = append(schemas, schema) -} -``` - - -```rust -let rows = client.query( - "SELECT DISTINCT ens_indexer_schema_name FROM ensnode.metadata ORDER BY ens_indexer_schema_name", - &[], -).await?; - -let schemas: Vec = rows.iter() - .map(|row| row.get(0)) - .collect(); -``` - - - -### Step 4: Check Indexing Status - -Before querying, verify the indexer is in a good state: - - - -```typescript -const statusResult = await pool.query(` - SELECT value->>'status' as status, - value->>'progress' as progress - FROM ensnode.metadata - WHERE ens_indexer_schema_name = $1 - AND key = 'ensindexer_indexing_status' -`, ['ensindexer_mainnet']); - -const status = statusResult.rows[0]; -if (status.status !== 'following') { - console.warn(`Warning: Indexer is in ${status.status} state (${status.progress}% complete)`); - console.warn('Query performance may be degraded'); -} -``` - - - - - -### Step 5: Query Domain Data - -Fetch domains from an ENSIndexer Schema: - - - -```typescript -// Query v1 domains -const domains = await pool.query(` - SELECT id, parent_id, owner_id, label_hash - FROM ensindexer_mainnet.v1_domains - WHERE owner_id = $1 - LIMIT 100 -`, ['\\x1234567890abcdef1234567890abcdef12345678']); - -// Query v2 domains with registry info -const v2Domains = await pool.query(` - SELECT d.id, d.token_id, d.owner_id, r.chain_id - FROM ensindexer_mainnet.v2_domains d - JOIN ensindexer_mainnet.registries r ON d.registry_id = r.id - WHERE d.owner_id = $1 -`, ['\\x1234567890abcdef1234567890abcdef12345678']); -``` - - -```python -# Query domains with label resolution -cursor.execute(""" - SELECT d.id, d.owner_id, l.interpreted as label - FROM ensindexer_mainnet.v1_domains d - LEFT JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash - WHERE d.owner_id = %s - LIMIT 100 -"", ('\\x1234567890abcdef1234567890abcdef12345678',)) - -domains = cursor.fetchall() -for domain in domains: - print(f"Domain: {domain['id']}, Label: {domain['label']}") -``` - - -```go -rows, err := pool.Query(context.Background(), ` - SELECT id, parent_id, owner_id, label_hash - FROM ensindexer_mainnet.v1_domains - WHERE owner_id = $1 - LIMIT 100 -`, ownerAddress) -if err != nil { - log.Fatal(err) -} -defer rows.Close() - -for rows.Next() { - var domain struct { - ID string - ParentID string - OwnerID string - LabelHash []byte - } - rows.Scan(&domain.ID, &domain.ParentID, &domain.OwnerID, &domain.LabelHash) - // Process domain... -} -``` - - - -### Step 6: Build Your Interface - -Wrap your queries in an API, CLI, or other interface: - - - -```typescript -import express from 'express'; - -const app = express(); - -// Get domains by owner -app.get('/api/domains/:owner', async (req, res) => { - const owner = req.params.owner; - - const result = await pool.query(` - SELECT d.id, d.parent_id, l.interpreted as name - FROM ensindexer_mainnet.v1_domains d - LEFT JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash - WHERE d.owner_id = $1 - `, ['\\x' + owner.replace('0x', '')]); - - res.json({ - owner, - domains: result.rows - }); -}); - -// Get registration info -app.get('/api/domains/:domainId/registration', async (req, res) => { - const domainId = req.params.domainId; - - const result = await pool.query(` - SELECT r.*, ra.timestamp - FROM ensindexer_mainnet.registrations r - JOIN ensindexer_mainnet.registrar_actions ra ON r.event_id = ra.event_ids[1] - WHERE r.domain_id = $1 - ORDER BY r.registration_index DESC - LIMIT 1 - `, [domainId]); - - res.json(result.rows[0]); -}); - -app.listen(3000, () => { - console.log('ENS API listening on port 3000'); -}); -``` - - -```python -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - -class Domain(BaseModel): - id: str - name: str | None - owner_id: str - -@app.get("/api/domains/{owner}") -async def get_domains(owner: str): - cursor = conn.cursor(cursor_factory=RealDictCursor) - cursor.execute(""" - SELECT d.id, l.interpreted as name, d.owner_id - FROM ensindexer_mainnet.v1_domains d - LEFT JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash - WHERE d.owner_id = %s - """, (owner,)) - - domains = cursor.fetchall() - return {"owner": owner, "domains": domains} - -@app.get("/api/domains/{domain_id}/registration") -async def get_registration(domain_id: str): - cursor = conn.cursor(cursor_factory=RealDictCursor) - cursor.execute(""" - SELECT r.* - FROM ensindexer_mainnet.registrations r - WHERE r.domain_id = %s - ORDER BY r.registration_index DESC - LIMIT 1 - """, (domain_id,)) - - return cursor.fetchone() -``` - - -```python -import click -import psycopg2 -from psycopg2.extras import RealDictCursor - -@click.group() -def cli(): - """ENSDb CLI - Query ENS data from the command line""" - pass - -@cli.command() -@click.argument('owner') -def domains(owner): - """Get domains owned by an address""" - conn = psycopg2.connect(database='ensdb') - cursor = conn.cursor(cursor_factory=RealDictCursor) - - cursor.execute(""" - SELECT d.id, l.interpreted as label - FROM ensindexer_mainnet.v1_domains d - LEFT JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash - WHERE d.owner_id = %s - """, (owner,)) - - for domain in cursor.fetchall(): - click.echo(f"{domain['id']}: {domain['label']}") - -@cli.command() -@click.argument('domain_id') -def registration(domain_id): - """Get registration info for a domain""" - conn = psycopg2.connect(database='ensdb') - cursor = conn.cursor(cursor_factory=RealDictCursor) - - cursor.execute(""" - SELECT * FROM ensindexer_mainnet.registrations - WHERE domain_id = %s - ORDER BY registration_index DESC - LIMIT 1 - """, (domain_id,)) - - reg = cursor.fetchone() - click.echo(f"Registration: {reg}") - -if __name__ == '__main__': - cli() -``` - - - -## Query Patterns - -### Multi-Schema Queries - -Query across multiple ENSIndexer Schemas: - -```sql --- Union domains from multiple chains -SELECT 'mainnet' as chain, id, owner_id -FROM ensindexer_mainnet.v1_domains -WHERE owner_id = '\x1234...' - -UNION ALL - -SELECT 'base' as chain, id, owner_id -FROM ensindexer_base.v1_domains -WHERE owner_id = '\x1234...'; -``` - -### Name Resolution - -Resolve a name to its records: - -```sql --- Find domain by label -SELECT d.id, d.owner_id -FROM ensindexer_mainnet.v1_domains d -JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash -WHERE l.interpreted = 'vitalik'; - --- Get resolver records for a domain -SELECT rr.name, rar.coin_type, rar.value as address -FROM ensindexer_mainnet.resolver_records rr -LEFT JOIN ensindexer_mainnet.resolver_address_records rar - ON rr.chain_id = rar.chain_id - AND rr.address = rar.address - AND rr.node = rar.node -WHERE rr.node = '\x1234...'; -``` - -### Event History - -Get event history for a domain: - -```sql -SELECT e.*, de.domain_id -FROM ensindexer_mainnet.events e -JOIN ensindexer_mainnet.domain_events de ON e.id = de.event_id -WHERE de.domain_id = '\x1234...' -ORDER BY e.block_number DESC, e.log_index DESC -LIMIT 100; -``` - -## Best Practices - -### Query Performance - -1. **Check indexing status** before heavy queries -2. **Use LIMIT** for exploratory queries -3. **Filter early** with WHERE clauses on indexed columns -4. **Use EXPLAIN ANALYZE** to debug slow queries - -### Connection Management - -- Use connection pooling for production workloads -- Set appropriate pool sizes (typically 10-50 connections) -- Handle connection failures gracefully - -### Error Handling - -- Handle missing schemas gracefully (discover before querying) -- Handle missing tables (check schema version) -- Handle slow queries during backfill - -## Multi-Language Examples - -See the [Usage Guides](/ensdb/usage/) for complete examples in: -- TypeScript (with and without SDK) -- Python -- Go -- Rust - -## Testing Your Reader - -Verify your reader works correctly: - -1. **Schema discovery** — Finds all available ENSIndexer Schemas -2. **Status checking** — Handles backfill vs following states -3. **Data queries** — Returns correct domain, registration, and event data -4. **Error handling** — Gracefully handles missing data -5. **Performance** — Queries complete in acceptable time - -## Related Documentation - -- **[Database Schemas](/ensdb/concepts/database-schemas/)** — Complete table reference -- **[Querying Guide](/ensdb/usage/querying/)** — SQL patterns and examples -- **[ENSDb SDK](/ensdb/usage/ensdb-sdk/)** — TypeScript SDK reference -- **[Use Cases](/ensdb/use-cases/)** — Real-world reader examples diff --git a/docs/ensnode.io/src/content/docs/ensdb/integrations/writer.mdx b/docs/ensnode.io/src/content/docs/ensdb/integrations/writer.mdx deleted file mode 100644 index 902ee3886a..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/integrations/writer.mdx +++ /dev/null @@ -1,452 +0,0 @@ ---- -title: Build a Custom Writer -description: Build an indexer that writes ENS data to ENSDb following the open standard. Learn the ENSNode Schema requirements, ENSIndexer Schema structure, and indexing lifecycle. -sidebar: - label: Build a Writer - order: 2 ---- - -import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; - -A **writer** indexes onchain ENS data and writes it to an ENSDb instance. This guide explains how to build a custom writer that follows the ENSDb open standard. - -## What a Writer Does - -```mermaid -flowchart LR - Onchain["Onchain ENS State
(Ethereum)"] - Writer["Your Writer
(Custom Indexer)"] - ENSDb["ENSDb Instance
(PostgreSQL)"] - - Onchain -->|1. Read Events| Writer - Writer -->|2. Transform| Writer - Writer -->|3. Write Data| ENSDb - Writer -->|4. Update Metadata| ENSDb -``` - -1. **Read** — Connect to an Ethereum node and subscribe to ENS-related events -2. **Transform** — Convert onchain data to ENSDb schema format -3. **Write** — Insert data into ENSIndexer Schema tables -4. **Metadata** — Update ENSNode Schema with indexing status and configuration - -## Architecture Overview - -Your writer will interact with two schemas: - -```mermaid -erDiagram - WRITER["Your Writer"] ||--o{ ENSNODE_METADATA : "updates" - WRITER ||--|| ENSINDEXER_SCHEMA : "creates & writes" - - ENSNODE_METADATA { - text ens_indexer_schema_name PK - text key PK - text value_version - jsonb value - } - - ENSINDEXER_SCHEMA["ensindexer_* Schema"] { - text schema_name - timestamp created_at - } -``` - -### ENSNode Schema Interactions - -Your writer must: -- **Create** the ENSNode Schema on first run (if it doesn't exist) -- **Register** itself in the `metadata` table with: - - `ensdb_version`: Your schema version - - `ensindexer_public_config`: Your public configuration - - `ensindexer_indexing_status`: Current indexing state - -### ENSIndexer Schema Interactions - -Your writer must: -- **Create** a schema with a unique name (e.g., `ensindexer_mycustom`) -- **Create** all tables defined in the 5 sub-schemas -- **Maintain** indexes appropriately (dropped during backfill, created during following) - -## Implementation Guide - -### Step 1: Set Up Your Project - -Create a new project with PostgreSQL connectivity: - - - -```bash -mkdir my-ensdb-writer -cd my-ensdb-writer -npm init -y -npm install pg @ensnode/ensdb-sdk viem -``` - - -```bash -mkdir my-ensdb-writer -cd my-ensdb-writer -python -m venv venv -source venv/bin/activate -pip install psycopg2-binary web3 -``` - - -```bash -mkdir my-ensdb-writer -cd my-ensdb-writer -go mod init my-ensdb-writer -go get github.com/jackc/pgx/v5 -go get github.com/ethereum/go-ethereum -``` - - - -### Step 2: Connect to PostgreSQL - - - -```typescript -import { Pool } from 'pg'; - -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, -}); - -// Or use ENSDb SDK -import { EnsDbWriter } from '@ensnode/ensdb-sdk'; - -const writer = new EnsDbWriter( - process.env.DATABASE_URL!, - 'ensindexer_mycustom' // Your schema name -); -``` - - -```python -import psycopg2 -from psycopg2.extras import RealDictCursor - -conn = psycopg2.connect( - host="localhost", - database="ensdb", - user="postgres", - password="password" -) -``` - - -```go -import ( - "context" - "github.com/jackc/pgx/v5/pgxpool" -) - -pool, err := pgxpool.New(context.Background(), "postgresql://user:pass@localhost/ensdb") -if err != nil { - log.Fatal(err) -} -defer pool.Close() -``` - - - -### Step 3: Initialize ENSNode Schema - -Create the ENSNode Schema and migrations table if they don't exist: - - - -```typescript -// Using SDK - handles migrations automatically -await writer.migrateEnsNodeSchema('./migrations/ensnode'); -``` - - -```sql --- Create ENSNode schema -CREATE SCHEMA IF NOT EXISTS ensnode; - --- Create metadata table -CREATE TABLE IF NOT EXISTS ensnode.metadata ( - ens_indexer_schema_name TEXT NOT NULL, - key TEXT NOT NULL, - value_version TEXT NOT NULL, - value JSONB NOT NULL, - PRIMARY KEY (ens_indexer_schema_name, key) -); -``` - - - -### Step 4: Create ENSIndexer Schema - -Create your dynamic schema with all required tables: - -```sql --- Create your schema -CREATE SCHEMA IF NOT EXISTS ensindexer_mycustom; - --- Create tables from all 5 sub-schemas (ensv2, protocol-acceleration, registrars, subgraph, tokenscope) --- See Database Schemas reference for complete DDL: /ensdb/concepts/database-schemas/ -``` - - - -### Step 5: Register Your Writer - -Insert metadata about your indexer: - -```sql --- Register schema version -INSERT INTO ensnode.metadata (ens_indexer_schema_name, key, value_version, value) -VALUES ( - 'ensindexer_mycustom', - 'ensdb_version', - '1.0.0', - '"1.0.0"'::jsonb -); - --- Register public configuration -INSERT INTO ensnode.metadata (ens_indexer_schema_name, key, value_version, value) -VALUES ( - 'ensindexer_mycustom', - 'ensindexer_public_config', - '1.0.0', - '{ - "chains": ["mainnet"], - "plugins": ["ensv2"], - "version": "1.0.0" - }'::jsonb -); - --- Register initial indexing status -INSERT INTO ensnode.metadata (ens_indexer_schema_name, key, value_version, value) -VALUES ( - 'ensindexer_mycustom', - 'ensindexer_indexing_status', - '1.0.0', - '{ - "status": "backfill", - "progress": 0, - "chains": {} - }'::jsonb -); -``` - -### Step 6: Implement Indexing Logic - -Connect to an Ethereum node and process events: - - - -```typescript -import { createPublicClient, http, parseAbi } from 'viem'; -import { mainnet } from 'viem/chains'; - -const client = createPublicClient({ - chain: mainnet, - transport: http(process.env.ETHEREUM_RPC_URL), -}); - -// Example: Index ENS Registry events -const ensRegistryAbi = parseAbi([ - 'event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner)', -]); - -// Get historical logs -const logs = await client.getLogs({ - address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', - event: ensRegistryAbi[0], - fromBlock: 0n, - toBlock: 'latest', -}); - -// Transform and insert -for (const log of logs) { - const node = log.args.node; - const label = log.args.label; - const owner = log.args.owner; - - // Transform to ENSDb format - const domainId = `${node}`; - const labelHash = `${label}`; - - // Insert into your schema - await pool.query(` - INSERT INTO ensindexer_mycustom.v1_domains (id, parent_id, owner_id, label_hash) - VALUES ($1, $2, $3, $4) - ON CONFLICT (id) DO UPDATE SET - owner_id = EXCLUDED.owner_id - `, [domainId, node, owner, labelHash]); -} -``` - - - -### Step 7: Update Indexing Status - -Periodically update the metadata with progress: - -```sql -UPDATE ensnode.metadata -SET value = '{ - "status": "backfill", - "progress": 45, - "chains": { - "1": { - "latestIndexedBlock": 18500000, - "targetBlock": 21000000 - } - } -}'::jsonb -WHERE ens_indexer_schema_name = 'ensindexer_mycustom' - AND key = 'ensindexer_indexing_status'; -``` - -When caught up to chain head: - -```sql -UPDATE ensnode.metadata -SET value = '{ - "status": "following", - "progress": 100, - "chains": { - "1": { - "latestIndexedBlock": 21000000 - } - } -}'::jsonb -WHERE ens_indexer_schema_name = 'ensindexer_mycustom' - AND key = 'ensindexer_indexing_status'; -``` - -### Step 8: Handle Index Management - -Drop indexes during backfill for performance: - -```sql --- During backfill -DROP INDEX IF EXISTS ensindexer_mycustom.v1_domains_by_owner; -``` - -Create indexes when following for query performance: - -```sql --- When following -CREATE INDEX v1_domains_by_owner -ON ensindexer_mycustom.v1_domains(owner_id); -``` - -## Schema Versioning - -Your writer must track schema versions: - -1. **Compute a checksum** of your schema definition -2. **Store it** in ENSNode metadata -3. **Validate** on startup that the database matches expected version - - - -```typescript -import { getDrizzleSchemaChecksum } from '@ensnode/ensdb-sdk'; -import * as schema from '@ensnode/ensdb-sdk/ensindexer-abstract'; - -const checksum = getDrizzleSchemaChecksum(schema); -// Store in metadata -await writer.upsertEnsDbVersion(checksum); -``` - - - -## Best Practices - -### Error Handling - -- Use transactions for multi-table writes -- Implement idempotent inserts (ON CONFLICT) -- Log errors but continue indexing when possible - -### Performance - -- Batch inserts when possible (100-1000 rows per batch) -- Drop indexes during backfill -- Use prepared statements for repeated queries - -### State Management - -- Persist last indexed block to survive restarts -- Handle chain reorganizations by rewinding and re-indexing -- Update indexing status frequently enough for monitoring - -## Complete Example - -Here's a minimal but complete writer example in TypeScript: - -```typescript -import { EnsDbWriter } from '@ensnode/ensdb-sdk'; -import { createPublicClient, http } from 'viem'; -import { mainnet } from 'viem/chains'; - -class CustomEnsDbWriter { - private writer: EnsDbWriter; - private client: ReturnType; - - constructor(ensDbUrl: string, schemaName: string, rpcUrl: string) { - this.writer = new EnsDbWriter(ensDbUrl, schemaName); - this.client = createPublicClient({ - chain: mainnet, - transport: http(rpcUrl), - }); - } - - async initialize(): Promise { - // Run ENSNode Schema migrations - await this.writer.migrateEnsNodeSchema('./migrations'); - - // Register configuration - await this.writer.upsertEnsIndexerPublicConfig({ - chains: ['mainnet'], - plugins: ['custom'], - }); - - // Set initial status - await this.writer.upsertIndexingStatusSnapshot({ - status: 'backfill', - progress: 0, - }); - } - - async run(): Promise { - // Start indexing... - // (Implementation depends on your specific requirements) - } -} - -// Usage -const writer = new CustomEnsDbWriter( - 'postgresql://localhost/ensdb', - 'ensindexer_mycustom', - 'https://mainnet.example.com' -); - -await writer.initialize(); -await writer.run(); -``` - -## Testing Your Writer - -Verify your writer follows the standard: - -1. **Schema creation** — ENSIndexer Schema exists with all tables -2. **Metadata registration** — ENSNode metadata has your entries -3. **Data insertion** — Data appears in correct tables -4. **Reader compatibility** — Existing readers can query your data - -## Related Documentation - -- **[Database Schemas](/ensdb/concepts/database-schemas/)** — Complete schema reference -- **[Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle/)** — How indexing phases work -- **[ENSIndexer Contributing](/ensindexer/contributing/)** — Reference implementation diff --git a/docs/ensnode.io/src/content/docs/ensdb/operations/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/operations/index.mdx deleted file mode 100644 index c3bb0cfce2..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/operations/index.mdx +++ /dev/null @@ -1,324 +0,0 @@ ---- -title: ENSNode Operations -description: Operational guidance for ENSNode operators, including multitenancy strategies, ENS Namespace isolation, cost optimization, and backup/restore procedures. -sidebar: - label: Operations - order: 7 ---- - -import { Aside, Card, CardGrid } from '@astrojs/starlight/components'; - -This guide covers operational considerations for running ENSNode in production, with a focus on cost optimization, resource isolation, and efficient deployment strategies. - -## Multitenancy and Resource Sharing - -An ENSDb instance is **multi-tenant** by design — it can store data from multiple [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) (tenants) that share common infrastructure while maintaining data isolation. - -### What Tenants Share - -| Resource | Sharing Model | Purpose | -|----------|---------------|---------| -| Ponder Schema (`ponder_sync`) | Shared across all tenants | RPC cache to reduce redundant blockchain calls | -| ENSNode Schema (`ensnode`) | Shared across all tenants | Metadata tracking for all connected indexers | -| PostgreSQL Server Resources | Shared (CPU, memory, I/O) | Database engine and connection handling | - -### What Tenants Own - -Each tenant (ENSIndexer instance) has: -- **Dedicated ENSIndexer Schema** — fully isolated data namespace (e.g., `ensindexer_mainnet`, `ensindexer_base`) -- **Independent indexing lifecycle** — can start, stop, or restart without affecting other tenants -- **Own configuration and status** — tracked separately in the ENSNode Metadata Table - - - -## ENS Namespace Isolation - -The most critical operational decision is how to handle [ENS Namespaces](/ensindexer/usage/ens-namespaces). An ENS Namespace (like "mainnet" or "sepolia") defines which chains an ENSIndexer instance indexes. - -### The Problem: Mixed Namespaces - -When a single ENSDb instance hosts ENSIndexer instances for **different namespaces** (e.g., some indexing "mainnet", others indexing "sepolia"): - -1. **The Ponder Schema caches RPC data for ALL indexed chains** -2. **Mainnet data dominates the cache** — due to significantly more blocks, events, and contract state -3. **Testnet data becomes a tiny fraction** — even if you're primarily interested in testnets - -```mermaid -flowchart TB - subgraph Mixed["Mixed ENSDb Instance
(mainnet + sepolia tenants)"] - direction TB - Ponder["Ponder Schema (ponder_sync)"] - MainnetCache["99%+ mainnet RPC cache"] - SepoliaCache["<1% sepolia RPC cache"] - MainnetIndexer["ENSIndexer Schema: ensindexer_mainnet"] - SepoliaIndexer["ENSIndexer Schema: ensindexer_sepolia"] - - Ponder --> MainnetCache - Ponder --> SepoliaCache - end - - style MainnetCache fill:#ffcccc - style SepoliaCache fill:#ccffcc -``` - -### The Solution: Namespace-Per-Instance - -For lean, cost-effective testnet operations, use **separate ENSDb instances** per ENS Namespace: - -| ENSDb Instance | ENS Namespace | Tenants | Ponder Schema Contents | -|---------------|---------------|---------|----------------------| -| `ensdb_mainnet` | mainnet | Mainnet indexers | Mainnet chains only | -| `ensdb_sepolia` | sepolia | Sepolia indexers | Testnet chains only | - - - -## Cost Optimization Strategies - -### 1. Dedicated Testnet Instances - -If you primarily need testnet data, deploy a dedicated `ensdb_sepolia` instance: - -```bash -# Separate PostgreSQL databases -psql postgresql://localhost:5432/ensdb_mainnet # Production -psql postgresql://localhost:5432/ensdb_sepolia # Testing/development -``` - -**Benefits:** -- **Smaller snapshots** — tens of GB instead of hundreds -- **Faster restore** — minutes instead of hours -- **Lower storage costs** — no mainnet bloat -- **Reusable RPC cache** — when you restore, the testnet cache is already primed - -### 2. Snapshot Strategy - -Take **namespace-specific ENSDb snapshots** for efficient backup and restore: - -| Snapshot Type | Contents | Use Case | -|--------------|----------|----------| -| `ensdb_mainnet_full` | Complete mainnet ENSDb | Production deployments | -| `ensdb_sepolia_full` | Complete sepolia ENSDb | Development, CI/CD, testing | - - - -### 3. Development Environment - -For local development or CI pipelines: - -1. Download the latest `ensdb_sepolia` snapshot -2. Import into a local PostgreSQL instance -3. Connect ENSApi to query testnet data - -```bash -# Download snapshot (URL is illustrative — service not yet available) -# curl -O https://snapshots.ensnode.io/ensdb_sepolia_latest.sql.gz - -# Restore to local database -# gunzip < ensdb_sepolia_latest.sql.gz | psql postgresql://localhost:5432/ensdb_sepolia - -# Start ENSApi pointing to the restored ENSDb -# DATABASE_URL=postgresql://localhost:5432/ensdb_sepolia pnpm start -``` - -## Deployment Patterns - -### Pattern 1: Single-Instance Multi-Tenant (Production) - -One ENSDb instance hosting multiple mainnet chain indexers: - -```mermaid -flowchart TB - subgraph Pattern1["ensdb_mainnet"] - direction TB - Mainnet["ensindexer_mainnet
(Ethereum mainnet ENS)"] - Base["ensindexer_base
(Base L2 ENS)"] - Linea["ensindexer_linea
(Linea L2 ENS)"] - DNS["ensindexer_3dns
(3DNS on mainnet)"] - end -``` - -**When to use:** Production environments indexing multiple chains within the same ENS Namespace. - -### Pattern 2: Namespace-Isolated Instances (Recommended) - -Separate ENSDb instances per ENS Namespace: - -```mermaid -flowchart TB - subgraph PGServer["PostgreSQL Server"] - direction TB - subgraph MainnetDB["ensdb_mainnet
(mainnet namespace only)"] - Mainnet["ensindexer_mainnet"] - Base["ensindexer_base"] - Linea["ensindexer_linea"] - end - - subgraph SepoliaDB["ensdb_sepolia
(sepolia namespace only)"] - Sepolia["ensindexer_sepolia"] - BaseSepolia["ensindexer_base_sepolia"] - end - end -``` - -**When to use:** When you need efficient testnet deployments or want precise control over snapshot boundaries. - -### Pattern 3: Environment-Based Isolation - -Separate ENSDb instances per deployment environment: - -```mermaid -flowchart TB - subgraph PGServer["PostgreSQL Server"] - direction TB - Prod["ensdb_production
(mainnet, full history)"] - Staging["ensdb_staging
(mainnet, recent history only)"] - Dev["ensdb_development
(sepolia, for testing)"] - end -``` - -**When to use:** When different environments have different data freshness requirements. - -## Backup and Restore Procedures - -### Taking a Snapshot - -A complete ENSDb snapshot includes all schemas: - -```bash -# Full database dump (all schemas) -pg_dump -Fc postgresql://host:5432/ensdb_mainnet > ensdb_mainnet_$(date +%Y%m%d).dump - -# Verify size -ls -lh ensdb_mainnet_*.dump -``` - -### Restoring from Snapshot - -```bash -# Create fresh database -createdb postgresql://host:5432/ensdb_restored - -# Restore from dump -pg_restore -d postgresql://host:5432/ensdb_restored ensdb_mainnet_20240115.dump - -# Verify tenants are present -psql postgresql://host:5432/ensdb_restored -c "SELECT DISTINCT ens_indexer_schema_name FROM ensnode.metadata;" -``` - -### Selective Restore (Advanced) - -If you only need specific tenants from a snapshot, you can restore specific schemas: - -```bash -# Restore only specific ENSIndexer Schema and required system schemas -pg_restore \ - --schema=ponder_sync \ - --schema=ensnode \ - --schema=ensindexer_base \ - -d postgresql://host:5432/ensdb_base_only \ - ensdb_mainnet_20240115.dump -``` - - - -## Monitoring and Alerts - -### Key Metrics - -| Metric | Source | Alert Threshold | -|--------|--------|-----------------| -| Ponder Schema size | `pg_total_relation_size('ponder_sync.*')` | > 80% of disk | -| ENSIndexer lag | `ensnode.metadata` ensindexer_indexing_status | > 100 blocks behind | -| Active tenants | `COUNT(DISTINCT ens_indexer_schema_name)` | Unexpected drop | -| Disk utilization | PostgreSQL system stats | > 85% | - -### Health Check Query - -```sql --- Check all tenant statuses in an ENSDb instance -SELECT - ens_indexer_schema_name, - value->>'status' as status, - value->>'lastSyncedBlock' as last_block, - value->>'chainId' as chain_id -FROM ensnode.metadata -WHERE key = 'ensindexer_indexing_status'; -``` - -## Troubleshooting - -### Issue: ENSDb Instance Growing Too Large - -**Symptoms:** Disk usage increasing rapidly, slow queries, backup failures. - -**Diagnosis:** -```sql --- Check Ponder Schema size by table -SELECT - schemaname, - tablename, - pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size -FROM pg_tables -WHERE schemaname = 'ponder_sync' -ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC; -``` - -**Solutions:** -1. **Separate namespaces** — Move testnet indexers to a dedicated `ensdb_sepolia` instance -2. **Verify tenant configurations** — Ensure all tenants are intentionally included -3. **Consider schema-specific restore** — If some tenants are no longer needed - -### Issue: High RPC Costs Despite Cache - -**Symptoms:** RPC usage higher than expected, cache hit rate low. - -**Possible causes:** -- Mixed namespaces diluting cache effectiveness -- Too many ENSDb instances with isolated Ponder Schemas (no sharing) -- Infrequent queries causing cache eviction - -### Issue: Slow Indexer Restart - -**Symptoms:** Indexer takes hours to resume after restart. - -**Diagnosis:** -```sql --- Check if Ponder Schema has required cache entries -SELECT COUNT(*) FROM ponder_sync.blocks WHERE chain_id = 1; -``` - -**Solution:** Ensure you're restoring from a snapshot with a primed Ponder Schema, not starting from scratch. - -## Best Practices Summary - -1. **Isolate by ENS Namespace** — Separate `ensdb_mainnet` and `ensdb_sepolia` instances -2. **Share within Namespace** — Use multitenancy for multiple chains in the same namespace -3. **Snapshot strategically** — Take namespace-specific snapshots for efficient restore -4. **Monitor Ponder Schema size** — It's your primary indicator of resource usage -5. **Document tenant configurations** — Know which ENSIndexer instances are writing to each ENSDb instance - -## Related Documentation - - - -Deep dive into Ponder Schema, ENSNode Schema, and ENSIndexer Schemas - - -How multitenancy works and how schemas relate - - -How ENSIndexer uses namespaces to determine which chains to index - - diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/index.mdx deleted file mode 100644 index ba74a4fa36..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/index.mdx +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: ENSDb SDK -description: TypeScript utilities for type-safe ENSDb access. -sidebar: - label: ENSDb SDK - order: 2 ---- - -import { LinkCard } from '@astrojs/starlight/components'; - -The [ENSDb SDK](/ensdb/concepts/glossary#ensdb-sdk) (`@ensnode/ensdb-sdk`) provides TypeScript utilities for working with ENSDb. While ENSDb is a standard PostgreSQL database usable from any language with a PostgreSQL driver, the SDK offers type-safe access and convenience methods for TypeScript. - -For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). - -:::note -You don't need the SDK to use ENSDb. See [Querying Guide](/ensdb/usage/querying) for language-agnostic SQL examples. -::: - -## Installation - -```bash -npm install @ensnode/ensdb-sdk -``` - -## Package Structure - -The SDK exports: - -| Export | Purpose | -|--------|---------| -| `EnsDbReader` | Read-only queries for ENSDb | -| `EnsDbWriter` | Write interface for metadata and migrations | -| [Schema Definitions](/ensdb/concepts/glossary#schema-definition) | Drizzle schemas for ENSNode and ENSIndexer | -| `getDrizzleSchemaChecksum` | Compute [Schema Checksum](/ensdb/concepts/glossary#schema-checksum) for schema definitions | - -## Schema Definitions - -The SDK exports Drizzle [Schema Definitions](/ensdb/concepts/glossary#schema-definition) for ENSDb schemas: - -### Abstract ENSIndexer Schema - -```typescript -import * as abstractEnsIndexerSchema from '@ensnode/ensdb-sdk/ensindexer-abstract'; - -// Contains table definitions for: -// - v1_domains, v2_domains -// - labels, accounts -// - registrations, renewals -// - events, domain_events, resolver_events -// - permissions tables -// ... and more -``` - -:::note -The abstract ENSIndexer Schema is called "abstract" because tables don't reference a specific [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name). Use `EnsDbReader` or `EnsDbWriter` to get a "concrete" schema bound to a specific name. -::: - -### ENSNode Schema - -```typescript -import * as ensNodeSchema from '@ensnode/ensdb-sdk/ensnode'; - -// Contains the metadata table definition -const { metadata } = ensNodeSchema; -``` - -## ENSDb Reader - - - -## ENSDb Writer - - - -## Schema Checksums - -Compute a checksum for a Drizzle schema definition: - -```typescript -import { getDrizzleSchemaChecksum } from '@ensnode/ensdb-sdk'; -import * as schema from '@ensnode/ensdb-sdk/ensindexer-abstract'; - -const checksum = getDrizzleSchemaChecksum(schema); -// Returns: 10-character checksum string -``` - -Use this to detect when [Schema Definitions](/ensdb/concepts/glossary#schema-definition) change. - -## Related Documentation - -- **[Querying Guide](/ensdb/usage/querying)** — SQL examples for any language -- **[Database Schemas](/ensdb/concepts/database-schemas)** — Schema structure and tables -- **[Glossary](/ensdb/concepts/glossary)** — [ENSDb SDK](/ensdb/concepts/glossary#ensdb-sdk), [Reader](/ensdb/concepts/glossary#ensdb-reader), [Writer](/ensdb/concepts/glossary#ensdb-writer) definitions diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/reader.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/reader.mdx deleted file mode 100644 index de75dbbfe1..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/reader.mdx +++ /dev/null @@ -1,160 +0,0 @@ ---- -title: ENSDb Reader -description: Type-safe querying for ENSNode Metadata and ENSIndexer data. -sidebar: - label: ENSDb Reader - order: 3 ---- - -The [ENSDb Reader](/ensdb/concepts/glossary#ensdb-reader) provides a convenience interface for querying ENSDb data. It wraps Drizzle ORM with ENSDb-specific knowledge. - -For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). - -## Purpose - -The [ENSDb Reader](/ensdb/concepts/glossary#ensdb-reader): - -- Queries [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) records -- Provides typed access to indexed ENS data via Drizzle ORM -- Exposes the underlying Drizzle client for complex queries - -## Constructor - -```typescript -import { EnsDbReader } from '@ensnode/ensdb-sdk'; - -const reader = new EnsDbReader( - 'postgresql://user:password@host:5432/ensdb', // ENSDb connection string - 'ensindexer_mainnet' // ENSIndexer Schema Name -); -``` - -Parameters: -- `ensDbUrl` — PostgreSQL connection string for the [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance) -- `ensIndexerSchemaName` — The [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name) to query - -## Properties - -### `ensDb` - -The underlying Drizzle client for building custom queries: - -```typescript -const db = reader.ensDb; - -// Build custom queries -const results = await db - .select() - .from(reader.ensIndexerSchema.v1_domains) - .where(eq(reader.ensIndexerSchema.v1_domains.owner_id, '0x...')); -``` - -### `ensIndexerSchema` - -The "concrete" [ENSIndexer Schema](/ensdb/concepts/glossary#ensindexer-schema) definition for use in queries: - -```typescript -// Access tables from the ENSIndexer Schema -const { v1_domains, v2_domains, labels } = reader.ensIndexerSchema; -``` - -### `ensIndexerSchemaName` - -The [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name) used by this reader: - -```typescript -console.log(reader.ensIndexerSchemaName); // 'ensindexer_mainnet' -``` - -### `ensNodeSchema` - -The [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) definition: - -```typescript -// Access the metadata table -const { metadata } = reader.ensNodeSchema; -``` - -## Methods - -### `getEnsDbVersion()` - -Get the ENSDb version for this [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance): - -```typescript -const version = await reader.getEnsDbVersion(); -// Returns: string | undefined -``` - -### `getEnsIndexerPublicConfig()` - -Get the public configuration for this [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance): - -```typescript -const config = await reader.getEnsIndexerPublicConfig(); -// Returns: EnsIndexerPublicConfig | undefined -``` - -### `getIndexingStatusSnapshot()` - -Get the [Indexing Status](/ensdb/concepts/glossary#indexing-status) snapshot: - -```typescript -const status = await reader.getIndexingStatusSnapshot(); -// Returns: CrossChainIndexingStatusSnapshot | undefined - -if (status) { - console.log(status.status); // 'backfill' | 'following' | etc. -} -``` - -## Querying ENSIndexer Data - -For querying indexed data (domains, labels, etc.), use the Drizzle client with the `ensIndexerSchema`: - -```typescript -import { eq } from 'drizzle-orm'; - -// Query v1 domains -const v1Domains = await reader.ensDb - .select() - .from(reader.ensIndexerSchema.v1_domains) - .where(eq(reader.ensIndexerSchema.v1_domains.owner_id, '0x1234...')) - .limit(10); - -// Query v2 domains -const v2Domains = await reader.ensDb - .select() - .from(reader.ensIndexerSchema.v2_domains) - .where(eq(reader.ensIndexerSchema.v2_domains.registry_id, 'some-registry-id')); - -// Query labels -const label = await reader.ensDb - .select() - .from(reader.ensIndexerSchema.labels) - .where(eq(reader.ensIndexerSchema.labels.label_hash, '0xabc123...')); -``` - -## Multiple ENSIndexer Schemas - -To query data from multiple [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema), create separate readers: - -```typescript -const mainnetReader = new EnsDbReader(ensDbUrl, 'ensindexer_mainnet'); -const l2Reader = new EnsDbReader(ensDbUrl, 'ensindexer_l2'); - -// Query each schema -const mainnetDomains = await mainnetReader.ensDb - .select() - .from(mainnetReader.ensIndexerSchema.v1_domains); - -const l2Domains = await l2Reader.ensDb - .select() - .from(l2Reader.ensIndexerSchema.v1_domains); -``` - -## Related Documentation - -- **[ENSDb Writer](/ensdb/usage/ensdb-sdk/writer/)** — Write interface for ENSDb -- **[Database Schemas](/ensdb/concepts/database-schemas)** — Table definitions -- **[Querying Guide](/ensdb/usage/querying)** — SQL examples for any language diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/writer.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/writer.mdx deleted file mode 100644 index 068111266f..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/usage/ensdb-sdk/writer.mdx +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: ENSDb Writer -description: Write interface for ENSDb initialization and metadata updates. -sidebar: - label: ENSDb Writer - order: 4 ---- - -The [ENSDb Writer](/ensdb/concepts/glossary#ensdb-writer) provides an interface for writing to ENSDb. It extends [ENSDb Reader](/ensdb/usage/ensdb-sdk/reader/) with write capabilities, and is primarily used by [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance). - -For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). - -## Purpose - -The [ENSDb Writer](/ensdb/concepts/glossary#ensdb-writer): - -- Runs database migrations for [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) -- Updates [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) records -- Inherits all read capabilities from [ENSDb Reader](/ensdb/usage/ensdb-sdk/reader/) - -:::caution -The ENSDb Writer is intended for use by [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance). Most ENSDb consumers only need the [ENSDb Reader](/ensdb/usage/ensdb-sdk/reader/). -::: - -## Constructor - -```typescript -import { EnsDbWriter } from '@ensnode/ensdb-sdk'; - -const writer = new EnsDbWriter( - 'postgresql://user:password@host:5432/ensdb', // ENSDb connection string - 'ensindexer_mainnet' // ENSIndexer Schema Name -); -``` - -Parameters: -- `ensDbUrl` — PostgreSQL connection string for the [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance) -- `ensIndexerSchemaName` — The [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name) for this instance - -:::note -EnsDbWriter extends EnsDbReader, so you can use all reader methods like `getEnsDbVersion()`, `getEnsIndexerPublicConfig()`, and `getIndexingStatusSnapshot()` on a writer instance. -::: - -## Methods - -### `migrateEnsNodeSchema(migrationsDirPath)` - -Execute pending database migrations for [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema): - -```typescript -await writer.migrateEnsNodeSchema('./migrations/ensnode'); -``` - -Parameters: -- `migrationsDirPath` — File path to the directory containing database migration files - -This creates or updates the [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) and its tables. - -### `upsertEnsDbVersion(ensDbVersion)` - -Upsert the ENSDb version for this [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance): - -```typescript -await writer.upsertEnsDbVersion('1.2.3'); -``` - -Creates or updates the record with key `ensdb_version` in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table). - -### `upsertEnsIndexerPublicConfig(config)` - -Upsert the public configuration for this [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance): - -```typescript -await writer.upsertEnsIndexerPublicConfig({ - chains: ['mainnet', 'base'], - // ... other config -}); -``` - -Creates or updates the record with key `ensindexer_public_config` in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table). - -### `upsertIndexingStatusSnapshot(status)` - -Upsert the [Indexing Status](/ensdb/concepts/glossary#indexing-status) snapshot: - -```typescript -await writer.upsertIndexingStatusSnapshot({ - status: 'following', - progress: 100, - // ... other status fields -}); -``` - -Creates or updates the record with key `ensindexer_indexing_status` in [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table). - -## ENSNode Metadata Keys - -The [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) uses these keys: - -| Key | Method | Description | -|-----|--------|-------------| -| `ensdb_version` | `upsertEnsDbVersion()` | ENSDb version string | -| `ensindexer_public_config` | `upsertEnsIndexerPublicConfig()` | ENSIndexer public configuration | -| `ensindexer_indexing_status` | `upsertIndexingStatusSnapshot()` | [Indexing Status](/ensdb/concepts/glossary#indexing-status) snapshot | - -## Use by ENSIndexer - -[ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance) use the ENSDb Writer during: - -1. **Startup** — Run migrations for [ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema) -2. **Registration** — Upsert public config to [ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table) -3. **Indexing** — Update [Indexing Status](/ensdb/concepts/glossary#indexing-status) periodically -4. **Completion** — Record final status - -## Migration Pattern - -```typescript -import { EnsDbWriter } from '@ensnode/ensdb-sdk'; - -const writer = new EnsDbWriter(ensDbUrl, mySchemaName); - -// Run ENSNode Schema migrations -await writer.migrateEnsNodeSchema('./migrations/ensnode'); - -// Register this ENSIndexer instance -await writer.upsertEnsIndexerPublicConfig(myPublicConfig); - -// During indexing - update status periodically -await writer.upsertIndexingStatusSnapshot({ - status: 'backfill', - progress: 50, -}); - -// When following -await writer.upsertIndexingStatusSnapshot({ - status: 'following', - progress: 100, -}); -``` - -## Related Documentation - -- **[ENSDb Reader](/ensdb/usage/ensdb-sdk/reader/)** — Read interface (inherited by Writer) -- **[ENSNode Schema](/ensdb/concepts/glossary#ensnode-schema)** — Schema structure -- **[ENSNode Metadata Table](/ensdb/concepts/glossary#ensnode-metadata-table)** — Metadata storage -- **[Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle)** — Status transitions diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx deleted file mode 100644 index c5faf57e9b..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx +++ /dev/null @@ -1,137 +0,0 @@ ---- -title: Using ENSDb -description: Practical guides for querying and integrating with ENSDb. -sidebar: - label: Usage - order: 4 ---- - -import { LinkCard } from '@astrojs/starlight/components'; - -This section provides practical guides for working with ENSDb. Since ENSDb is a standard PostgreSQL database, you can use any PostgreSQL client in any programming language. - -For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). - -## Querying ENSDb - - - -## TypeScript SDK - - - -## Language Examples - -ENSDb works with any PostgreSQL client. Here are connection examples for common languages: - -### Python - -```python -import psycopg2 - -conn = psycopg2.connect( - host="localhost", - database="ensdb", - user="postgres", - password="password" -) - -cursor = conn.cursor() - -# Discover [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema) -cursor.execute(""" - SELECT DISTINCT ens_indexer_schema_name - FROM ensnode.metadata -""") -schemas = cursor.fetchall() - -# Query indexed data -cursor.execute(""" - SELECT * FROM ensindexer_abc123.v1_domains - LIMIT 10 -""") -domains = cursor.fetchall() -``` - -### JavaScript (Node.js) - -```javascript -const { Pool } = require('pg'); - -const pool = new Pool({ - host: 'localhost', - database: 'ensdb', - user: 'postgres', - password: 'password', -}); - -// Discover ENSIndexer Schemas -const { rows: schemas } = await pool.query(` - SELECT DISTINCT ens_indexer_schema_name - FROM ensnode.metadata -`); - -// Query indexed data -const { rows: domains } = await pool.query(` - SELECT * FROM ensindexer_abc123.v1_domains - LIMIT 10 -`); -``` - -### Go - -```go -package main - -import ( - "database/sql" - "fmt" - _ "github.com/lib/pq" -) - -func main() { - connStr := "host=localhost dbname=ensdb user=postgres password=password sslmode=disable" - db, err := sql.Open("postgres", connStr) - if err != nil { - panic(err) - } - defer db.Close() - - // Discover ENSIndexer Schemas - rows, err := db.Query(` - SELECT DISTINCT ens_indexer_schema_name - FROM ensnode.metadata - `) - // ... -} -``` - -### Rust - -```rust -use postgres::{Client, NoTls}; - -fn main() { - let mut client = Client::connect( - "host=localhost dbname=ensdb user=postgres password=password", - NoTls, - ).unwrap(); - - // Discover ENSIndexer Schemas - for row in client.query( - "SELECT DISTINCT ens_indexer_schema_name FROM ensnode.metadata", - &[], - ).unwrap() { - let schema_name: String = row.get(0); - println!("{}", schema_name); - } -} -``` diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/querying.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/querying.mdx deleted file mode 100644 index 71e660038e..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/usage/querying.mdx +++ /dev/null @@ -1,290 +0,0 @@ ---- -title: Querying ENSDb -description: SQL examples for discovering schemas and querying indexed ENS data. -sidebar: - label: Querying Guide - order: 1 ---- - -Query ENSDb using any PostgreSQL client with SQL. This guide provides practical examples for common query patterns. - -For terminology definitions, see the [Glossary](/ensdb/concepts/glossary). - -## Connection - -Connect to an ENSDb instance using standard PostgreSQL connection parameters: - -```bash -psql postgresql://user:password@host:5432/ensdb -``` - -Connection parameters: - -| Parameter | Description | -|-----------|-------------| -| `host` | ENSDb server hostname | -| `port` | PostgreSQL port (default: 5432) | -| `database` | Database name (typically `ensdb`) | -| `user` | Database user | -| `password` | Database password | - -## Discovering ENSIndexer Schemas - -Before querying indexed data, discover which [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema) exist in the [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance): - -### List All ENSIndexer Schemas - -```sql -SELECT DISTINCT ens_indexer_schema_name -FROM ensnode.metadata -ORDER BY ens_indexer_schema_name; -``` - -Result: - -``` -ens_indexer_schema_name -──────────────────────── -ensindexer_mainnet -ensindexer_l2 -ensindexer_custom -``` - -### Get Schema Details - -```sql -SELECT - ens_indexer_schema_name, - key, - value_version, - value -FROM ensnode.metadata -WHERE ens_indexer_schema_name = 'ensindexer_mainnet' -ORDER BY key; -``` - -### Check Indexing Status - -```sql -SELECT - ens_indexer_schema_name, - value->>'status' as status, - value->>'progress' as progress -FROM ensnode.metadata -WHERE key = 'ensindexer_indexing_status'; -``` - -Result: - -``` -ens_indexer_schema_name status progress -──────────────────────── ─────── ───────── -ensindexer_mainnet following 100% -ensindexer_l2 backfill 45% -``` - -:::tip[Performance] -Queries are faster when ENSIndexer is in `following` status ([indexes](/ensdb/concepts/glossary#database-objects) are created). During `backfill`, queries may be slower because [indexes](/ensdb/concepts/glossary#database-objects) are dropped to optimize write throughput. -::: - -## Querying ENSIndexer Data - -Once you know the [ENSIndexer Schema Name](/ensdb/concepts/glossary#ensindexer-schema-name), query its tables: - -### List Tables - -```sql -SELECT table_name -FROM information_schema.tables -WHERE table_schema = 'ensindexer_mainnet'; -``` - -Result: - -``` -table_name -────────────────────── -v1_domains -v2_domains -labels -accounts -registrations -renewals -events -domain_events -... -``` - -### Query Domains - -```sql --- ENSv1 domains -SELECT - id, - parent_id, - owner_id, - label_hash -FROM ensindexer_mainnet.v1_domains -LIMIT 10; -``` - -```sql --- ENSv2 domains -SELECT - id, - registry_id, - owner_id, - label_hash, - token_id -FROM ensindexer_mainnet.v2_domains -LIMIT 10; -``` - -### Query by Owner - -```sql --- Domains owned by address -SELECT * -FROM ensindexer_mainnet.v1_domains -WHERE owner_id = '\x1234567890abcdef1234567890abcdef12345678'; -``` - -### Query Labels (Name Healing) - -```sql --- Look up interpreted label for a labelhash -SELECT label_hash, interpreted -FROM ensindexer_mainnet.labels -WHERE label_hash = '\x_af2caa...03cc'; - --- Search labels by interpreted text -SELECT label_hash, interpreted -FROM ensindexer_mainnet.labels -WHERE interpreted LIKE '%vitalik%'; -``` - -### Query Registrations - -```sql -SELECT - r.id, - r.domain_id, - r.type, - r.start, - r.expiry, - r.registrant_id -FROM ensindexer_mainnet.registrations r -WHERE r.expiry > EXTRACT(EPOCH FROM NOW()) -ORDER BY r.expiry ASC -LIMIT 20; -``` - -### Query Events - -```sql --- Recent events for a domain -SELECT e.* -FROM ensindexer_mainnet.events e -JOIN ensindexer_mainnet.domain_events de ON e.id = de.event_id -WHERE de.domain_id = '0x...' -ORDER BY e.block_number DESC -LIMIT 10; -``` - -## Cross-Schema Queries - -Query across multiple [ENSIndexer Schemas](/ensdb/concepts/glossary#ensindexer-schema) in one query: - -### Union Data from Multiple Schemas - -```sql -SELECT 'mainnet' as source, id, owner_id -FROM ensindexer_mainnet.v1_domains -WHERE owner_id = '\x1234...' - -UNION ALL - -SELECT 'l2' as source, id, owner_id -FROM ensindexer_l2.v1_domains -WHERE owner_id = '\x1234...'; -``` - -### Compare Schema Content - -```sql -SELECT - 'mainnet' as schema, - COUNT(*) as domain_count -FROM ensindexer_mainnet.v1_domains - -UNION ALL - -SELECT - 'l2' as schema, - COUNT(*) as domain_count -FROM ensindexer_l2.v1_domains; -``` - -## Querying Ponder Schema - -The [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) (`ponder_sync`) contains RPC cache data. It's primarily used internally by [ENSIndexer instances](/ensdb/concepts/glossary#ensindexer-instance), but can be queried for debugging: - -```sql -SELECT table_name -FROM information_schema.tables -WHERE table_schema = 'ponder_sync'; -``` - -:::note -The [Ponder Schema](/ensdb/concepts/glossary#ponder-schema) structure is defined by Ponder and may change between Ponder versions. It's not intended for direct querying by ENSDb consumers. -::: - -## Performance Tips - -### Use Indexes - -Queries are fastest when an [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) is in `following` [Indexing Status](/ensdb/concepts/glossary#indexing-status). Check before running heavy queries: - -```sql -SELECT value->>'status' as status -FROM ensnode.metadata -WHERE ens_indexer_schema_name = 'ensindexer_mainnet' - AND key = 'ensindexer_indexing_status'; -``` - -### Use LIMIT - -Always limit results during exploration: - -```sql -SELECT * FROM ensindexer_mainnet.v1_domains LIMIT 10; -``` - -### Use WHERE Clauses - -Filter early to reduce data scanned: - -```sql --- Good: filters on indexed column -SELECT * FROM ensindexer_mainnet.v1_domains -WHERE owner_id = '\x1234...'; - --- Slow: scans full table -SELECT * FROM ensindexer_mainnet.v1_domains -WHERE label_hash NOT IN (SELECT label_hash FROM ensindexer_mainnet.labels); -``` - -### Use EXPLAIN ANALYZE - -Check query plans for slow queries: - -```sql -EXPLAIN ANALYZE -SELECT * FROM ensindexer_mainnet.v1_domains WHERE owner_id = '\x1234...'; -``` - -## Related Documentation - -- **[ENSDb SDK](/ensdb/concepts/glossary#ensdb-sdk)** — TypeScript utilities for type-safe queries -- **[Database Schemas](/ensdb/concepts/database-schemas)** — Table definitions -- **[Indexing Lifecycle](/ensdb/concepts/indexing-lifecycle)** — Why query performance varies diff --git a/docs/ensnode.io/src/content/docs/ensdb/use-cases/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/use-cases/index.mdx deleted file mode 100644 index 46f89a93a2..0000000000 --- a/docs/ensnode.io/src/content/docs/ensdb/use-cases/index.mdx +++ /dev/null @@ -1,502 +0,0 @@ ---- -title: ENSDb Use Cases -description: Real-world examples of what you can build with ENSDb. Analytics dashboards, CLIs, custom APIs, data pipelines, and more. -sidebar: - label: Use Cases - order: 5 ---- - -import { Card, CardGrid, Aside, Tabs, TabItem } from '@astrojs/starlight/components'; - -ENSDb unlocks a new universe of ENS applications. Here are real-world use cases you can build today. - -## Use Case Categories - - - -Real-time analytics, market intelligence, and visualization dashboards with sub-second query latency. - - -Specialized GraphQL, REST, or gRPC APIs tailored to specific use cases or applications. - - -CLIs, SDKs, and developer utilities for ENS operations and data exploration. - - -ETL pipelines, data warehouses, search indexes, and real-time streaming systems. - - -ML models for name valuation, trend prediction, and pattern recognition. - - -Real-time monitoring of ENS state changes, expiration alerts, and event notifications. - - - ---- - -## Analytics & Dashboards - -Build analytics dashboards that exceed what's possible on platforms like Dune. - -### Why ENSDb Over Dune? - -| Capability | Dune | ENSDb | -|------------|------|-------| -| Query latency | Seconds to minutes | Sub-second | -| Data freshness | Hours delayed | Real-time | -| Complex joins | Limited | Full SQL power | -| Custom aggregations | Constrained | Unlimited | -| Private data | No | Yes | -| Cost at scale | Expensive | Predictable | - -### Example: Domain Ownership Analytics - -Track ownership concentration, identify whales, analyze trends: - -```sql --- Top domain owners by count -SELECT owner_id, COUNT(*) as domain_count -FROM ensindexer_mainnet.v1_domains -GROUP BY owner_id -ORDER BY domain_count DESC -LIMIT 100; - --- Ownership over time -SELECT - DATE_TRUNC('month', to_timestamp(e.timestamp)) as month, - COUNT(DISTINCT d.owner_id) as unique_owners, - COUNT(*) as new_domains -FROM ensindexer_mainnet.events e -JOIN ensindexer_mainnet.domain_events de ON e.id = de.event_id -JOIN ensindexer_mainnet.v1_domains d ON de.domain_id = d.id -WHERE e.selector = '\x0x...' -- NewOwner event -GROUP BY month -ORDER BY month; -``` - -### Example: Registration Market Trends - -Analyze registration patterns, pricing trends, and market activity: - -```sql --- Daily registration volume with pricing -SELECT - DATE_TRUNC('day', to_timestamp(ra.timestamp)) as day, - COUNT(*) as registrations, - AVG(ra.total) as avg_cost_wei, - SUM(ra.total) as total_revenue_wei -FROM ensindexer_mainnet.registrar_actions ra -WHERE ra.type = 'registration' - AND ra.timestamp > EXTRACT(EPOCH FROM NOW() - INTERVAL '30 days') -GROUP BY day -ORDER BY day DESC; - --- Premium vs standard registrations -SELECT - CASE - WHEN ra.premium > 0 THEN 'premium' - ELSE 'standard' - END as type, - COUNT(*) as count, - AVG(ra.total) as avg_cost -FROM ensindexer_mainnet.registrar_actions ra -WHERE ra.type = 'registration' -GROUP BY type; -``` - -### Tools to Build With - -- **Grafana** — Dashboards and alerting -- **Metabase** — Business intelligence -- **Apache Superset** — Modern data exploration -- **Custom React/Vue dashboards** — Tailored interfaces - ---- - -## Custom APIs - -Build specialized APIs for specific use cases. - -### Example: Domain Search API - -A fast search API for finding available or registered names: - -```typescript -// Express.js example -app.get('/api/search', async (req, res) => { - const { query, tld = 'eth' } = req.query; - - // Search labels - const labels = await pool.query(` - SELECT - l.interpreted, - EXISTS( - SELECT 1 FROM ensindexer_mainnet.v1_domains d - WHERE d.label_hash = l.label_hash - ) as registered - FROM ensindexer_mainnet.labels l - WHERE l.interpreted ILIKE $1 - LIMIT 100 - `, [`%${query}%`]); - - // Check exact match availability - const exact = await pool.query(` - SELECT d.id, d.owner_id, r.expiry - FROM ensindexer_mainnet.v1_domains d - LEFT JOIN ensindexer_mainnet.registrations r ON d.id = r.domain_id - JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash - WHERE l.interpreted = $1 - `, [query]); - - res.json({ - suggestions: labels.rows, - exactMatch: exact.rows[0] || null, - available: exact.rows.length === 0 - }); -}); -``` - -### Example: ENS Profile API - -Aggregate all ENS data for an address: - -```sql --- Get complete profile for an address -WITH domains AS ( - SELECT d.id, l.interpreted as name - FROM ensindexer_mainnet.v1_domains d - JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash - WHERE d.owner_id = '\x1234...' -), -records AS ( - SELECT - rr.node, - rr.name as primary_name, - jsonb_object_agg(rar.coin_type::text, rar.value) as addresses, - jsonb_object_agg(rtr.key, rtr.value) as texts - FROM ensindexer_mainnet.resolver_records rr - LEFT JOIN ensindexer_mainnet.resolver_address_records rar - ON (rr.chain_id, rr.address, rr.node) = (rar.chain_id, rar.address, rar.node) - LEFT JOIN ensindexer_mainnet.resolver_text_records rtr - ON (rr.chain_id, rr.address, rr.node) = (rtr.chain_id, rtr.address, rtr.node) - WHERE rr.node IN (SELECT id FROM domains) - GROUP BY rr.node, rr.name -) -SELECT - d.id, - d.name, - r.primary_name, - r.addresses, - r.texts -FROM domains d -LEFT JOIN records r ON d.id = r.node; -``` - ---- - -## Developer Tools - -Build CLIs and developer utilities. - -### Example: ENS CLI - -A command-line tool for ENS operations: - -```bash -# Check domain info -$ ensdb domain vitalik.eth -Owner: 0x1234... -Resolver: 0x5678... -Records: - - ETH: 0x1234... - - BTC: bc1q... - - com.twitter: @vitalikbuterin -Expiry: 2025-12-31 - -# List domains by owner -$ ensdb list 0x1234... --format json -[{"name": "vitalik.eth", ...}, ...] - -# Check expiration dates -$ ensdb expiring --within 30d --format table -Name Expiry Days Left -vitalik.eth 2025-12-31 365 -... - -# Export data -$ ensdb export --owner 0x1234... --format csv > my_domains.csv -``` - -### Implementation - - - -```python -import click -import psycopg2 -from psycopg2.extras import RealDictCursor - -@click.group() -def cli(): - """ENS CLI - Query ENS data from ENSDb""" - pass - -@cli.command() -@click.argument('name') -def domain(name): - """Get information about a domain""" - conn = psycopg2.connect(database='ensdb') - cursor = conn.cursor(cursor_factory=RealDictCursor) - - # Normalize name to get node - cursor.execute(""" - SELECT d.id, d.owner_id, r.expiry - FROM ensindexer_mainnet.v1_domains d - LEFT JOIN ensindexer_mainnet.registrations r ON d.id = r.domain_id - JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash - WHERE l.interpreted = %s - """, (name.replace('.eth', ''),)) - - domain = cursor.fetchone() - if not domain: - click.echo(f"Domain {name} not found") - return - - click.echo(f"Domain: {name}") - click.echo(f"Owner: {domain['owner_id']}") - click.echo(f"Expiry: {domain['expiry']}") - -@cli.command() -@click.argument('owner') -@click.option('--format', default='table', type=click.Choice(['table', 'json'])) -def list(owner, format): - """List domains owned by an address""" - # Implementation... - -if __name__ == '__main__': - cli() -``` - - - ---- - -## Data Pipelines - -Integrate ENS data into your existing infrastructure. - -### Example: Webhook Pipeline - -Trigger webhooks on ENS state changes: - -```python -import asyncio -import asyncpg -import aiohttp -from datetime import datetime - -async def webhook_pipeline(): - conn = await asyncpg.connect('postgresql://localhost/ensdb') - - # Track last processed block - last_block = load_checkpoint() - - async with conn.add_listener('ens_events') as queue: - async for event in queue: - # New event detected - event_data = json.loads(event.payload) - - # Fetch full event details - row = await conn.fetchrow(""" - SELECT e.*, de.domain_id - FROM ensindexer_mainnet.events e - JOIN ensindexer_mainnet.domain_events de ON e.id = de.event_id - WHERE e.id = $1 - """, event_data['event_id']) - - # Send webhook - async with aiohttp.ClientSession() as session: - await session.post( - 'https://your-app.com/webhooks/ens', - json={ - 'event': row['selector'], - 'domain': row['domain_id'], - 'block': row['block_number'], - 'tx': row['transaction_hash'], - } - ) - -asyncio.run(webhook_pipeline()) -``` - -### Example: Data Warehouse Sync - -Sync ENS data to your data warehouse: - -```python -# Daily sync job -from google.cloud import bigquery - -def sync_to_warehouse(): - # Query ENSDb - ens_data = query_ensdb(""" - SELECT - d.id, - l.interpreted as label, - d.owner_id, - r.expiry, - e.timestamp as created_at - FROM ensindexer_mainnet.v1_domains d - JOIN ensindexer_mainnet.labels l ON d.label_hash = l.label_hash - LEFT JOIN ensindexer_mainnet.registrations r ON d.id = r.domain_id - LEFT JOIN ensindexer_mainnet.events e ON d.id = ( - SELECT domain_id FROM ensindexer_mainnet.domain_events - WHERE event_id = r.event_id - ) - WHERE d.created_at > %s - """, (yesterday,)) - - # Load to BigQuery/Snowflake/Redshift - client = bigquery.Client() - table = client.dataset('ens').table('domains') - - errors = client.insert_rows(table, ens_data) - if errors: - print(f"Errors: {errors}") -``` - ---- - -## AI & Machine Learning - -Train models on complete ENS datasets. - -### Example: Name Valuation Model - -Predict the value of an ENS name: - -```python -import pandas as pd -from sklearn.ensemble import RandomForestRegressor - -# Extract features from ENSDb -df = pd.read_sql(""" - SELECT - l.interpreted, - LENGTH(l.interpreted) as length, - CASE WHEN l.interpreted ~ '^[0-9]+$' THEN 1 ELSE 0 END as is_numeric, - CASE WHEN l.interpreted ~ '^[a-z]+$' THEN 1 ELSE 0 END as is_alpha, - -- Add more features... - ra.total as price_wei - FROM ensindexer_mainnet.labels l - JOIN ensindexer_mainnet.v1_domains d ON l.label_hash = d.label_hash - JOIN ensindexer_mainnet.registrar_actions ra ON d.id = ra.node - WHERE ra.type = 'registration' - AND ra.total IS NOT NULL -"", conn) - -# Train model -X = df[['length', 'is_numeric', 'is_alpha']] -y = df['price_wei'] - -model = RandomForestRegressor() -model.fit(X, y) - -# Predict -prediction = model.predict([[5, 0, 1]]) # 5-letter alpha name -print(f"Predicted value: {prediction[0]} wei") -``` - ---- - -## Monitoring & Alerts - -Build real-time monitoring systems. - -### Example: Expiration Monitor - -Alert users before their names expire: - -```python -from datetime import datetime, timedelta -import smtplib - -def check_expiring_names(): - conn = psycopg2.connect(database='ensdb') - cursor = conn.cursor(cursor_factory=RealDictCursor) - - # Find names expiring in next 30 days - cursor.execute(""" - SELECT - l.interpreted as name, - d.owner_id, - rl.expires_at, - EXTRACT(DAY FROM to_timestamp(rl.expires_at) - NOW()) as days_left - FROM ensindexer_mainnet.registration_lifecycles rl - JOIN ensindexer_mainnet.labels l ON rl.node = ( - SELECT id FROM ensindexer_mainnet.v1_domains - WHERE label_hash = l.label_hash LIMIT 1 - ) - JOIN ensindexer_mainnet.v1_domains d ON rl.node = d.id - WHERE rl.expires_at < EXTRACT(EPOCH FROM NOW() + INTERVAL '30 days') - AND rl.expires_at > EXTRACT(EPOCH FROM NOW()) - ORDER BY rl.expires_at - """) - - expiring = cursor.fetchall() - - # Group by owner and send alerts - by_owner = {} - for name in expiring: - owner = name['owner_id'] - if owner not in by_owner: - by_owner[owner] = [] - by_owner[owner].append(name) - - for owner, names in by_owner.items(): - send_expiration_alert(owner, names) - -def send_expiration_alert(owner, names): - # Send email/push notification... - pass -``` - ---- - -## Success Stories - -Here are examples of what teams could build with ENSDb: - -:::note -The following are illustrative examples of potential use cases. These specific implementations may not exist yet. -::: - -### Analytics Platform -> "We built a real-time ENS analytics platform with sub-second query latency. What used to take 30 seconds on Dune now happens instantly." - -### Domain Marketplace -> "Our marketplace uses a custom reader to show name availability, pricing history, and ownership records in real-time." - -### Portfolio Tracker -> "Users connect their wallet and see all their ENS names, expiration dates, and renewal costs in one dashboard." - -### Research Tool -> "Academic researchers use ENSDb to study ENS adoption patterns, name distribution, and market dynamics." - ---- - -## Getting Started - -Pick a use case and start building: - -1. **For Analytics** — Start with SQL queries in [Querying Guide](/ensdb/usage/querying/) -2. **For APIs** — Build a [Custom Reader](/ensdb/integrations/reader/) -3. **For CLIs** — Use Python + Click or Go + Cobra -4. **For Pipelines** — Set up change data capture with PostgreSQL logical replication - -## Related Documentation - -- **[Building Integrations](/ensdb/integrations/)** — Build custom readers and writers -- **[Querying Guide](/ensdb/usage/querying/)** — SQL patterns and examples -- **[Database Schemas](/ensdb/concepts/database-schemas/)** — Complete schema reference From d9b125d7636ba072f4bc07e1469d6a1530ba217b Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 17 Apr 2026 08:52:22 +0200 Subject: [PATCH 3/3] Create a compact structure for the docs mvp --- .../config/integrations/starlight.ts | 14 +- .../docs/ensdb/concepts/architecture.mdx | 10 + .../docs/ensdb/concepts/database-schemas.mdx | 10 + .../content/docs/ensdb/concepts/glossary.mdx | 2 +- .../src/content/docs/ensdb/concepts/index.mdx | 55 ++--- .../ensnode.io/src/content/docs/ensdb/faq.mdx | 8 + .../src/content/docs/ensdb/index.mdx | 203 +++--------------- .../docs/ensdb/integrations/ensnode.mdx | 81 +++++++ .../src/content/docs/ensdb/usage/index.mdx | 22 ++ .../src/content/docs/ensdb/usage/sdk.mdx | 41 ++++ .../src/content/docs/ensdb/usage/sql.mdx | 50 +++++ .../docs/ensdb/usage/troubleshooting.mdx | 8 + 12 files changed, 284 insertions(+), 220 deletions(-) create mode 100644 docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/faq.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/integrations/ensnode.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/sdk.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/sql.mdx create mode 100644 docs/ensnode.io/src/content/docs/ensdb/usage/troubleshooting.mdx diff --git a/docs/ensnode.io/config/integrations/starlight.ts b/docs/ensnode.io/config/integrations/starlight.ts index b55c260b12..3c9b5e2f55 100644 --- a/docs/ensnode.io/config/integrations/starlight.ts +++ b/docs/ensnode.io/config/integrations/starlight.ts @@ -129,24 +129,18 @@ export function starlight(): AstroIntegration { autogenerate: { directory: "ensdb/concepts" }, }, { - label: "Usage", + label: "Using ENSDb", collapsed: false, autogenerate: { directory: "ensdb/usage" }, }, - { - label: "Use Cases", - collapsed: false, - autogenerate: { directory: "ensdb/use-cases" }, - }, { label: "Integrations", - collapsed: true, + collapsed: false, autogenerate: { directory: "ensdb/integrations" }, }, { - label: "Operations", - collapsed: true, - autogenerate: { directory: "ensdb/operations" }, + label: "FAQ", + link: "/ensdb/faq", }, ], }, diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx new file mode 100644 index 0000000000..8c86f17571 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/concepts/architecture.mdx @@ -0,0 +1,10 @@ +--- +title: Architecture +description: Understanding the architecture of ENSDb, including its modular design and data flow. +sidebar: + label: Architecture + order: 3 +keywords: [ensdb, architecture, design, data flow] +--- + +This section explains the architecture of ENSDb, including its modular design and how data flows through the system. Understanding this architecture will help you work effectively with ENSDb. diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx new file mode 100644 index 0000000000..4793bf7f6b --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx @@ -0,0 +1,10 @@ +--- +title: Database Schemas +description: Detailed explanation of all database schemas that make up ENSDb. +sidebar: + label: Database Schemas + order: 4 +keywords: [ensdb, database schemas, ponder schema, ensnode schema, ensindexer schema] +--- + +This section explains the different database schemas used in ENSDb, including the Ponder Schema, ENSNode Schema, and the modular ENSIndexer Schema. diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/glossary.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/glossary.mdx index 2a7e1d6f32..44a79a6fce 100644 --- a/docs/ensnode.io/src/content/docs/ensdb/concepts/glossary.mdx +++ b/docs/ensnode.io/src/content/docs/ensdb/concepts/glossary.mdx @@ -3,7 +3,7 @@ title: Glossary description: Core terminology used throughout ENSDb documentation. sidebar: label: Glossary - order: 1 + order: 2 keywords: [ensdb, glossary, terminology, definitions] --- diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/index.mdx index 134fecf4b9..e288d32de5 100644 --- a/docs/ensnode.io/src/content/docs/ensdb/concepts/index.mdx +++ b/docs/ensnode.io/src/content/docs/ensdb/concepts/index.mdx @@ -1,63 +1,44 @@ --- -title: ENSDb Concepts +title: Overview description: Understanding the fundamentals of ENSDb architecture, schemas, and data flow. sidebar: - label: Concepts - order: 3 + label: Overview + order: 1 --- import { LinkCard } from '@astrojs/starlight/components'; -This section explains the core concepts of ENSDb. Understanding these fundamentals will help you work effectively with ENSDb, whether you're querying data, integrating with third-party services, or operating an ENSNode instance. +## Who Should Read This + +- **Developers integrating with ENSDb** — Understand the schema structure to write effective queries +- **DevOps operators running ENSNode** — Understand how ENSDb behaves during different indexing phases +- **Anyone building custom writers or readers** — Learn the architecture for implementing the standard ## Core Concepts +Understanding these fundamentals will help you work effectively with ENSDb, whether you're querying data, integrating with third-party services, or operating an ENSNode instance. + - - -## What is ENSDb? - -ENSDb is an **open standard** for bi-directional ENS integration. It represents a new category of integration point for building on ENS: - -- **Open Standard** — Anyone can build writers or readers following the schema specifications -- **Bi-Directional** — Write operations produce an ENSDb, read operations consume it -- **Language Agnostic** — Any programming language with PostgreSQL support -- **Complete State** — The entire onchain ENS state, in your database - -Read the [What is ENSDb?](/ensdb/) overview for the full vision. - -## Who Should Read This - -- **Developers integrating with ENSDb** — Understand the schema structure to write effective queries -- **DevOps operators running ENSNode** — Understand how ENSDb behaves during different indexing phases -- **Anyone building custom writers or readers** — Learn the architecture for implementing the standard - ## Next Steps After understanding these concepts: -1. **[Query ENSDb with SQL](/ensdb/usage/querying)** — Language-agnostic SQL examples -2. **[Use the TypeScript SDK](/ensdb/usage/ensdb-sdk/)** — Type-safe database access -3. **[Build an Integration](/ensdb/integrations/)** — Create custom writers or readers -4. **[Explore Use Cases](/ensdb/use-cases/)** — See what others are building +1. **[Query ENSDb with SQL](/ensdb/usage/sql)** — Language-agnostic SQL examples +2. **[Use the TypeScript SDK](/ensdb/usage/sdk)** — Type-safe database access diff --git a/docs/ensnode.io/src/content/docs/ensdb/faq.mdx b/docs/ensnode.io/src/content/docs/ensdb/faq.mdx new file mode 100644 index 0000000000..e4ff4ccc82 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/faq.mdx @@ -0,0 +1,8 @@ +--- +title: Frequently Asked Questions +sidebar: + label: FAQ + order: 90 +--- + +This page answers the most common questions about ENSDb. diff --git a/docs/ensnode.io/src/content/docs/ensdb/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/index.mdx index 586fb07fa4..cc815db2b9 100644 --- a/docs/ensnode.io/src/content/docs/ensdb/index.mdx +++ b/docs/ensnode.io/src/content/docs/ensdb/index.mdx @@ -10,7 +10,7 @@ import { LinkCard, Aside, Card, CardGrid } from '@astrojs/starlight/components'; ## Vision -Getting the whole onchain state of ENS in your database. +Get the whole onchain state of ENS in your database. ## Core Philosophy @@ -23,96 +23,14 @@ Any app following the standard can: - Perform read operations against the ENSDb instance. -### Reference Implementation - -ENSNode is the reference implementation of the ENSDb standard, providing a complete ecosystem of tools and services for building with ENSDb. Each ENSNode instance includes: -- [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance) — The PostgreSQL database following the ENSDb standard -- [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) — The reference ENSDb Writer implementation that indexes onchain ENS data -- [ENSApi instance](/ensdb/concepts/glossary#ensapi-instance) — The reference ENSDb Reader implementation that serves GraphQL and REST APIs - -```mermaid -flowchart LR - subgraph ENSNode["ENSNode Environment"] - subgraph ENSNodeMainnet["ENSNode 'Mainnet' instance"] - direction LR - ENSIndexerMainnet[ENSIndexer 'Mainnet' instance] - ENSApiMainnet@{ shape: procs, label: "ENSApi Sepolia instances" } - end - - subgraph ENSNodeSepolia["ENSNode 'Sepolia' instance"] - direction LR - ENSIndexerSepolia[ENSIndexer 'Sepolia' instance] - ENSApiSepolia[ENSApi 'Sepolia' instance] - end - end - - subgraph PostgreSQLServer["PostgreSQL Server instance"] - ENSDbMainnet[(ENSDb 'Mainnet' instance)] - PSMainnet(Ponder Schema) - NSMainnet(ENSNode Schema) - EISMainnet@{ shape: procs, label: "ENSIndexer Schema" } - - ENSDbMainnet -->|1..1| PSMainnet - ENSDbMainnet -->|1..1| NSMainnet - ENSDbMainnet -->|1..*| EISMainnet - - - ENSDbSepolia[(ENSDb 'Sepolia' instance)] - PSSepolia(Ponder Schema) - NSSepolia(ENSNode Schema) - EISSepolia@{ shape: procs, label: "ENSIndexer Schema" } - - ENSDbSepolia -->|1..1| PSSepolia - ENSDbSepolia -->|1..1| NSSepolia - ENSDbSepolia -->|1..*| EISSepolia - end - - ENSIndexerMainnet -->|Write| ENSDbMainnet - ENSIndexerSepolia -->|Write| ENSDbSepolia - ENSApiMainnet -->|Read| ENSDbMainnet - ENSApiSepolia -->|Read| ENSDbSepolia - -``` - - - -## What is an ENSDb Instance? - -An **ENSDb instance** is a PostgreSQL database that follows the ENSDb open standard. Key characteristics: - -| Aspect | Description | -|--------|-------------| -| **What it is** | A PostgreSQL database (logical database within a server) | -| **Where it runs** | Served from a PostgreSQL server | -| **Multi-tenancy** | One ENSDb instance can store data from multiple ENSIndexer instances (tenants) | -| **Contains** | Complete indexed ENS state | - -### Example: Multi-Instance Server - -A single PostgreSQL server can serve multiple ENSDb instances for different environments: - -```mermaid -flowchart TB - subgraph PGServer["PostgreSQL Server (localhost:5432)"] - direction TB - Mainnet["ensdb_mainnet
← Production environment (mainnet data)"] - Testnet["ensdb_testnet
← Pre-production environment (testnet data)"] - Devnet["ensdb_devnet
← Staging / local development environment"] - end -``` - -Each ENSDb instance is an independent database containing complete ENS data for its respective environment. - ## What You Get ### Complete ENS State -ENSDb contains the **entire onchain state of ENS**: +An **ENSDb instance** contains the **entire onchain state of ENS**: - All domains (ENSv1 and ENSv2) - All registrations and renewals @@ -120,16 +38,15 @@ ENSDb contains the **entire onchain state of ENS**: - All events and ownership history - All NFT/token data for names - ### PostgreSQL Benefits By building on a PostgreSQL database, ENSDb inherits world-class capabilities: -- **ACID transactions** — Data integrity guarantees -- **Complex queries** — Joins, aggregations, window functions -- **Scalability** — Replication, sharding, connection pooling -- **Ecosystem** — Mature tools, ORMs, dashboards, analytics platforms -- **Reliability** — Decades of production-proven technology +- ACID transactions — Data integrity guarantees +- Complex queries — Joins, aggregations, window functions +- Scalability — Replication, sharding, connection pooling +- Ecosystem — Mature tools, ORMs, dashboards, analytics platforms +- Reliability — Decades of production-proven technology ## What You Can Build @@ -145,7 +62,7 @@ Create real-time dashboards and analytics pipelines. Better than Dune — you ha Build command-line tools for ENS operations. Query domains, check expiration, analyze name patterns — all from your terminal. - + Build reactive systems that respond to ENS state changes. Monitor registration lifecycles, ownership transfers, resolver updates. @@ -158,103 +75,45 @@ Train machine learning models on complete ENS datasets. Predict name values, det ## Quick Start -### Connect with Any PostgreSQL Client - -Connect to an ENSDb instance (a PostgreSQL database). The examples below assume you that ENSDb instances are served from a PostgreSQL server at `host:5432` with databases named `ensdb_mainnet`, `ensdb_testnet`, and `ensdb_devnet`: - -```bash -# Production environment (mainnet data) -psql postgresql://user:password@host:5432/ensdb_mainnet - -# Pre-production environment (testnet data) -psql postgresql://user:password@host:5432/ensdb_testnet - -# Staging / local development environment -psql postgresql://user:password@host:5432/ensdb_devnet -``` - -### Discover Available Schemas - -Once connected to an ENSDb instance, discover its ENSIndexer Schemas: - -```sql -SELECT DISTINCT ens_indexer_schema_name -FROM ensnode.metadata; -``` - -### Query Indexed Data - -Query data from a specific ENSIndexer Schema within your ENSDb instance: - -```sql --- Get domains from the ENSIndexer instance using the `ensindexer_mainnet` ENSIndexer Schema Name -SELECT * FROM ensindexer_mainnet.v1_domains LIMIT 10; - --- Get indexing status for the ENSIndexer instance using the `ensindexer_mainnet` ENSIndexer Schema Name -SELECT * FROM "ensnode"."metadata" -WHERE ens_indexer_schema_name = 'ensindexer_mainnet' -AND key = 'ensindexer_indexing_status' -AND value -> 'data' -> 'omnichainSnapshot' ->> 'omnichainStatus' = 'omnichain-following'; - -``` - -### Use the TypeScript SDK - -For TypeScript projects, the [ENSDb SDK](/ensdb/concepts/glossary#ensdb-sdk) provides typed access: - -```typescript -import { EnsDbReader } from '@ensnode/ensdb-sdk'; + -// Connect to a specific ENSDb instance by providing its connection string and the ENSIndexer Schema Name you want to query -const ensDbReader = new EnsDbReader(ensDbConnectionString, ensIndexerSchemaName); -const v1Domains = await - ensDbReader.ensDb.select() - .from(ensDbReader.ensIndexerSchema.v1Domain) - .limit(10); - .limit(10); -``` + ## Documentation Structure - - -## Get Started - -- **[Query ENSDb with SQL](/ensdb/usage/querying)** — Language-agnostic SQL examples -- **[Use the TypeScript SDK](/ensdb/usage/ensdb-sdk/)** — Type-safe database access -- **[Learn the Architecture](/ensdb/concepts/architecture)** — How schemas relate and data flows -- **[Build an Integration](/ensdb/integrations/)** — Create custom writers or readers - ## Related Projects -- **[ENSIndexer](/ensindexer/)** — The reference writer implementation -- **[ENSApi](/ensapi/)** — The reference reader implementation -- **[ENSRainbow](/ensrainbow/)** — Label healing service for ENSIndexer +- **[ENSIndexer](/ensindexer/)** — The reference ENSDb Writer implementation +- **[ENSApi](/ensapi/)** — The reference ENSDb Reader implementation diff --git a/docs/ensnode.io/src/content/docs/ensdb/integrations/ensnode.mdx b/docs/ensnode.io/src/content/docs/ensdb/integrations/ensnode.mdx new file mode 100644 index 0000000000..ceb9dd9307 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/integrations/ensnode.mdx @@ -0,0 +1,81 @@ +--- +title: ENSNode Reference Implementation +description: Overview of ENSNode, the reference implementation of the ENSDb standard, including its components and architecture. +sidebar: + label: ENSNode Reference Implementation + order: 1 +--- +import { Aside } from '@astrojs/starlight/components'; + +ENSNode is the reference implementation of the [ENSDb standard](/ensdb/concepts/glossary#ensdb-standard), providing a complete ecosystem of tools and services for building with ENSDb. Each ENSNode instance includes: +- At least one [ENSDb instance](/ensdb/concepts/glossary#ensdb-instance) — The PostgreSQL database following the ENSDb standard. +- At least one [ENSIndexer instance](/ensdb/concepts/glossary#ensindexer-instance) — The reference ENSDb Writer implementation that writes data into the ENSDb instance. +- At least one [ENSApi instance](/ensdb/concepts/glossary#ensapi-instance) — The reference ENSDb Reader implementation that serves GraphQL and REST APIs. + +```mermaid +flowchart LR + subgraph ENSNode["ENSNode Environment"] + subgraph ENSNodeMainnet["ENSNode 'Mainnet' instance"] + direction LR + ENSIndexerMainnet[ENSIndexer 'Mainnet' instance] + ENSApiMainnet@{ shape: procs, label: "ENSApi Mainnet instances" } + end + + subgraph ENSNodeSepolia["ENSNode 'Sepolia' instance"] + direction LR + ENSIndexerSepolia[ENSIndexer 'Sepolia' instance] + ENSApiSepolia[ENSApi 'Sepolia' instance] + end + end + + subgraph PostgreSQLServer["PostgreSQL Server instance"] + ENSDbMainnet[(ENSDb 'Mainnet' instance)] + PSMainnet(Ponder Schema) + NSMainnet(ENSNode Schema) + EISMainnet@{ shape: procs, label: "ENSIndexer Schema" } + + ENSDbMainnet -->|1..1| PSMainnet + ENSDbMainnet -->|1..1| NSMainnet + ENSDbMainnet -->|1..*| EISMainnet + + + ENSDbSepolia[(ENSDb 'Sepolia' instance)] + PSSepolia(Ponder Schema) + NSSepolia(ENSNode Schema) + EISSepolia@{ shape: procs, label: "ENSIndexer Schema" } + + ENSDbSepolia -->|1..1| PSSepolia + ENSDbSepolia -->|1..1| NSSepolia + ENSDbSepolia -->|1..*| EISSepolia + end + + ENSIndexerMainnet -->|Write| ENSDbMainnet + ENSIndexerSepolia -->|Write| ENSDbSepolia + ENSApiMainnet -->|Read| ENSDbMainnet + ENSApiSepolia -->|Read| ENSDbSepolia + +``` + + + + +### Single PostgreSQL Server, Multiple ENSDb Instances + +A single PostgreSQL server can serve multiple ENSDb instances for different ENS Namespaces. This allows you to have separate ENSDb instances based on your needs. For example: +- Your production environment can have a dedicated ENSDb instance for ENS data from the ENS Namespace "mainnet". +- Your staging environment can have a separate ENSDb instance for ENS data from the ENS Namespace "sepolia". +- Your local development environment can have its own ENSDb instance for testing with local or ephemeral data from the ENS Namespace "ens-test-env". + +```mermaid +flowchart TB + subgraph PGServer["PostgreSQL Server"] + direction TB + Mainnet[(ENSDb Mainnet instance)] + Testnet[(ENSDb Sepolia instance)] + Devnet[(ENSDb ENS Test Env instance)] + end +``` + +Each ENSDb instance is an independent database containing complete ENS data for its respective environment. diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx new file mode 100644 index 0000000000..dd999644c7 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/usage/index.mdx @@ -0,0 +1,22 @@ +--- +title: Using ENSDb +description: Guides for integrating and using ENSDb in your applications. +sidebar: + label: Overview + order: 1 +--- +import { LinkCard } from '@astrojs/starlight/components'; + +You can interact with the ENSDb instance using the dedicated TypeScript SDK or the regular PostgreSQL interface. This section provides guides for both methods, as well as troubleshooting tips and answers to frequently asked questions. + + + + diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/sdk.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/sdk.mdx new file mode 100644 index 0000000000..6960983b1b --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/usage/sdk.mdx @@ -0,0 +1,41 @@ +--- +title: ENSDb SDK +sidebar: + label: ENSDb SDK + order: 2 +--- + +This page provides an overview of the ENSDb SDK and how to use it in your applications. + +## Intro + +For TypeScript projects, the [ENSDb SDK](/ensdb/concepts/glossary#ensdb-sdk) provides a convenient and efficient way to interact with your ENSDb instance. + +### Installation + +You can install the [`@ensnode/ensdb-sdk`](https://www.npmjs.com/package/@ensnode/ensdb-sdk) package from the NPM registry, using your preferred package manager: + +```bash +npm install @ensnode/ensdb-sdk +pnpm install @ensnode/ensdb-sdk +yarn add @ensnode/ensdb-sdk +``` + +### Example Usage + +```typescript +import { EnsDbReader } from '@ensnode/ensdb-sdk'; + +// Connect to a specific ENSDb instance by providing its connection string and +// the ENSIndexer Schema Name you want to query +const ensDbReader = new EnsDbReader(ensDbConnectionString, ensIndexerSchemaName); + +// Get domains from the ENSIndexer Schema +const v1Domains = await + ensDbReader.ensDb.select() + .from(ensDbReader.ensIndexerSchema.v1Domain) + .limit(10); + +// Get indexing status +const indexingStatus = await ensDbReader.getIndexingStatusSnapshot() +``` diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/sql.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/sql.mdx new file mode 100644 index 0000000000..ada38d026b --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/usage/sql.mdx @@ -0,0 +1,50 @@ +--- +title: ENSDb SQL +sidebar: + label: ENSDb SQL + order: 3 +--- + +## Intro +This page provides an overview of the ENSDb SQL interface and how to use it in your applications. + +## Example Queries + +### Connect with Any PostgreSQL Client + +Connect to an ENSDb instance (a PostgreSQL database). The examples below assume you that ENSDb instances are served from a PostgreSQL server at `host:5432` with databases named `ensdb_mainnet`, `ensdb_testnet`, and `ensdb_devnet`: + +```bash +# Production environment (mainnet data) +psql postgresql://user:password@host:5432/ensdb_mainnet + +# Pre-production environment (testnet data) +psql postgresql://user:password@host:5432/ensdb_testnet + +# Staging / local development environment +psql postgresql://user:password@host:5432/ensdb_devnet +``` + +### Discover Available Schemas + +Once connected to an ENSDb instance, discover its ENSIndexer Schemas: + +```sql +SELECT DISTINCT ens_indexer_schema_name +FROM ensnode.metadata; +``` + +### Query Data + +Query data from your ENSDb instance: + +```sql +-- Get domains from the ENSIndexer Schema with the `ensindexer_mainnet` ENSIndexer Schema Name +SELECT * FROM ensindexer_mainnet.v1_domains LIMIT 10; + +-- Get indexing status for the ENSNode Schema with the `ensindexer_mainnet` ENSIndexer Schema Name +SELECT * FROM "ensnode"."metadata" +WHERE ens_indexer_schema_name = 'ensindexer_mainnet' +AND key = 'ensindexer_indexing_status' +AND value -> 'data' -> 'omnichainSnapshot' ->> 'omnichainStatus' = 'omnichain-following'; +``` diff --git a/docs/ensnode.io/src/content/docs/ensdb/usage/troubleshooting.mdx b/docs/ensnode.io/src/content/docs/ensdb/usage/troubleshooting.mdx new file mode 100644 index 0000000000..61c0dd8e80 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/ensdb/usage/troubleshooting.mdx @@ -0,0 +1,8 @@ +--- +title: Troubleshooting +sidebar: + label: Troubleshooting + order: 99 +--- + +This page aggregates the most common issues users run into when operating an ENSDb instance.