# Database Schemas

This page explains the different database schemas in ENSDb, including the ENSNode Schema, and the modular ENSDb Writer Schema.

## Overview of ENSDb Schemas

An ENSDb instance can have two distinct kinds of database schemas: a single shared **ENSNode Schema** for operational metadata, and one **ENSDb Writer Schema** per running ENSDb Writer instance for all indexed ENS data. <a href="https://dbdiagram.io/d/ENSDb-69f0d325ddb9320fdc7bd564" target="_blank" rel="noopener noreferrer">View Interactive Diagram of ENSDb Schemas</a>.

## ENSNode Schema

The `ensnode` database schema contains shared operational metadata, called ENSNode Metadata, for the entire ENSDb database.

:::note[Multi-tenancy]
ENSDb supports multiple ENSDb Writer instances coexisting in the same database, each with its own isolated database schema. The ENSNode `metadata` table is the central registry that tracks all of them. Each instance identifies itself by writing rows scoped to its own database schema name via the `ens_indexer_schema_name` column.
:::

### ENSNode Metadata

Possible key-value pairs are defined by the `EnsNodeMetadata` union type. As of now, it includes: `EnsNodeMetadataIndexingMetadataContext`.

| Column                    | Type    | Nullable | Description                                                                                                                                                                                       |
| ------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ens_indexer_schema_name` | `text`  | no       | References the name of the ENSDb Writer Schema that the metadata record belongs to. This allows multi-tenancy where multiple ENSDb Writer instances can write to the same ENSNode Metadata table. |
| `key`                     | `text`  | no       | Allowed keys: `indexing_metadata_context`.                                                                                                                                                        |
| `value`                   | `jsonb` | no       | Guaranteed to be a serialized representation of a JSON object.                                                                                                                                    |

**Primary key:** `(ens_indexer_schema_name, key)` — ensures that there is only one record for each key per ENSDb Writer instance.

#### Known keys

| Key                         | TypeScript type                      | Description                                                          |
| --------------------------- | ------------------------------------ | -------------------------------------------------------------------- |
| `indexing_metadata_context` | `IndexingMetadataContextInitialized` | Stores indexing status and stack info for the ENSDb Writer instance. |

---

## ENSDb Writer Schema

Each ENSDb Writer instance owns a dedicated database schema in ENSDb. All indexed ENS data for that instance lives within it, fully isolated from other instances.

The ENSDb Writer Schema is modular and can be composed of multiple logical database sub-schemas. Each sub-schema is implemented for the exact requirements determined by a specific implementation of an [ENSNode Plugin](/docs/services/ensdb/concepts/glossary#ensnode-plugin).

---

### ENS Unigraph

The ENS Unigraph schema — the unified, polymorphic ENSv1 + ENSv2 data model — is documented in full in its canonical reference in the Integrate section:

:::tip[Unigraph Schema Reference]
The complete table-by-table reference for the Unigraph schema now lives at **[Unigraph Schema Reference](/docs/integrate/unigraph/schema-reference)**.
:::

---

### Protocol Acceleration

Defined in [`protocol-acceleration.schema.ts`](https://github.com/namehash/ensnode/blob/main/packages/ensdb-sdk/src/ensindexer-abstract/protocol-acceleration.schema.ts).

Provides accelerated lookups for the Resolution API. Rather than traversing the full namegraph at query time for common operations, this database sub-schema materializes the minimal state needed to answer resolution queries efficiently.

#### reverse_name_records

Tracks an Account's ENSIP-19 Reverse Name Records by CoinType.

:::caution[Not a source of truth for Primary Names]
This is **not** a cohesive, materialized index of all of an account's Primary Names. It is **only** an index of its ENSIP-19 Reverse Name Records stored by a `StandaloneReverseRegistrar`:

- `default.reverse`
- `[coinType].reverse`
- **Not** `*.addr.reverse`

These records **cannot** be queried directly and used as a source of truth — you **must** perform Forward Resolution to resolve a consistent set of an Account's ENSIP-19 Primary Names. These records are used to power Protocol Acceleration for those ReverseResolvers backed by a `StandaloneReverseRegistrar`.
:::

| Column      | Type          | Nullable | Description                                                                                                                     |
| ----------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `address`   | `text`        | no       | The account address. Part of primary key.                                                                                       |
| `coin_type` | `numeric(78)` | no       | ENSIP-19 coin type. Part of primary key.                                                                                        |
| `value`     | `text`        | no       | Represents the ENSIP-19 Reverse Name Record for a given `(address, coin_type)`. Guaranteed to be a non-empty `InterpretedName`. |

**Primary key:** `(address, coin_type)`.

#### domain_resolver_relations

Tracks Domain-Resolver relationships. This powers: (1) Domain-Resolver relationships within the GraphQL API, and (2) accelerated lookups of a Domain's Resolver within the Resolution API.

It is keyed by `(chain_id, address, domain_id)` to match the on-chain data model of Registry / (shadow)Registry Domain-Resolver relationships.

| Column      | Type     | Nullable | Description                                                                         |
| ----------- | -------- | -------- | ----------------------------------------------------------------------------------- |
| `chain_id`  | `bigint` | no       | Keyed by `(chain_id, address, domain_id)`. Part of primary key.                     |
| `address`   | `text`   | no       | The Registry (`ENSv1Registry` or `ENSv2Registry`)'s AccountId. Part of primary key. |
| `domain_id` | `text`   | no       | Part of primary key.                                                                |
| `resolver`  | `text`   | no       | The Domain's assigned Resolver's address. Always scoped to `chain_id`.              |

**Primary key:** `(chain_id, address, domain_id)`.

**Indexes:** `domain_id` — secondary lookup off the PK. The namegraph-walk recursive CTE in `get-domain-by-interpreted-name` left-joins on `domain_id` alone, which the PK (leading-column `chain_id, address`) cannot serve.

**Relations:** has one `resolver` via `(chain_id, resolver)`.

#### resolvers

Represents an individual `IResolver` contract observed by the indexer — either by emitting at least one event, or by being assigned as a Domain's Resolver (via a `domain_resolver_relations` row). Note that Resolver contracts can exist on-chain but not emit any events and still function properly, so checks against a Resolver's existence and metadata must be done at runtime.

| Column        | Type      | Nullable | Description                                                                                                                                                                                                                        |
| ------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`          | `text`    | no       | Keyed by `(chain_id, address)`. Primary key.                                                                                                                                                                                       |
| `chain_id`    | `bigint`  | no       | Chain the resolver contract is deployed on.                                                                                                                                                                                        |
| `address`     | `text`    | no       | Address of the resolver contract.                                                                                                                                                                                                  |
| `is_extended` | `boolean` | no       | Whether the Resolver implements ENSIP-10 wildcard resolution (`IExtendedResolver`, interfaceId `0x9061b923`), determined via a single cached `supportsInterface` RPC the first time the Resolver is observed. Defaults to `false`. |

**Indexes:** unique on `(chain_id, address)`.

**Relations:** has many `resolver_records`.

#### resolver_records

Tracks a set of records for a specified `node` within a `resolver` contract on `chain_id`.

Represents one `name` resolution record (see ENSIP-3), has many `resolver_address_records` (unique by `coin_type`, see ENSIP-9), and has many `resolver_text_records` (unique by key, see ENSIP-5).

:::caution[Not ENSIP-10 compliant]
These record values do **not** allow the caller to confidently resolve records for names without following Forward Resolution according to the ENS protocol. A direct query to the database for a record's value is not ENSIP-10 nor CCIP-Read compliant.
:::

| Column        | Type          | Nullable | Description                                                                                                                                                                                                                                                                                 |
| ------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`          | `text`        | no       | Keyed by `(chain_id, resolver, node)`. Primary key.                                                                                                                                                                                                                                         |
| `chain_id`    | `bigint`      | no       | Part of the composite key.                                                                                                                                                                                                                                                                  |
| `address`     | `text`        | no       | Resolver contract address. Part of the composite key.                                                                                                                                                                                                                                       |
| `node`        | `text`        | no       | The name's namehash. Part of the composite key.                                                                                                                                                                                                                                             |
| `name`        | `text`        | yes      | The reverse-resolution (ENSIP-3) `name()` record, used for Reverse Resolution. If present, guaranteed to be a non-empty `InterpretedName`.                                                                                                                                                  |
| `contenthash` | `text`        | yes      | ENSIP-7 contenthash raw bytes, or `null` if not set.                                                                                                                                                                                                                                        |
| `pubkeyX`     | `text`        | yes      | PubkeyResolver X coordinate. Invariant: both `pubkeyX` and `pubkeyY` are either both `null` or both set.                                                                                                                                                                                    |
| `pubkeyY`     | `text`        | yes      | PubkeyResolver Y coordinate. Invariant: both `pubkeyX` and `pubkeyY` are either both `null` or both set.                                                                                                                                                                                    |
| `dnszonehash` | `text`        | yes      | `IDNSZoneResolver` zone hash, or `null` if not set.                                                                                                                                                                                                                                         |
| `version`     | `numeric(78)` | yes      | `IVersionableResolver` version. `null` when no `VersionChanged` event has been seen for this `(chain_id, address, node)` — the resolver may not implement `IVersionableResolver`, or simply may never have been version-bumped. Consumers should treat `null` as "unknown" rather than `0`. |

**Indexes:** unique on `(chain_id, address, node)`.

**Relations:** belongs to one `resolvers` record via `(chain_id, address)`, has many `resolver_address_records`, has many `resolver_text_records`.

#### resolver_address_records

Tracks address records for a `node` by `coin_type` within a `resolver` on `chain_id`.

Keyed by `(chain_id, resolver, node, coin_type)`, where the composite key segment `(chain_id, resolver, node)` describes a `resolver_records` entity. A `resolver_address_record` is then additionally keyed by `coin_type`.

| Column      | Type          | Nullable | Description                                                                                                                                                                                                   |
| ----------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chain_id`  | `bigint`      | no       | Part of primary key.                                                                                                                                                                                          |
| `address`   | `text`        | no       | Resolver contract address. Part of primary key.                                                                                                                                                               |
| `node`      | `text`        | no       | Name namehash. Part of primary key.                                                                                                                                                                           |
| `coin_type` | `numeric(78)` | no       | All well-known CoinTypes fit into a JavaScript number but NOT a Postgres `integer`, and must be stored as `bigint`. Part of primary key.                                                                      |
| `value`     | `hex`         | no       | The value of the Address Record specified by `((chain_id, resolver, node), coin_type)`. Interpreted by `interpretAddressRecordValue` — see its implementation for additional context and specific guarantees. |

**Primary key:** `(chain_id, address, node, coin_type)`.

**Relations:** belongs to one `resolver_records` record via `(chain_id, address, node)`.

#### resolver_text_records

Tracks text records for a `node` by `key` within a `resolver` on `chain_id`.

Keyed by `(chain_id, resolver, node, key)`, where the composite key segment `(chain_id, resolver, node)` describes a `resolver_records` entity. A `resolver_text_record` is then additionally keyed by `key`.

| Column     | Type     | Nullable | Description                                                                                                                                                                                       |
| ---------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chain_id` | `bigint` | no       | Part of primary key.                                                                                                                                                                              |
| `address`  | `text`   | no       | Resolver contract address. Part of primary key.                                                                                                                                                   |
| `node`     | `text`   | no       | Name namehash. Part of primary key.                                                                                                                                                               |
| `key`      | `text`   | no       | Text record key. Part of primary key.                                                                                                                                                             |
| `value`    | `text`   | no       | The value of the Text Record specified by `((chain_id, resolver, node), key)`. Interpreted by `interpretTextRecordValue` — see its implementation for additional context and specific guarantees. |

**Primary key:** `(chain_id, address, node, key)`.

**Relations:** belongs to one `resolver_records` record via `(chain_id, address, node)`.

#### migrated_nodes_by_parent

Tracks the migration status of a node, keyed by `(parent_node, label_hash)`. Due to a security issue, ENS migrated from the `RegistryOld` contract to a new Registry contract. When indexing events, the indexer must ignore any events on `RegistryOld` for domains that have since been migrated to the new Registry.

The set of nodes registered in the (new) Registry contract on the ENS Root Chain is stored here. When a `RegistryOld#NewOwner` event is encountered (which emits both `parent_node` and `label_hash` directly), the relevant row is looked up here; if it exists, the event is ignored.

:::note
This logic is only necessary for the ENS Root Chain — the only chain that includes the Registry migration. This Registry migration tracking is isolated to the Protocol Acceleration plugin. The subgraph plugin implements its own Registry migration logic. By isolating this logic here, the Protocol Acceleration plugin can be run independently of other plugins. The Unigraph plugin depends on the Protocol Acceleration plugin in order to piggyback on this Registry migration logic.
:::

The composite key is chosen so that Ponder's profile-pattern matcher can decompose it from event args directly, keeping the read on the indexing-cache prefetch hot-path.

| Column        | Type   | Nullable |
| ------------- | ------ | -------- |
| `parent_node` | `text` | no       |
| `label_hash`  | `text` | no       |

**Primary key:** `(parent_node, label_hash)`.

#### migrated_nodes_by_node

Sibling lookup-by-namehash table for `migrated_nodes_by_parent`, keyed by `node`. The three `RegistryOld` handlers (`Transfer` / `NewTTL` / `NewResolver`) emit only the post-namehash `node` and cannot reconstruct the `(parent_node, label_hash)` pair without an unprofileable reverse lookup. Existence in this table is equivalent to existence in `migrated_nodes_by_parent`; both rows are written together by the migration helper. See [`protocol-acceleration/migrated-node-db-helpers.ts`](https://github.com/namehash/ensnode/blob/main/apps/ensindexer/src/lib/protocol-acceleration/migrated-node-db-helpers.ts) for the full rationale.

| Column | Type   | Nullable |
| ------ | ------ | -------- |
| `node` | `text` | no       |

**Primary key:** `node`.

---

### Registrars

Defined in [`registrars.schema.ts`](https://github.com/namehash/ensnode/blob/main/packages/ensdb-sdk/src/ensindexer-abstract/registrars.schema.ts).

Models the lifecycle of ENS name registrations and renewals as logical actions, aggregating data from multiple on-chain events (e.g. a `BaseRegistrar` event and a `RegistrarController` event) into a single record per logical action.

#### Enums

**`registrar_action_type`** — Types of "logical registrar action".

| Value          |
| -------------- |
| `registration` |
| `renewal`      |

#### subregistries

A "subregistry" represents a smart contract that manages the subnames of a given parent name.

The following simplifying assumptions are currently in place:

1. No two subregistries hold state for the same node.
2. The subregistry associated with name X in the ENS root registry exclusively holds state for subnames of X.

These assumptions hold for the current scope of indexing logic but may not hold as indexing expands to handle more complex scenarios.

| Column           | Type   | Nullable | Description                                                                                                                                                                                   |
| ---------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subregistry_id` | `text` | no       | Identifies the chainId and address of the smart contract associated with the subregistry. Guaranteed to be a fully lowercase string formatted according to the CAIP-10 standard. Primary key. |
| `node`           | `text` | no       | The node (namehash) of the name the subregistry manages subnames of. Examples: `eth`, `base.eth`, `linea.eth`. Guaranteed to be a fully lowercase hex string representation of 32 bytes.      |

**Indexes:** unique on `node`.

**Relations:** has many `registration_lifecycles`.

#### registration_lifecycles

A "registration lifecycle" represents a single cycle of a name being registered once, followed by renewals (expiry date extensions) any number of times.

:::caution
This data model only tracks the **most recently created** registration lifecycle record for a name, and does not track all registration lifecycle records for a name across time. Therefore, if a name goes through multiple cycles of (registration → expiry → release), this data model only stores data for the most recently created registration lifecycle.
:::

| Column           | Type          | Nullable | Description                                                                                                                                                                                                                                                                   |
| ---------------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `node`           | `text`        | no       | The node (namehash) of the FQDN of the domain the registration lifecycle is associated with. Guaranteed to be a subname of the node of the subregistry identified by `subregistry_id`. Guaranteed to be a fully lowercase hex string representation of 32 bytes. Primary key. |
| `subregistry_id` | `text`        | no       | Identifies the chainId and address of the subregistry smart contract that manages the registration lifecycle. Guaranteed to be a fully lowercase CAIP-10 string.                                                                                                              |
| `expires_at`     | `numeric(78)` | no       | Unix timestamp when the Registration Lifecycle is scheduled to expire.                                                                                                                                                                                                        |

**Indexes:** `subregistry_id`.

**Relations:** belongs to one `subregistries` record, has many `registrar_actions` records.

#### registrar_actions

Models "logical actions" rather than "events" because a single logical action, such as a single registration or renewal, may emit multiple on-chain events from multiple contracts where each individual event may only provide a subset of the data about the full logical action. Each logical action in this table is associated with a single transaction. A single transaction may perform any number of logical actions.

For example, consider the logical registrar action of registering a direct subname of `.eth`. This logical action spans interactions across multiple contracts:

1. The `EthBaseRegistrar` contract emits a `NameRegistered` event, enabling tracking of `node`, `incrementalDuration`, and `registrant`.
2. A `RegistrarController` contract emits its own `NameRegistered` event, enabling tracking of `baseCost`, `premium`, `total`, and `encodedReferrer`.

The state from both events is aggregated into a single logical registrar action.

| Column                 | Type                    | Nullable | Description                                                                                                                                                                                                                                                                                    |
| ---------------------- | ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                   | `text`                  | no       | Deterministic and globally unique identifier for the logical registrar action. Represents the _initial_ on-chain event associated with the action. Guaranteed to be the first element in `eventIds`. Primary key. See note below about the ID format.                                          |
| `type`                 | `registrar_action_type` | no       | `registration` or `renewal`.                                                                                                                                                                                                                                                                   |
| `subregistry_id`       | `text`                  | no       | The ID of the subregistry the action was taken on. Identifies the chainId and address of the associated subregistry smart contract. Guaranteed to be a fully lowercase CAIP-10 string.                                                                                                         |
| `node`                 | `text`                  | no       | The node (namehash) of the FQDN of the domain associated with the action. Guaranteed to be a fully lowercase hex string representation of 32 bytes.                                                                                                                                            |
| `incremental_duration` | `numeric(78)`           | no       | Duration added to the registration by this action, in seconds. May be `0`. See detailed description below.                                                                                                                                                                                     |
| `base_cost`            | `numeric(78)`           | yes      | Base cost in wei. Guaranteed to be `null` if and only if `total` is `null`. Otherwise a non-negative value.                                                                                                                                                                                    |
| `premium`              | `numeric(78)`           | yes      | Premium cost in wei above `base_cost`. Guaranteed to be `null` if and only if `total` is `null`. Guaranteed to be zero when `type` is `renewal`.                                                                                                                                               |
| `total`                | `numeric(78)`           | yes      | Total cost in wei, equal to the sum of `base_cost` and `premium`. Guaranteed to be `null` if and only if both `base_cost` and `premium` are `null`.                                                                                                                                            |
| `registrant`           | `text`                  | no       | Identifies the address that initiated the action and is paying `total` (if applicable). May not be the owner of the name — there are no restrictions on who may renew a name, and the initial owner may be distinct from the registrant. Guaranteed to be a fully lowercase address.           |
| `encoded_referrer`     | `text`                  | yes      | The raw 32-byte referrer value emitted on-chain. `null` if no referrer information was present in the indexed events.                                                                                                                                                                          |
| `decoded_referrer`     | `text`                  | yes      | The referrer address decoded from `encoded_referrer` using strict left-zero-padding validation. `null` if `encoded_referrer` is `null`. May be the zero address to represent that an `encoded_referrer` is defined but interpreted as no referrer. Guaranteed to be a fully lowercase address. |
| `block_number`         | `numeric(78)`           | no       | Block number that includes the action. The chainId of this block is the same as is referenced in `subregistry_id`.                                                                                                                                                                             |
| `timestamp`            | `numeric(78)`           | no       | Unix timestamp of the block referenced by `block_number`.                                                                                                                                                                                                                                      |
| `transaction_hash`     | `text`                  | no       | Transaction hash of the action. The chainId of this transaction is the same as referenced in `subregistry_id`. Note that a single transaction may be associated with any number of logical registrar actions.                                                                                  |
| `event_ids`            | `text[]`                | no       | Array of Ponder event IDs that contributed to this record. Guarantees: at least 1 element; ordered chronologically by `log_index` within `block_number`; the first element equals the `id` of this record.                                                                                     |

**Indexes:** `decoded_referrer`, `timestamp`.

**Relations:** belongs to one `registration_lifecycles` record via `node`.

##### id format

The `id` value is a Ponder checkpoint string — a fixed-length decimal string encoding the following fields (left to right, most to least significant):

| Field               | Width (digits) | Description                               |
| ------------------- | -------------- | ----------------------------------------- |
| `block_timestamp`   | 10             | Unix seconds timestamp of the block       |
| `chain_id`          | 16             | EIP-155 chain ID                          |
| `block_number`      | 16             | Block number                              |
| `transaction_index` | 16             | Index of the transaction within the block |
| `event_type`        | 1              | Internal Ponder event type (always `5`)   |
| `event_index`       | 16             | Index of the event within the transaction |

All fields are zero-padded to their fixed widths, so the string has constant length and lexicographic order equals chronological order. Because all registrar actions originate from Ponder log (smart-contract event) handlers, every `id` shares the same `event_type` digit (`5`), making direct lexicographic or bigint comparison safe for establishing total chronological order.

##### incremental_duration semantics

If `type` is `registration`: represents the duration between `block_timestamp` and the initial `expires_at` value that the associated registration lifecycle will be initialized with.

If `type` is `renewal`: represents the incremental increase in duration made to the `expires_at` value in the associated registration lifecycle. A registration lifecycle may be extended via renewal even after it expires, as long as it is still within its grace period.

**Example:** A registration lifecycle is scheduled to expire on Jan 1 midnight UTC. It is currently 30 days past expiration, with 60 days of grace period remaining.

1. A renewal with 10 days incremental duration: the lifecycle remains "expired" but now has 70 days of grace period remaining.
2. A renewal with 50 days incremental duration: the lifecycle becomes "active" again but will expire again in 20 days.

After the grace period expires entirely, the name is considered "released" and can no longer be renewed — it must be registered again, starting a new lifecycle.

#### \_ensindexer_registrar_action_metadata

:::caution[Internal implementation detail]
This table is an **internal implementation detail of ENSIndexer** and should not be queried outside of ENSIndexer. It is used to temporarily store data during event handler execution to correlate multiple on-chain events into a single `registrar_actions` record.

Multiple logical registrar actions may be taken on the same `node` in the same `transaction_hash` (e.g. a single transaction that registers and then renews a name twice). To support this, when the last event handler for a logical registrar action has completed its processing, the record referenced by `logical_event_key` must be removed.
:::

**`_ensindexer_registrar_action_metadata_type`** enum:

| Value                              |
| ---------------------------------- |
| `CURRENT_LOGICAL_REGISTRAR_ACTION` |

| Column              | Type                                         | Nullable | Description                                                                                                                                                                                                                                          |
| ------------------- | -------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `metadata_type`     | `_ensindexer_registrar_action_metadata_type` | no       | The type of internal registrar action metadata being stored. Primary key.                                                                                                                                                                            |
| `logical_event_key` | `text`                                       | no       | A fully lowercase string formatted as `{domain_id}:{transaction_hash}`.                                                                                                                                                                              |
| `logical_event_id`  | `text`                                       | no       | Holds the `id` value of the existing `registrar_actions` record currently being built as an aggregation of on-chain events. Used by subsequent event handlers to identify which logical registrar action to aggregate additional indexed state into. |

---

### Subgraph

Defined in [`subgraph.schema.ts`](https://github.com/namehash/ensnode/blob/main/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts).

A complete re-implementation of the legacy ENS Subgraph data model. When the `subgraph_` prefix is stripped and the resulting database schema is paired with `@ensnode/ponder-subgraph`, the resulting GraphQL API is fully compatible with the legacy ENS Subgraph.

#### subgraph_domains

| Column                | Type          | Nullable | Description                                                                                                                                                                                                                                                                                                                  |
| --------------------- | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                  | `text`        | no       | The namehash of the name. Primary key.                                                                                                                                                                                                                                                                                       |
| `name`                | `text`        | yes      | The ENS name that this Domain represents. In subgraph-compatible mode: `null` for the root node, or a Subgraph Interpreted Name. Otherwise: an Interpreted Name (normalized, or consisting entirely of Interpreted Labels). The root node's name is `''` (empty string) rather than `null` in practice.                      |
| `label_name`          | `text`        | yes      | The label associated with the Domain. In subgraph-compatible mode: `null` for the root node or a subgraph-unindexable label; otherwise a Subgraph Interpreted Label. In non-compatible mode: `null` exclusively for the root node; otherwise a normalized label, or an Encoded LabelHash for unknown or unnormalized labels. |
| `labelhash`           | `text`        | yes      | `keccak256(label_name)`.                                                                                                                                                                                                                                                                                                     |
| `parent_id`           | `text`        | yes      | The namehash (`id`) of the parent name.                                                                                                                                                                                                                                                                                      |
| `subdomain_count`     | `integer`     | no       | The number of subdomains. Default `0`.                                                                                                                                                                                                                                                                                       |
| `resolved_address_id` | `text`        | yes      | Address logged from the current resolver, if any.                                                                                                                                                                                                                                                                            |
| `resolver_id`         | `text`        | yes      | The resolver that controls the domain's settings.                                                                                                                                                                                                                                                                            |
| `ttl`                 | `numeric(78)` | yes      | The time-to-live (TTL) value of the domain's records.                                                                                                                                                                                                                                                                        |
| `is_migrated`         | `boolean`     | no       | Indicates whether the domain has been migrated to a new registrar. Default `false`.                                                                                                                                                                                                                                          |
| `created_at`          | `numeric(78)` | no       | The time when the domain was created.                                                                                                                                                                                                                                                                                        |
| `owner_id`            | `text`        | no       | The account that owns the domain.                                                                                                                                                                                                                                                                                            |
| `registrant_id`       | `text`        | yes      | The account that owns the ERC721 NFT for the domain.                                                                                                                                                                                                                                                                         |
| `wrapped_owner_id`    | `text`        | yes      | The account that owns the wrapped domain.                                                                                                                                                                                                                                                                                    |
| `expiry_date`         | `numeric(78)` | yes      | The expiry date for the domain, from either the registration or the wrapped domain if PCC is burned.                                                                                                                                                                                                                         |

**Indexes:**

- `name` — hash index, because some `name` values exceed the btree max row size (8191 bytes).
- `name` — GIN trigram index for partial-match filters (`_contains`, `_starts_with`, `_ends_with`).
- `labelhash`, `parent_id`, `owner_id`, `registrant_id`, `wrapped_owner_id`, `resolved_address_id`.

**Relations:** has one `subgraph_accounts` record (resolved_address), has one `subgraph_accounts` record (owner), has one `subgraph_accounts` record (registrant), has one `subgraph_accounts` record (wrapped_owner), has one `subgraph_resolvers` record, has one `subgraph_domains` record (parent), has many `subgraph_domains` records (subdomains), has one `subgraph_wrapped_domains` record, has one `subgraph_registrations` record, has many domain event tables.

#### subgraph_accounts

| Column | Type   | Nullable |
| ------ | ------ | -------- |
| `id`   | `text` | no       |

**Relations:** has many `subgraph_domains` records, has many `subgraph_wrapped_domains` records, has many `subgraph_registrations` records.

#### subgraph_resolvers

| Column         | Type            | Nullable | Description                                                                                                             |
| -------------- | --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `id`           | `text`          | no       | Unique identifier: concatenation of the domain namehash and the resolver address. Primary key.                          |
| `domain_id`    | `text`          | no       | The domain that this resolver is associated with.                                                                       |
| `address`      | `text`          | no       | The address of the resolver contract.                                                                                   |
| `addr_id`      | `text`          | yes      | The current value of the `addr` record for this resolver, as determined by the associated events.                       |
| `content_hash` | `text`          | yes      | The content hash for this resolver, in binary format.                                                                   |
| `texts`        | `text[]`        | yes      | The set of observed text record keys for this resolver. Nullable (not defaulting to `[]`) to match subgraph behavior.   |
| `coin_types`   | `numeric(78)[]` | yes      | The set of observed SLIP-44 coin types for this resolver. Nullable (not defaulting to `[]`) to match subgraph behavior. |

**Indexes:** `domain_id`.

**Relations:** has one `subgraph_accounts` record (addr), has one `subgraph_domains` record, has many resolver event tables.

#### subgraph_registrations

| Column              | Type          | Nullable | Description                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------- | ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                | `text`        | no       | The unique identifier of the registration (namehash). Primary key.                                                                                                                                                                                                                                                                                                                                 |
| `domain_id`         | `text`        | no       | The domain name associated with the registration.                                                                                                                                                                                                                                                                                                                                                  |
| `registration_date` | `numeric(78)` | no       | The registration date of the domain.                                                                                                                                                                                                                                                                                                                                                               |
| `expiry_date`       | `numeric(78)` | no       | The expiry date of the domain.                                                                                                                                                                                                                                                                                                                                                                     |
| `cost`              | `numeric(78)` | yes      | The cost associated with the domain registration.                                                                                                                                                                                                                                                                                                                                                  |
| `registrant_id`     | `text`        | no       | The account that registered the domain.                                                                                                                                                                                                                                                                                                                                                            |
| `label_name`        | `text`        | yes      | The label associated with the domain registration. In subgraph-compatible mode: `null` for a subgraph-unindexable label; otherwise a Subgraph Interpreted Label. In non-compatible mode: a normalized label, or an Encoded LabelHash for unnormalized labels. `null` is not expected in practice because there is no Registration entity for the root node (the only node with a null label_name). |

**Indexes:** `domain_id`, `registration_date`, `expiry_date`.

**Relations:** has one `subgraph_domains` record, has one `subgraph_accounts` record (registrant), has many registration event tables.

#### subgraph_wrapped_domains

| Column        | Type          | Nullable | Description                                                                                                                                                                                                                                                                                                                                                                            |
| ------------- | ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`          | `text`        | no       | The unique identifier for each instance of the WrappedDomain entity. Primary key.                                                                                                                                                                                                                                                                                                      |
| `domain_id`   | `text`        | no       | The domain that is wrapped by this WrappedDomain.                                                                                                                                                                                                                                                                                                                                      |
| `expiry_date` | `numeric(78)` | no       | The expiry date of the wrapped domain.                                                                                                                                                                                                                                                                                                                                                 |
| `fuses`       | `integer`     | no       | The number of fuses remaining on the wrapped domain.                                                                                                                                                                                                                                                                                                                                   |
| `owner_id`    | `text`        | no       | The account that owns this WrappedDomain.                                                                                                                                                                                                                                                                                                                                              |
| `name`        | `text`        | yes      | The name that this WrappedDomain represents. Names are emitted by the NameWrapper contract as DNS-Encoded Names which may be malformed, resulting in `null`. In subgraph-compatible mode: `null` for malformed or subgraph-unindexable labels; otherwise a Subgraph Interpreted Label. In non-compatible mode: `null` for a malformed DNS-Encoded Name; otherwise an Interpreted Name. |

**Indexes:** `domain_id`.

**Relations:** has one `subgraph_domains` record, has one `subgraph_accounts` record (owner).

#### Event tables

All event tables share the base columns `id` (primary key), `block_number`, and `transaction_id`. Domain event tables additionally carry `domain_id`; registration event tables carry `registration_id`; resolver event tables carry `resolver_id`. The indexes on each event table are `(domain_id/resolver_id/registration_id)` for reverse lookups and `(domain_id/resolver_id/registration_id, id)` for sorted pagination.

**Domain event tables**

| Table                        | Additional columns                         |
| ---------------------------- | ------------------------------------------ |
| `subgraph_transfers`         | `owner_id`                                 |
| `subgraph_new_owners`        | `owner_id`, `parent_domain_id`             |
| `subgraph_new_resolvers`     | `resolver_id`                              |
| `subgraph_new_ttls`          | `ttl`                                      |
| `subgraph_wrapped_transfers` | `owner_id`                                 |
| `subgraph_name_wrapped`      | `name`, `fuses`, `owner_id`, `expiry_date` |
| `subgraph_name_unwrapped`    | `owner_id`                                 |
| `subgraph_fuses_set`         | `fuses`                                    |
| `subgraph_expiry_extended`   | `expiry_date`                              |

**Registration event tables**

| Table                       | Additional columns             |
| --------------------------- | ------------------------------ |
| `subgraph_name_registered`  | `registrant_id`, `expiry_date` |
| `subgraph_name_renewed`     | `expiry_date`                  |
| `subgraph_name_transferred` | `new_owner_id`                 |

**Resolver event tables**

| Table                             | Additional columns                 |
| --------------------------------- | ---------------------------------- |
| `subgraph_addr_changed`           | `addr_id`                          |
| `subgraph_multicoin_addr_changed` | `coin_type`, `addr`                |
| `subgraph_name_changed`           | `name`                             |
| `subgraph_abi_changed`            | `content_type`                     |
| `subgraph_pubkey_changed`         | `x`, `y`                           |
| `subgraph_text_changed`           | `key`, `value`                     |
| `subgraph_contenthash_changed`    | `hash`                             |
| `subgraph_interface_changed`      | `interface_id`, `implementer`      |
| `subgraph_authorisation_changed`  | `owner`, `target`, `is_authorized` |
| `subgraph_version_changed`        | `version`                          |

---

### TokenScope

Defined in [`tokenscope.schema.ts`](https://github.com/namehash/ensnode/blob/main/packages/ensdb-sdk/src/ensindexer-abstract/tokenscope.schema.ts).

Tracks ENS-related NFT token ownership and secondary market sales via the Seaport protocol.

#### name_sales

| Column             | Type          | Nullable | Description                                                                                                                                                                          |
| ------------------ | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`               | `text`        | no       | Unique and deterministic identifier of the on-chain event associated with the sale. Composite key format: `{chain_id}-{block_number}-{log_index}` (e.g. `1-1234567-5`). Primary key. |
| `chain_id`         | `bigint`      | no       | The chain where the sale occurred.                                                                                                                                                   |
| `block_number`     | `numeric(78)` | no       | The block number on `chain_id` where the sale occurred.                                                                                                                              |
| `log_index`        | `integer`     | no       | The log index position of the sale event within `block_number`.                                                                                                                      |
| `transaction_hash` | `text`        | no       | The EVM transaction hash on `chain_id` associated with the sale.                                                                                                                     |
| `order_hash`       | `text`        | no       | The Seaport order hash.                                                                                                                                                              |
| `contract_address` | `text`        | no       | The address of the contract on `chain_id` that manages `token_id`.                                                                                                                   |
| `token_id`         | `numeric(78)` | no       | The tokenId managed by `contract_address` that was sold.                                                                                                                             |
| `asset_namespace`  | `text`        | no       | The CAIP-19 Asset Namespace of the token that was sold. Either `erc721` or `erc1155`.                                                                                                |
| `asset_id`         | `text`        | no       | The CAIP-19 Asset ID of the token that was sold. A globally unique reference to the specific asset.                                                                                  |
| `domain_id`        | `text`        | no       | The namehash (Node) of the ENS domain that was sold.                                                                                                                                 |
| `buyer`            | `text`        | no       | The account that bought the token controlling ownership of `domain_id` from the seller, for the amount of currency associated with the sale.                                         |
| `seller`           | `text`        | no       | The account that sold the token controlling ownership of `domain_id` to the buyer, for the amount of currency associated with the sale.                                              |
| `currency`         | `text`        | no       | Currency of the payment. One of: ETH, USDC, or DAI.                                                                                                                                  |
| `amount`           | `numeric(78)` | no       | The amount of currency paid, denominated in the smallest unit. ETH/WETH: wei (1 ETH = 10^18). USDC: micro-units (1 USDC = 10^6). DAI: wei-equivalent (1 DAI = 10^18).                |
| `timestamp`        | `numeric(78)` | no       | Unix timestamp of the block when the sale occurred.                                                                                                                                  |

**Indexes:** `domain_id`, `asset_id`, `buyer`, `seller`, `timestamp`.

#### name_tokens

After an NFT is indexed, it is never deleted from the index. When an indexed NFT is burned on-chain, its record is retained and its `mint_status` is updated to `burned`. If the NFT is minted again after being burned, `mint_status` is updated back to `minted`.

| Column             | Type          | Nullable | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------ | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`               | `text`        | no       | The CAIP-19 Asset ID of the token. A globally unique reference to this token. Primary key.                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `domain_id`        | `text`        | no       | The namehash (Node) of the ENS name associated with the token. An ENS name may have more than one distinct token across time. It is also possible for multiple distinct tokens for an ENS name to have a `mint_status` of `minted` at the same time — for example, when a direct subname of `.eth` is wrapped by the NameWrapper (one `minted` token managed by the `BaseRegistrar`, owned by the NameWrapper; one `minted` token managed by the NameWrapper, owned by the effective owner).                                |
| `chain_id`         | `bigint`      | no       | The chain that manages the token.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `contract_address` | `text`        | no       | The address of the contract on `chain_id` that manages the token.                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `token_id`         | `numeric(78)` | no       | The tokenId of the token managed by `contract_address`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `asset_namespace`  | `text`        | no       | The CAIP-19 Asset Namespace of the token. Either `erc721` or `erc1155`.                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `owner`            | `text`        | no       | The account that owns the token. Value is the zero address if and only if `mint_status` is `burned`. Note: the owner of the token for a given `domain_id` may differ from the owner of the associated node in the registry. For example, if address X owns `foo.eth` in both the `BaseRegistrar` and the Registry, and X transfers registry ownership directly to Y, the `BaseRegistrar` token owner remains X. The `BaseRegistrar` implements a `reclaim` function allowing the token owner to reclaim registry ownership. |
| `mint_status`      | `text`        | no       | Either `minted` or `burned`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |

**Indexes:** `domain_id`, `owner`.